Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v0.20] [feature] add describe command (#2055) #2068

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions cmd/vclusterctl/cmd/describe.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions cmd/vclusterctl/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
211 changes: 211 additions & 0 deletions pkg/cli/describe_helm.go
Original file line number Diff line number Diff line change
@@ -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
}
62 changes: 62 additions & 0 deletions pkg/cli/describe_platform.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading