Skip to content

Commit 450e528

Browse files
authored
[feature] add describe command (#2055)
* [feat] added describe command * fixed conditions * now uses camelcase
1 parent bde5b8d commit 450e528

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed

cmd/vclusterctl/cmd/describe.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cmd
2+
3+
import (
4+
"cmp"
5+
"fmt"
6+
"os"
7+
8+
"github.com/loft-sh/log"
9+
"github.com/loft-sh/vcluster/pkg/cli"
10+
"github.com/loft-sh/vcluster/pkg/cli/config"
11+
"github.com/loft-sh/vcluster/pkg/cli/flags"
12+
pdefaults "github.com/loft-sh/vcluster/pkg/platform/defaults"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// DescribeCmd holds the describe cmd flags
17+
type DescribeCmd struct {
18+
*flags.GlobalFlags
19+
20+
output string
21+
log log.Logger
22+
project string
23+
}
24+
25+
// NewDescribeCmd creates a new command
26+
func NewDescribeCmd(globalFlags *flags.GlobalFlags, defaults *pdefaults.Defaults) *cobra.Command {
27+
cmd := &DescribeCmd{
28+
GlobalFlags: globalFlags,
29+
log: log.GetInstance(),
30+
}
31+
driver := ""
32+
33+
cobraCmd := &cobra.Command{
34+
Use: "describe",
35+
Short: "Describes a virtual cluster",
36+
Long: `#######################################################
37+
################## vcluster describe ##################
38+
#######################################################
39+
describes a virtual cluster
40+
41+
Example:
42+
vcluster describe test
43+
vcluster describe -o json test
44+
#######################################################
45+
`,
46+
Args: cobra.ExactArgs(1),
47+
RunE: func(cobraCmd *cobra.Command, args []string) error {
48+
return cmd.Run(cobraCmd, driver, args[0])
49+
},
50+
}
51+
p, _ := defaults.Get(pdefaults.KeyProject, "")
52+
53+
cobraCmd.Flags().StringVar(&driver, "driver", "", "The driver to use for managing the virtual cluster, can be either helm or platform.")
54+
cobraCmd.Flags().StringVarP(&cmd.output, "output", "o", "", "The format to use to display the information, can either be json or yaml")
55+
cobraCmd.Flags().StringVarP(&cmd.project, "project", "p", p, "The project to use")
56+
57+
return cobraCmd
58+
}
59+
60+
// Run executes the functionality
61+
func (cmd *DescribeCmd) Run(cobraCmd *cobra.Command, driver, name string) error {
62+
cfg := cmd.LoadedConfig(cmd.log)
63+
64+
// If driver has been passed as flag use it, otherwise read it from the config file
65+
driverType, err := config.ParseDriverType(cmp.Or(driver, string(cfg.Driver.Type)))
66+
if err != nil {
67+
return fmt.Errorf("parse driver type: %w", err)
68+
}
69+
if driverType == config.PlatformDriver {
70+
return cli.DescribePlatform(cobraCmd.Context(), cmd.GlobalFlags, os.Stdout, cmd.log, name, cmd.project, cmd.output)
71+
}
72+
73+
return cli.DescribeHelm(cobraCmd.Context(), cmd.GlobalFlags, os.Stdout, name, cmd.output)
74+
}

cmd/vclusterctl/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func BuildRoot(log log.Logger) (*cobra.Command, *flags.GlobalFlags, error) {
116116
rootCmd.AddCommand(NewConnectCmd(globalFlags))
117117
rootCmd.AddCommand(NewCreateCmd(globalFlags))
118118
rootCmd.AddCommand(NewListCmd(globalFlags))
119+
rootCmd.AddCommand(NewDescribeCmd(globalFlags, defaults))
119120
rootCmd.AddCommand(NewDeleteCmd(globalFlags))
120121
rootCmd.AddCommand(NewPauseCmd(globalFlags))
121122
rootCmd.AddCommand(NewResumeCmd(globalFlags))

pkg/cli/describe_helm.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/ghodss/yaml"
11+
"github.com/loft-sh/log"
12+
"github.com/loft-sh/vcluster/config"
13+
"github.com/loft-sh/vcluster/pkg/cli/find"
14+
"github.com/loft-sh/vcluster/pkg/cli/flags"
15+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/client-go/kubernetes"
17+
"k8s.io/client-go/tools/clientcmd"
18+
)
19+
20+
type DescribeOutput struct {
21+
Distro string `json:"distro"`
22+
Version string `json:"version"`
23+
BackingStore string `json:"backingStore"`
24+
ImageTags ImageTag `json:"imageTags"`
25+
}
26+
27+
type ImageTag struct {
28+
APIServer string `json:"apiServer,omitempty"`
29+
Syncer string `json:"syncer,omitempty"`
30+
Scheduler string `json:"scheduler,omitempty"`
31+
ControllerManager string `json:"controllerManager,omitempty"`
32+
}
33+
34+
func DescribeHelm(ctx context.Context, flags *flags.GlobalFlags, output io.Writer, name, format string) error {
35+
namespace := "vcluster-" + name
36+
if flags.Namespace != "" {
37+
namespace = flags.Namespace
38+
}
39+
40+
secretName := "vc-config-" + name
41+
42+
kConf := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{})
43+
rawConfig, err := kConf.RawConfig()
44+
if err != nil {
45+
return err
46+
}
47+
clientConfig, err := kConf.ClientConfig()
48+
if err != nil {
49+
return err
50+
}
51+
52+
clientset, err := kubernetes.NewForConfig(clientConfig)
53+
if err != nil {
54+
return err
55+
}
56+
57+
secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{})
58+
if err != nil {
59+
return err
60+
}
61+
62+
configBytes, ok := secret.Data["config.yaml"]
63+
if !ok {
64+
return fmt.Errorf("secret %s in namespace %s does not contain the expected 'config.yaml' field", secretName, namespace)
65+
}
66+
67+
switch format {
68+
case "yaml":
69+
_, err = output.Write(configBytes)
70+
return err
71+
case "json":
72+
b, err := yaml.YAMLToJSON(configBytes)
73+
if err != nil {
74+
return err
75+
}
76+
_, err = output.Write(b)
77+
return err
78+
}
79+
fVcluster, err := find.GetVCluster(ctx, rawConfig.CurrentContext, name, namespace, log.Discard)
80+
if err != nil {
81+
return err
82+
}
83+
84+
describeOutput := &DescribeOutput{}
85+
err = extractFromValues(describeOutput, configBytes, format, fVcluster.Version, output)
86+
if err != nil {
87+
return err
88+
}
89+
90+
describeOutput.Version = fVcluster.Version
91+
92+
b, err := yaml.Marshal(describeOutput)
93+
if err != nil {
94+
return err
95+
}
96+
_, err = output.Write(b)
97+
98+
return err
99+
}
100+
101+
func extractFromValues(d *DescribeOutput, configBytes []byte, format, version string, output io.Writer) error {
102+
conf := &config.Config{}
103+
err := yaml.Unmarshal(configBytes, conf)
104+
if err != nil {
105+
return err
106+
}
107+
108+
switch format {
109+
case "yaml":
110+
_, err := output.Write(configBytes)
111+
return err
112+
case "json":
113+
err := json.NewEncoder(output).Encode(conf)
114+
if err != nil {
115+
return err
116+
}
117+
default:
118+
d.Distro = conf.Distro()
119+
d.BackingStore = string(conf.BackingStoreType())
120+
syncer, api, scheduler, controllerManager := getImageTags(conf, version)
121+
d.ImageTags = ImageTag{
122+
Syncer: syncer,
123+
APIServer: api,
124+
Scheduler: scheduler,
125+
ControllerManager: controllerManager,
126+
}
127+
}
128+
129+
return nil
130+
}
131+
132+
func valueOrDefaultRegistry(value, def string) string {
133+
if value != "" {
134+
return value
135+
}
136+
if def != "" {
137+
return def
138+
}
139+
return "ghcr.io/loft-sh"
140+
}
141+
142+
func valueOrDefaultSyncerImage(value string) string {
143+
if value != "" {
144+
return value
145+
}
146+
return "vcluster-pro"
147+
}
148+
149+
func getImageTags(c *config.Config, version string) (syncer, api, scheduler, controllerManager string) {
150+
syncerConfig := c.ControlPlane.StatefulSet.Image
151+
defaultRegistry := c.ControlPlane.Advanced.DefaultImageRegistry
152+
153+
syncer = valueOrDefaultRegistry(syncerConfig.Registry, defaultRegistry) + "/" + valueOrDefaultSyncerImage(syncerConfig.Repository) + ":" + syncerConfig.Tag
154+
if syncerConfig.Tag == "" {
155+
// the chart uses the chart version for the syncer tag, so the tag isn't set by default
156+
syncer += version
157+
}
158+
159+
switch c.Distro() {
160+
case config.K8SDistro:
161+
k8s := c.ControlPlane.Distro.K8S
162+
163+
api = valueOrDefaultRegistry(k8s.APIServer.Image.Registry, defaultRegistry) + "/" + k8s.APIServer.Image.Repository + ":" + k8s.APIServer.Image.Tag
164+
if k8s.APIServer.Image.Repository == "" {
165+
// with the platform driver if only the registry is set we won't be able to display complete info
166+
api = ""
167+
}
168+
169+
scheduler = valueOrDefaultRegistry(k8s.Scheduler.Image.Registry, defaultRegistry) + "/" + k8s.Scheduler.Image.Repository + ":" + k8s.Scheduler.Image.Tag
170+
if k8s.Scheduler.Image.Repository == "" {
171+
// with the platform driver if only the registry is set we won't be able to display complete info
172+
scheduler = ""
173+
}
174+
175+
controllerManager = valueOrDefaultRegistry(k8s.ControllerManager.Image.Registry, defaultRegistry) + "/" + k8s.ControllerManager.Image.Repository + ":" + k8s.ControllerManager.Image.Tag
176+
if k8s.ControllerManager.Image.Repository == "" {
177+
// with the platform driver if only the registry is set we won't be able to display complete info
178+
controllerManager = ""
179+
}
180+
181+
case config.K3SDistro:
182+
k3s := c.ControlPlane.Distro.K3S
183+
184+
api = valueOrDefaultRegistry(k3s.Image.Registry, defaultRegistry) + "/" + k3s.Image.Repository + ":" + k3s.Image.Tag
185+
if strings.HasPrefix(api, valueOrDefaultRegistry(k3s.Image.Registry, defaultRegistry)+"/:") {
186+
// with the platform driver if only the registry is set we won't be able to display complete info
187+
api = ""
188+
}
189+
case config.K0SDistro:
190+
k0s := c.ControlPlane.Distro.K0S
191+
192+
api = valueOrDefaultRegistry(k0s.Image.Registry, defaultRegistry) + "/" + k0s.Image.Repository + ":" + k0s.Image.Tag
193+
194+
if strings.HasPrefix(api, valueOrDefaultRegistry(k0s.Image.Registry, defaultRegistry)+"/:") {
195+
// with the platform driver if only the registry is set we won't be able to display complete info
196+
api = ""
197+
}
198+
}
199+
200+
syncer = strings.TrimPrefix(syncer, "/")
201+
api = strings.TrimPrefix(api, "/")
202+
scheduler = strings.TrimPrefix(scheduler, "/")
203+
controllerManager = strings.TrimPrefix(controllerManager, "/")
204+
205+
syncer = strings.TrimSuffix(syncer, ":")
206+
api = strings.TrimSuffix(api, ":")
207+
scheduler = strings.TrimSuffix(scheduler, ":")
208+
controllerManager = strings.TrimSuffix(controllerManager, ":")
209+
210+
return syncer, api, scheduler, controllerManager
211+
}

pkg/cli/describe_platform.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
8+
"github.com/ghodss/yaml"
9+
"github.com/loft-sh/log"
10+
"github.com/loft-sh/vcluster/pkg/cli/flags"
11+
"github.com/loft-sh/vcluster/pkg/platform"
12+
)
13+
14+
func DescribePlatform(ctx context.Context, globalFlags *flags.GlobalFlags, output io.Writer, l log.Logger, name, projectName, format string) error {
15+
platformClient, err := platform.InitClientFromConfig(ctx, globalFlags.LoadedConfig(l))
16+
if err != nil {
17+
return err
18+
}
19+
20+
proVClusters, err := platform.ListVClusters(ctx, platformClient, name, projectName)
21+
if err != nil {
22+
return err
23+
}
24+
25+
// provclusters should be len(1), because 0 exits beforehand, and there's only 1
26+
// vcluster with a name in a project
27+
values := proVClusters[0].VirtualCluster.Status.VirtualCluster.HelmRelease.Values
28+
version := proVClusters[0].VirtualCluster.Status.VirtualCluster.HelmRelease.Chart.Version
29+
30+
switch format {
31+
case "yaml":
32+
_, err = output.Write([]byte(values))
33+
return err
34+
case "json":
35+
b, err := yaml.YAMLToJSON([]byte(values))
36+
if err != nil {
37+
return err
38+
}
39+
_, err = output.Write(b)
40+
return err
41+
}
42+
describeOutput := &DescribeOutput{}
43+
44+
describeOutput.Version = version
45+
46+
err = extractFromValues(describeOutput, []byte(values), format, version, output)
47+
if err != nil {
48+
return err
49+
}
50+
51+
if describeOutput.ImageTags.Syncer == "" {
52+
describeOutput.ImageTags.Syncer = fmt.Sprintf("ghcr.io/loft-sh/vcluster-pro:%s", version)
53+
}
54+
55+
b, err := yaml.Marshal(describeOutput)
56+
if err != nil {
57+
return err
58+
}
59+
_, err = output.Write(b)
60+
61+
return err
62+
}

0 commit comments

Comments
 (0)