From 9bc8217fcd3c97533f6b9d42bbd783a9d797a58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:14:36 +0200 Subject: [PATCH] cli: output CSI driver versions on `status` (#2128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Output CSI driver versions * Improve status output * Correctly update CSI version on upgrades --------- Signed-off-by: Daniel Weiße --- cli/internal/cmd/BUILD.bazel | 1 - cli/internal/cmd/status.go | 33 +++++------ cli/internal/cmd/status_test.go | 78 +++++++++++++------------- cli/internal/helm/BUILD.bazel | 1 + cli/internal/helm/client.go | 86 ++++++++++++++--------------- cli/internal/helm/loader.go | 9 ++- cli/internal/helm/serviceversion.go | 55 ++++++++++++++++++ 7 files changed, 159 insertions(+), 104 deletions(-) create mode 100644 cli/internal/helm/serviceversion.go diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 120cd9858..1de9c2346 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -136,7 +136,6 @@ go_test( "//bootstrapper/initproto", "//cli/internal/cloudcmd", "//cli/internal/clusterid", - "//cli/internal/helm", "//cli/internal/iamid", "//cli/internal/kubernetes", "//cli/internal/terraform", diff --git a/cli/internal/cmd/status.go b/cli/internal/cmd/status.go index 029606a20..15add6d87 100644 --- a/cli/internal/cmd/status.go +++ b/cli/internal/cmd/status.go @@ -78,6 +78,9 @@ func runStatus(cmd *cobra.Command, _ []string) error { if err != nil { return fmt.Errorf("setting up helm client: %w", err) } + helmVersionGetter := func() (fmt.Stringer, error) { + return helmClient.Versions() + } stableClient := kubernetes.NewStableClient(kubeClient) @@ -97,7 +100,7 @@ func runStatus(cmd *cobra.Command, _ []string) error { } variant := conf.GetAttestationConfig().GetVariant() - output, err := status(cmd.Context(), kubeClient, stableClient, helmClient, kubernetes.NewNodeVersionClient(unstructuredClient), variant) + output, err := status(cmd.Context(), kubeClient, stableClient, helmVersionGetter, kubernetes.NewNodeVersionClient(unstructuredClient), variant) if err != nil { return fmt.Errorf("getting status: %w", err) } @@ -107,7 +110,10 @@ func runStatus(cmd *cobra.Command, _ []string) error { } // status queries the cluster for the relevant status information and returns the output string. -func status(ctx context.Context, kubeClient kubeClient, cmClient configMapClient, helmClient helmClient, dynamicInterface kubernetes.DynamicInterface, attestVariant variant.Variant) (string, error) { +func status( + ctx context.Context, kubeClient kubeClient, cmClient configMapClient, getHelmVersions func() (fmt.Stringer, error), + dynamicInterface kubernetes.DynamicInterface, attestVariant variant.Variant, +) (string, error) { nodeVersion, err := kubernetes.GetConstellationVersion(ctx, dynamicInterface) if err != nil { return "", fmt.Errorf("getting constellation version: %w", err) @@ -139,7 +145,7 @@ func status(ctx context.Context, kubeClient kubeClient, cmClient configMapClient return "", fmt.Errorf("getting configured versions: %w", err) } - serviceVersions, err := helmClient.Versions() + serviceVersions, err := getHelmVersions() if err != nil { return "", fmt.Errorf("getting service versions: %w", err) } @@ -153,11 +159,14 @@ func status(ctx context.Context, kubeClient kubeClient, cmClient configMapClient } // statusOutput creates the status cmd output string by formatting the received information. -func statusOutput(targetVersions kubernetes.TargetVersions, serviceVersions helm.ServiceVersions, status map[string]kubernetes.NodeStatus, nodeVersion v1alpha1.NodeVersion, rawAttestationConfig string) string { +func statusOutput( + targetVersions kubernetes.TargetVersions, serviceVersions fmt.Stringer, + status map[string]kubernetes.NodeStatus, nodeVersion v1alpha1.NodeVersion, rawAttestationConfig string, +) string { builder := strings.Builder{} builder.WriteString(targetVersionsString(targetVersions)) - builder.WriteString(serviceVersionsString(serviceVersions)) + builder.WriteString(serviceVersions.String()) builder.WriteString(fmt.Sprintf("Cluster status: %s\n", nodeVersion.Status.Conditions[0].Message)) builder.WriteString(nodeStatusString(status, targetVersions)) builder.WriteString(fmt.Sprintf("Attestation config:\n%s", indentEntireStringWithTab(rawAttestationConfig))) @@ -194,17 +203,6 @@ func nodeStatusString(status map[string]kubernetes.NodeStatus, targetVersions ku 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 kubernetes.TargetVersions) string { builder := strings.Builder{} @@ -222,6 +220,3 @@ type kubeClient interface { type configMapClient interface { GetCurrentConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) } -type helmClient interface { - Versions() (helm.ServiceVersions, error) -} diff --git a/cli/internal/cmd/status_test.go b/cli/internal/cmd/status_test.go index b56016f56..5d071e16c 100644 --- a/cli/internal/cmd/status_test.go +++ b/cli/internal/cmd/status_test.go @@ -8,11 +8,10 @@ package cmd import ( "context" + "fmt" "testing" - "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - consemver "github.com/edgelesssys/constellation/v2/internal/semver" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,29 +21,29 @@ import ( "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 -` + attestationConfigOutput +const successOutput = targetVersions + versionsOutput + nodesUpToDateOutput + attestationConfigOutput -const inProgressOutput = `Target versions: +const inProgressOutput = targetVersions + versionsOutput + nodesInProgressOutput + attestationConfigOutput + +const targetVersions = `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 +` + +const nodesUpToDateOutput = `Cluster status: Node version of every node is up to date +` + +const nodesInProgressOutput = `Cluster status: Some node versions are out of date Image: 1/2 Kubernetes: 1/2 -` + attestationConfigOutput +` + +const versionsOutput = `Service versions: + Cilium: v1.0.0 + cert-manager: v1.0.0 + constellation-operators: v1.1.0 + constellation-services: v1.1.0 +` const attestationConfigOutput = `Attestation config: measurements: @@ -93,7 +92,6 @@ const attestationConfigOutput = `Attestation config: func TestStatus(t *testing.T) { testCases := map[string]struct { kubeClient stubKubeClient - helmClient stubHelmClient nodeVersion updatev1alpha1.NodeVersion dynamicErr error expectedOutput string @@ -117,9 +115,6 @@ func TestStatus(t *testing.T) { }, }, }, - helmClient: stubHelmClient{ - serviceVersions: helm.NewServiceVersions(consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 1, 0, ""), consemver.NewFromInt(1, 1, 0, "")), - }, nodeVersion: updatev1alpha1.NodeVersion{ Spec: updatev1alpha1.NodeVersionSpec{ ImageVersion: "v1.1.0", @@ -167,9 +162,6 @@ func TestStatus(t *testing.T) { }, }, }, - helmClient: stubHelmClient{ - serviceVersions: helm.NewServiceVersions(consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 1, 0, ""), consemver.NewFromInt(1, 1, 0, "")), - }, nodeVersion: updatev1alpha1.NodeVersion{ Spec: updatev1alpha1.NodeVersionSpec{ ImageVersion: "v1.1.0", @@ -197,7 +189,14 @@ func TestStatus(t *testing.T) { require.NoError(err) configMapper := stubConfigMapperAWSNitro{} variant := variant.AWSNitroTPM{} - output, err := status(context.Background(), tc.kubeClient, configMapper, tc.helmClient, &stubDynamicInterface{data: unstructured.Unstructured{Object: raw}, err: tc.dynamicErr}, variant) + output, err := status( + context.Background(), + tc.kubeClient, + configMapper, + stubGetVersions(versionsOutput), + &stubDynamicInterface{data: unstructured.Unstructured{Object: raw}, err: tc.dynamicErr}, + variant, + ) if tc.wantErr { assert.Error(err) return @@ -227,15 +226,6 @@ 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 @@ -248,3 +238,17 @@ func (s *stubDynamicInterface) GetCurrent(_ context.Context, _ string) (*unstruc func (s *stubDynamicInterface) Update(_ context.Context, _ *unstructured.Unstructured) (*unstructured.Unstructured, error) { return &s.data, s.err } + +func stubGetVersions(output string) func() (fmt.Stringer, error) { + return func() (fmt.Stringer, error) { + return stubServiceVersions{output}, nil + } +} + +type stubServiceVersions struct { + output string +} + +func (s stubServiceVersions) String() string { + return s.output +} diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index 25aa7e756..611402932 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -8,6 +8,7 @@ go_library( "client.go", "helm.go", "loader.go", + "serviceversion.go", "values.go", ], embedsrcs = [ diff --git a/cli/internal/helm/client.go b/cli/internal/helm/client.go index cdfebd1e8..f0200f531 100644 --- a/cli/internal/helm/client.go +++ b/cli/internal/helm/client.go @@ -118,10 +118,12 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, idFile clus return fmt.Errorf("loading chart: %w", err) } - // define target version the chart is upgraded to + // Get version of the chart embedded in the CLI + // This is the version we are upgrading to + // Since our bundled charts are embedded with version 0.0.0, + // we need to update them to the same version as the CLI var upgradeVersion semver.Semver - if info == constellationOperatorsInfo || info == constellationServicesInfo { - // ensure that the services chart has the same version as the CLI + if info == constellationOperatorsInfo || info == constellationServicesInfo || info == csiInfo { updateVersions(chart, constants.BinaryVersion()) upgradeVersion = config.MicroserviceVersion } else { @@ -221,23 +223,26 @@ func (c *Client) Versions() (ServiceVersions, error) { if err != nil { return ServiceVersions{}, fmt.Errorf("getting %s version: %w", constellationServicesInfo.releaseName, err) } - awsLBVersion, err := c.currentVersion(awsLBControllerInfo.releaseName) - if err != nil && !errors.Is(err, errReleaseNotFound) { - return ServiceVersions{}, fmt.Errorf("getting %s version: %w", awsLBControllerInfo.releaseName, err) + csiVersions, err := c.csiVersions() + if err != nil { + return ServiceVersions{}, fmt.Errorf("getting CSI versions: %w", err) } - res := ServiceVersions{ + serviceVersions := ServiceVersions{ cilium: ciliumVersion, certManager: certManagerVersion, constellationOperators: operatorsVersion, constellationServices: servicesVersion, - awsLBController: awsLBVersion, - } - if awsLBVersion != (semver.Semver{}) { - res.awsLBController = awsLBVersion + csiVersions: csiVersions, } - return res, nil + if awsLBVersion, err := c.currentVersion(awsLBControllerInfo.releaseName); err == nil { + serviceVersions.awsLBController = awsLBVersion + } else if !errors.Is(err, errReleaseNotFound) { + return ServiceVersions{}, fmt.Errorf("getting %s version: %w", awsLBControllerInfo.releaseName, err) + } + + return serviceVersions, nil } // currentVersion returns the version of the currently installed helm release. @@ -261,43 +266,36 @@ func (c *Client) currentVersion(release string) (semver.Semver, error) { return semver.New(rel[0].Chart.Metadata.Version) } -// ServiceVersions bundles the versions of all services that are part of Constellation. -type ServiceVersions struct { - cilium semver.Semver - certManager semver.Semver - constellationOperators semver.Semver - constellationServices semver.Semver - awsLBController semver.Semver -} - -// NewServiceVersions returns a new ServiceVersions struct. -func NewServiceVersions(cilium, certManager, constellationOperators, constellationServices semver.Semver) ServiceVersions { - return ServiceVersions{ - cilium: cilium, - certManager: certManager, - constellationOperators: constellationOperators, - constellationServices: constellationServices, +func (c *Client) csiVersions() (map[string]semver.Semver, error) { + packedChartRelease, err := c.actions.listAction(csiInfo.releaseName) + if err != nil { + return nil, fmt.Errorf("listing %s: %w", csiInfo.releaseName, err) } -} -// Cilium returns the version of the Cilium release. -func (s ServiceVersions) Cilium() semver.Semver { - return s.cilium -} + csiVersions := make(map[string]semver.Semver) -// CertManager returns the version of the cert-manager release. -func (s ServiceVersions) CertManager() semver.Semver { - return s.certManager -} + // No CSI driver installed + if len(packedChartRelease) == 0 { + return csiVersions, nil + } -// ConstellationOperators returns the version of the constellation-operators release. -func (s ServiceVersions) ConstellationOperators() semver.Semver { - return s.constellationOperators -} + if len(packedChartRelease) > 1 { + return nil, fmt.Errorf("multiple releases found for %s", csiInfo.releaseName) + } -// ConstellationServices returns the version of the constellation-services release. -func (s ServiceVersions) ConstellationServices() semver.Semver { - return s.constellationServices + if packedChartRelease[0] == nil || packedChartRelease[0].Chart == nil { + return nil, fmt.Errorf("received invalid release %s", csiInfo.releaseName) + } + + dependencies := packedChartRelease[0].Chart.Metadata.Dependencies + for _, dep := range dependencies { + var err error + csiVersions[dep.Name], err = semver.New(dep.Version) + if err != nil { + return nil, fmt.Errorf("parsing CSI version %q: %w", dep.Name, err) + } + } + return csiVersions, nil } // installNewRelease installs a previously not installed release on the cluster. diff --git a/cli/internal/helm/loader.go b/cli/internal/helm/loader.go index d646b6224..d8593e241 100644 --- a/cli/internal/helm/loader.go +++ b/cli/internal/helm/loader.go @@ -48,11 +48,14 @@ type chartInfo struct { } var ( - ciliumInfo = chartInfo{releaseName: "cilium", chartName: "cilium", path: "charts/cilium"} - certManagerInfo = chartInfo{releaseName: "cert-manager", chartName: "cert-manager", path: "charts/cert-manager"} + // Charts we fetch from an upstream with real versions. + ciliumInfo = chartInfo{releaseName: "cilium", chartName: "cilium", path: "charts/cilium"} + certManagerInfo = chartInfo{releaseName: "cert-manager", chartName: "cert-manager", path: "charts/cert-manager"} + awsLBControllerInfo = chartInfo{releaseName: "aws-load-balancer-controller", chartName: "aws-load-balancer-controller", path: "charts/aws-load-balancer-controller"} + + // Bundled charts with embedded with version 0.0.0. constellationOperatorsInfo = chartInfo{releaseName: "constellation-operators", chartName: "constellation-operators", path: "charts/edgeless/operators"} constellationServicesInfo = chartInfo{releaseName: "constellation-services", chartName: "constellation-services", path: "charts/edgeless/constellation-services"} - awsLBControllerInfo = chartInfo{releaseName: "aws-load-balancer-controller", chartName: "aws-load-balancer-controller", path: "charts/aws-load-balancer-controller"} csiInfo = chartInfo{releaseName: "constellation-csi", chartName: "constellation-csi", path: "charts/edgeless/csi"} ) diff --git a/cli/internal/helm/serviceversion.go b/cli/internal/helm/serviceversion.go new file mode 100644 index 000000000..a3d9ca57c --- /dev/null +++ b/cli/internal/helm/serviceversion.go @@ -0,0 +1,55 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package helm + +import ( + "fmt" + "strings" + + "github.com/edgelesssys/constellation/v2/internal/semver" +) + +// ServiceVersions bundles the versions of all services that are part of Constellation. +type ServiceVersions struct { + cilium semver.Semver + certManager semver.Semver + constellationOperators semver.Semver + constellationServices semver.Semver + awsLBController semver.Semver + csiVersions map[string]semver.Semver +} + +// String returns a string representation of the ServiceVersions struct. +func (s ServiceVersions) String() string { + builder := strings.Builder{} + builder.WriteString("Service versions:\n") + builder.WriteString(fmt.Sprintf("\tCilium: %s\n", s.cilium)) + builder.WriteString(fmt.Sprintf("\tcert-manager: %s\n", s.certManager)) + builder.WriteString(fmt.Sprintf("\tconstellation-operators: %s\n", s.constellationOperators)) + builder.WriteString(fmt.Sprintf("\tconstellation-services: %s\n", s.constellationServices)) + + if s.awsLBController != (semver.Semver{}) { + builder.WriteString(fmt.Sprintf("\taws-load-balancer-controller: %s\n", s.awsLBController)) + } + + builder.WriteString("\tCSI:") + if len(s.csiVersions) != 0 { + builder.WriteString("\n") + for name, csiVersion := range s.csiVersions { + builder.WriteString(fmt.Sprintf("\t\t%s: %s\n", name, csiVersion)) + } + } else { + builder.WriteString(" not installed\n") + } + + return builder.String() +} + +// ConstellationServices returns the version of the constellation-services release. +func (s ServiceVersions) ConstellationServices() semver.Semver { + return s.constellationServices +}