From 794e4b34b433a6719d1598d290d2a2faf6b13677 Mon Sep 17 00:00:00 2001 From: Jeremy Facchetti Date: Wed, 14 Aug 2024 16:18:12 +0200 Subject: [PATCH] [feature] add describe command (#2055) * [feat] added describe command * fixed conditions * now uses camelcase (cherry picked from commit 450e5288b68b728222acf5d0046b529a79ac6c8d) --- cmd/vclusterctl/cmd/describe.go | 74 +++++++++++ cmd/vclusterctl/cmd/root.go | 1 + pkg/cli/describe_helm.go | 211 ++++++++++++++++++++++++++++++++ pkg/cli/describe_platform.go | 62 ++++++++++ 4 files changed, 348 insertions(+) create mode 100644 cmd/vclusterctl/cmd/describe.go create mode 100644 pkg/cli/describe_helm.go create mode 100644 pkg/cli/describe_platform.go diff --git a/cmd/vclusterctl/cmd/describe.go b/cmd/vclusterctl/cmd/describe.go new file mode 100644 index 0000000000..37e1ef0814 --- /dev/null +++ b/cmd/vclusterctl/cmd/describe.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "cmp" + "fmt" + "os" + + "github.com/loft-sh/log" + "github.com/loft-sh/vcluster/pkg/cli" + "github.com/loft-sh/vcluster/pkg/cli/config" + "github.com/loft-sh/vcluster/pkg/cli/flags" + pdefaults "github.com/loft-sh/vcluster/pkg/platform/defaults" + "github.com/spf13/cobra" +) + +// DescribeCmd holds the describe cmd flags +type DescribeCmd struct { + *flags.GlobalFlags + + output string + log log.Logger + project string +} + +// NewDescribeCmd creates a new command +func NewDescribeCmd(globalFlags *flags.GlobalFlags, defaults *pdefaults.Defaults) *cobra.Command { + cmd := &DescribeCmd{ + GlobalFlags: globalFlags, + log: log.GetInstance(), + } + driver := "" + + cobraCmd := &cobra.Command{ + Use: "describe", + Short: "Describes a virtual cluster", + Long: `####################################################### +################## vcluster describe ################## +####################################################### +describes a virtual cluster + +Example: +vcluster describe test +vcluster describe -o json test +####################################################### + `, + Args: cobra.ExactArgs(1), + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.Run(cobraCmd, driver, args[0]) + }, + } + p, _ := defaults.Get(pdefaults.KeyProject, "") + + cobraCmd.Flags().StringVar(&driver, "driver", "", "The driver to use for managing the virtual cluster, can be either helm or platform.") + cobraCmd.Flags().StringVarP(&cmd.output, "output", "o", "", "The format to use to display the information, can either be json or yaml") + cobraCmd.Flags().StringVarP(&cmd.project, "project", "p", p, "The project to use") + + return cobraCmd +} + +// Run executes the functionality +func (cmd *DescribeCmd) Run(cobraCmd *cobra.Command, driver, name string) error { + cfg := cmd.LoadedConfig(cmd.log) + + // If driver has been passed as flag use it, otherwise read it from the config file + driverType, err := config.ParseDriverType(cmp.Or(driver, string(cfg.Driver.Type))) + if err != nil { + return fmt.Errorf("parse driver type: %w", err) + } + if driverType == config.PlatformDriver { + return cli.DescribePlatform(cobraCmd.Context(), cmd.GlobalFlags, os.Stdout, cmd.log, name, cmd.project, cmd.output) + } + + return cli.DescribeHelm(cobraCmd.Context(), cmd.GlobalFlags, os.Stdout, name, cmd.output) +} diff --git a/cmd/vclusterctl/cmd/root.go b/cmd/vclusterctl/cmd/root.go index be9d457883..d3824d514f 100644 --- a/cmd/vclusterctl/cmd/root.go +++ b/cmd/vclusterctl/cmd/root.go @@ -108,6 +108,7 @@ func BuildRoot(log log.Logger) (*cobra.Command, error) { rootCmd.AddCommand(NewConnectCmd(globalFlags)) rootCmd.AddCommand(NewCreateCmd(globalFlags)) rootCmd.AddCommand(NewListCmd(globalFlags)) + rootCmd.AddCommand(NewDescribeCmd(globalFlags, defaults)) rootCmd.AddCommand(NewDeleteCmd(globalFlags)) rootCmd.AddCommand(NewPauseCmd(globalFlags)) rootCmd.AddCommand(NewResumeCmd(globalFlags)) diff --git a/pkg/cli/describe_helm.go b/pkg/cli/describe_helm.go new file mode 100644 index 0000000000..4e47a46c73 --- /dev/null +++ b/pkg/cli/describe_helm.go @@ -0,0 +1,211 @@ +package cli + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/ghodss/yaml" + "github.com/loft-sh/log" + "github.com/loft-sh/vcluster/config" + "github.com/loft-sh/vcluster/pkg/cli/find" + "github.com/loft-sh/vcluster/pkg/cli/flags" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type DescribeOutput struct { + Distro string `json:"distro"` + Version string `json:"version"` + BackingStore string `json:"backingStore"` + ImageTags ImageTag `json:"imageTags"` +} + +type ImageTag struct { + APIServer string `json:"apiServer,omitempty"` + Syncer string `json:"syncer,omitempty"` + Scheduler string `json:"scheduler,omitempty"` + ControllerManager string `json:"controllerManager,omitempty"` +} + +func DescribeHelm(ctx context.Context, flags *flags.GlobalFlags, output io.Writer, name, format string) error { + namespace := "vcluster-" + name + if flags.Namespace != "" { + namespace = flags.Namespace + } + + secretName := "vc-config-" + name + + kConf := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) + rawConfig, err := kConf.RawConfig() + if err != nil { + return err + } + clientConfig, err := kConf.ClientConfig() + if err != nil { + return err + } + + clientset, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return err + } + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{}) + if err != nil { + return err + } + + configBytes, ok := secret.Data["config.yaml"] + if !ok { + return fmt.Errorf("secret %s in namespace %s does not contain the expected 'config.yaml' field", secretName, namespace) + } + + switch format { + case "yaml": + _, err = output.Write(configBytes) + return err + case "json": + b, err := yaml.YAMLToJSON(configBytes) + if err != nil { + return err + } + _, err = output.Write(b) + return err + } + fVcluster, err := find.GetVCluster(ctx, rawConfig.CurrentContext, name, namespace, log.Discard) + if err != nil { + return err + } + + describeOutput := &DescribeOutput{} + err = extractFromValues(describeOutput, configBytes, format, fVcluster.Version, output) + if err != nil { + return err + } + + describeOutput.Version = fVcluster.Version + + b, err := yaml.Marshal(describeOutput) + if err != nil { + return err + } + _, err = output.Write(b) + + return err +} + +func extractFromValues(d *DescribeOutput, configBytes []byte, format, version string, output io.Writer) error { + conf := &config.Config{} + err := yaml.Unmarshal(configBytes, conf) + if err != nil { + return err + } + + switch format { + case "yaml": + _, err := output.Write(configBytes) + return err + case "json": + err := json.NewEncoder(output).Encode(conf) + if err != nil { + return err + } + default: + d.Distro = conf.Distro() + d.BackingStore = string(conf.BackingStoreType()) + syncer, api, scheduler, controllerManager := getImageTags(conf, version) + d.ImageTags = ImageTag{ + Syncer: syncer, + APIServer: api, + Scheduler: scheduler, + ControllerManager: controllerManager, + } + } + + return nil +} + +func valueOrDefaultRegistry(value, def string) string { + if value != "" { + return value + } + if def != "" { + return def + } + return "ghcr.io/loft-sh" +} + +func valueOrDefaultSyncerImage(value string) string { + if value != "" { + return value + } + return "vcluster-pro" +} + +func getImageTags(c *config.Config, version string) (syncer, api, scheduler, controllerManager string) { + syncerConfig := c.ControlPlane.StatefulSet.Image + defaultRegistry := c.ControlPlane.Advanced.DefaultImageRegistry + + syncer = valueOrDefaultRegistry(syncerConfig.Registry, defaultRegistry) + "/" + valueOrDefaultSyncerImage(syncerConfig.Repository) + ":" + syncerConfig.Tag + if syncerConfig.Tag == "" { + // the chart uses the chart version for the syncer tag, so the tag isn't set by default + syncer += version + } + + switch c.Distro() { + case config.K8SDistro: + k8s := c.ControlPlane.Distro.K8S + + api = valueOrDefaultRegistry(k8s.APIServer.Image.Registry, defaultRegistry) + "/" + k8s.APIServer.Image.Repository + ":" + k8s.APIServer.Image.Tag + if k8s.APIServer.Image.Repository == "" { + // with the platform driver if only the registry is set we won't be able to display complete info + api = "" + } + + scheduler = valueOrDefaultRegistry(k8s.Scheduler.Image.Registry, defaultRegistry) + "/" + k8s.Scheduler.Image.Repository + ":" + k8s.Scheduler.Image.Tag + if k8s.Scheduler.Image.Repository == "" { + // with the platform driver if only the registry is set we won't be able to display complete info + scheduler = "" + } + + controllerManager = valueOrDefaultRegistry(k8s.ControllerManager.Image.Registry, defaultRegistry) + "/" + k8s.ControllerManager.Image.Repository + ":" + k8s.ControllerManager.Image.Tag + if k8s.ControllerManager.Image.Repository == "" { + // with the platform driver if only the registry is set we won't be able to display complete info + controllerManager = "" + } + + case config.K3SDistro: + k3s := c.ControlPlane.Distro.K3S + + api = valueOrDefaultRegistry(k3s.Image.Registry, defaultRegistry) + "/" + k3s.Image.Repository + ":" + k3s.Image.Tag + if strings.HasPrefix(api, valueOrDefaultRegistry(k3s.Image.Registry, defaultRegistry)+"/:") { + // with the platform driver if only the registry is set we won't be able to display complete info + api = "" + } + case config.K0SDistro: + k0s := c.ControlPlane.Distro.K0S + + api = valueOrDefaultRegistry(k0s.Image.Registry, defaultRegistry) + "/" + k0s.Image.Repository + ":" + k0s.Image.Tag + + if strings.HasPrefix(api, valueOrDefaultRegistry(k0s.Image.Registry, defaultRegistry)+"/:") { + // with the platform driver if only the registry is set we won't be able to display complete info + api = "" + } + } + + syncer = strings.TrimPrefix(syncer, "/") + api = strings.TrimPrefix(api, "/") + scheduler = strings.TrimPrefix(scheduler, "/") + controllerManager = strings.TrimPrefix(controllerManager, "/") + + syncer = strings.TrimSuffix(syncer, ":") + api = strings.TrimSuffix(api, ":") + scheduler = strings.TrimSuffix(scheduler, ":") + controllerManager = strings.TrimSuffix(controllerManager, ":") + + return syncer, api, scheduler, controllerManager +} diff --git a/pkg/cli/describe_platform.go b/pkg/cli/describe_platform.go new file mode 100644 index 0000000000..8979b0df68 --- /dev/null +++ b/pkg/cli/describe_platform.go @@ -0,0 +1,62 @@ +package cli + +import ( + "context" + "fmt" + "io" + + "github.com/ghodss/yaml" + "github.com/loft-sh/log" + "github.com/loft-sh/vcluster/pkg/cli/flags" + "github.com/loft-sh/vcluster/pkg/platform" +) + +func DescribePlatform(ctx context.Context, globalFlags *flags.GlobalFlags, output io.Writer, l log.Logger, name, projectName, format string) error { + platformClient, err := platform.InitClientFromConfig(ctx, globalFlags.LoadedConfig(l)) + if err != nil { + return err + } + + proVClusters, err := platform.ListVClusters(ctx, platformClient, name, projectName) + if err != nil { + return err + } + + // provclusters should be len(1), because 0 exits beforehand, and there's only 1 + // vcluster with a name in a project + values := proVClusters[0].VirtualCluster.Status.VirtualCluster.HelmRelease.Values + version := proVClusters[0].VirtualCluster.Status.VirtualCluster.HelmRelease.Chart.Version + + switch format { + case "yaml": + _, err = output.Write([]byte(values)) + return err + case "json": + b, err := yaml.YAMLToJSON([]byte(values)) + if err != nil { + return err + } + _, err = output.Write(b) + return err + } + describeOutput := &DescribeOutput{} + + describeOutput.Version = version + + err = extractFromValues(describeOutput, []byte(values), format, version, output) + if err != nil { + return err + } + + if describeOutput.ImageTags.Syncer == "" { + describeOutput.ImageTags.Syncer = fmt.Sprintf("ghcr.io/loft-sh/vcluster-pro:%s", version) + } + + b, err := yaml.Marshal(describeOutput) + if err != nil { + return err + } + _, err = output.Write(b) + + return err +}