/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package cmd import ( "context" "errors" "fmt" "strings" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubecmd" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/spf13/afero" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) // 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 the status of a constellation cluster.\n\n" + "Shows microservice, image, and Kubernetes versions installed in the cluster. Also shows status of current version upgrades.", Args: cobra.NoArgs, RunE: runStatus, } return cmd } // runStatus runs the terminate command. func runStatus(cmd *cobra.Command, _ []string) error { log, err := newCLILogger(cmd) if err != nil { return fmt.Errorf("creating logger: %w", err) } defer log.Sync() flags, err := parseStatusFlags(cmd) if err != nil { return fmt.Errorf("parsing flags: %w", err) } fileHandler := file.NewHandler(afero.NewOsFs()) helmClient, err := helm.NewReleaseVersionClient(constants.AdminConfFilename, log) if err != nil { return fmt.Errorf("setting up helm client: %w", err) } helmVersionGetter := func() (fmt.Stringer, error) { return helmClient.Versions() } fetcher := attestationconfigapi.NewFetcher() conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) } variant := conf.GetAttestationConfig().GetVariant() kubeClient, err := kubecmd.New(cmd.OutOrStdout(), constants.AdminConfFilename, fileHandler, log) if err != nil { return fmt.Errorf("setting up kubernetes client: %w", err) } output, err := status(cmd.Context(), helmVersionGetter, kubeClient, variant) 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, getHelmVersions func() (fmt.Stringer, error), kubeClient kubeCmd, attestVariant variant.Variant, ) (string, error) { nodeVersion, err := kubeClient.GetConstellationVersion(ctx) if err != nil { return "", fmt.Errorf("getting constellation version: %w", err) } attestationConfig, err := kubeClient.GetClusterAttestationConfig(ctx, attestVariant) if err != nil { return "", fmt.Errorf("getting attestation config: %w", err) } prettyYAML, err := yaml.Marshal(attestationConfig) if err != nil { return "", fmt.Errorf("marshalling attestation config: %w", err) } serviceVersions, err := getHelmVersions() if err != nil { return "", fmt.Errorf("getting service versions: %w", err) } status, err := kubeClient.ClusterStatus(ctx) if err != nil { return "", fmt.Errorf("getting cluster status: %w", err) } return statusOutput(nodeVersion, serviceVersions, status, string(prettyYAML)), nil } // statusOutput creates the status cmd output string by formatting the received information. func statusOutput( nodeVersion kubecmd.NodeVersion, serviceVersions fmt.Stringer, status map[string]kubecmd.NodeStatus, rawAttestationConfig string, ) string { builder := strings.Builder{} builder.WriteString(targetVersionsString(nodeVersion)) builder.WriteString(serviceVersions.String()) builder.WriteString(fmt.Sprintf("Cluster status: %s\n", nodeVersion.ClusterStatus())) builder.WriteString(nodeStatusString(status, nodeVersion)) builder.WriteString(fmt.Sprintf("Attestation config:\n%s", indentEntireStringWithTab(rawAttestationConfig))) return builder.String() } func indentEntireStringWithTab(input string) string { lines := strings.Split(input, "\n") for i, line := range lines[:len(lines)-1] { lines[i] = "\t" + line } return strings.Join(lines, "\n") } // nodeStatusString creates the node status part of the output string. func nodeStatusString(status map[string]kubecmd.NodeStatus, targetVersions kubecmd.NodeVersion) string { var upToDateImages int var upToDateK8s int for _, node := range status { if node.KubeletVersion() == targetVersions.KubernetesVersion() { upToDateK8s++ } if node.ImageVersion() == targetVersions.ImageReference() { 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() } // targetVersionsString creates the target versions part of the output string. func targetVersionsString(target kubecmd.NodeVersion) string { builder := strings.Builder{} builder.WriteString("Target versions:\n") builder.WriteString(fmt.Sprintf("\tImage: %s\n", target.ImageVersion())) builder.WriteString(fmt.Sprintf("\tKubernetes: %s\n", target.KubernetesVersion())) return builder.String() } type statusFlags struct { workspace string force bool } func parseStatusFlags(cmd *cobra.Command) (statusFlags, error) { workspace, err := cmd.Flags().GetString("workspace") if err != nil { return statusFlags{}, fmt.Errorf("getting config flag: %w", err) } force, err := cmd.Flags().GetBool("force") if err != nil { return statusFlags{}, fmt.Errorf("getting config flag: %w", err) } return statusFlags{ workspace: workspace, force: force, }, nil } type kubeCmd interface { ClusterStatus(ctx context.Context) (map[string]kubecmd.NodeStatus, error) GetConstellationVersion(ctx context.Context) (kubecmd.NodeVersion, error) GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, error) }