Skip to content

Commit

Permalink
[feat] added describe command
Browse files Browse the repository at this point in the history
  • Loading branch information
facchettos committed Aug 14, 2024
1 parent 1e5ccb5 commit 88637f7
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 0 deletions.
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 @@ -116,6 +116,7 @@ func BuildRoot(log log.Logger) (*cobra.Command, *flags.GlobalFlags, 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
207 changes: 207 additions & 0 deletions pkg/cli/describe_helm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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:"Backing Store"`
ImageTags ImageTag `json:"Image tags"`
}

type ImageTag struct {
APIServer string `json:"apiServer,omitempty"`
Syncer string `json:"syncer,omitempty"`
Scheduler string `json:"scheduler,omitempty"`
ControllerManager string `json:"controllerManger,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
// this is the case where the repository is not set
if strings.HasPrefix(api, valueOrDefaultRegistry(k8s.APIServer.Image.Registry, defaultRegistry)+"/:") {
// 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 strings.HasPrefix(scheduler, valueOrDefaultRegistry(k8s.Scheduler.Image.Registry, defaultRegistry)+"/:") {
// 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 strings.HasPrefix(controllerManager, valueOrDefaultRegistry(k8s.ControllerManager.Image.Registry, defaultRegistry)+"/:") {
// 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.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
}

0 comments on commit 88637f7

Please sign in to comment.