diff --git a/src/cli/README.md b/src/cli/README.md index 48c0344541..cf33759772 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -8,6 +8,7 @@ Manages virtual machine-related operations in your Kubernetes cluster. | Command | Description | |--------------------|------------------------------------------------------------------------| | ansible-inventory | Generate ansible inventory from virtual machines | +| collect-debug-info | Collect debug information for VM: configuration, events, and logs | | console | Connect to a console of a virtual machine. | | port-forward | Forward local ports to a virtual machine. | | scp | SCP files from/to a virtual machine. | @@ -24,80 +25,95 @@ Manages virtual machine-related operations in your Kubernetes cluster. ```shell # Get inventory for default namespace in JSON format -d8 virtualization ansible-inventory -d8 virtualization ansible-inventory --list +d8 v ansible-inventory +d8 v ansible-inventory --list # Get host variables -d8 virtualization ansible-inventory --host myvm.mynamespace +d8 v ansible-inventory --host myvm.mynamespace # Specify namespace -d8 virtualization ansible-inventory -n mynamespace +d8 v ansible-inventory -n mynamespace # Specify output format (json, ini, yaml) -d8 virtualization ansible-inventory -o json -d8 virtualization ansible-inventory -o yaml -d8 virtualization ansible-inventory -o ini +d8 v ansible-inventory -o json +d8 v ansible-inventory -o yaml +d8 v ansible-inventory -o ini ``` #### console ```shell -d8 virtualization console myvm -d8 virtualization console myvm.mynamespace +d8 v console myvm +d8 v console myvm.mynamespace ``` #### port-forward ```shell -d8 virtualization port-forward myvm tcp/8080:8080 -d8 virtualization port-forward --stdio=true myvm.mynamespace 22 +d8 v port-forward myvm tcp/8080:8080 +d8 v port-forward --stdio=true myvm.mynamespace 22 ``` #### scp ```shell -d8 virtualization scp myfile.bin user@myvm:myfile.bin -d8 virtualization scp user@myvm:myfile.bin ~/myfile.bin +d8 v scp myfile.bin user@myvm:myfile.bin +d8 v scp user@myvm:myfile.bin ~/myfile.bin ``` #### ssh ```shell -d8 virtualization --identity-file=/path/to/ssh_key ssh user@myvm.mynamespace -d8 virtualization ssh --local-ssh=true --namespace=mynamespace --username=user myvm +d8 v --identity-file=/path/to/ssh_key ssh user@myvm.mynamespace +d8 v ssh --local-ssh=true --namespace=mynamespace --username=user myvm ``` #### vnc ```shell -d8 virtualization vnc myvm.mynamespace -d8 virtualization vnc myvm -n mynamespace +d8 v vnc myvm.mynamespace +d8 v vnc myvm -n mynamespace ``` #### start ```shell -d8 virtualization start myvm.mynamespace --wait -d8 virtualization start myvm -n mynamespace +d8 v start myvm.mynamespace --wait +d8 v start myvm -n mynamespace ``` #### stop ```shell -d8 virtualization stop myvm.mynamespace --force -d8 virtualization stop myvm -n mynamespace +d8 v stop myvm.mynamespace --force +d8 v stop myvm -n mynamespace ``` #### restart ```shell -d8 virtualization restart myvm.mynamespace --timeout=1m -d8 virtualization restart myvm -n mynamespace +d8 v restart myvm.mynamespace --timeout=1m +d8 v restart myvm -n mynamespace ``` #### evict ```shell -d8 virtualization evict myvm.mynamespace -d8 virtualization evict myvm -n mynamespace +d8 v evict myvm.mynamespace +d8 v evict myvm -n mynamespace +``` + +#### collect-debug-info + +```shell +# Collect debug info for VirtualMachine 'myvm' +d8 v collect-debug-info myvm +d8 v collect-debug-info myvm.mynamespace +d8 v collect-debug-info myvm -n mynamespace + +# Include pod logs in output +d8 v collect-debug-info --with-logs myvm + +# Enable debug output for permission errors +d8 v collect-debug-info --debug myvm ``` diff --git a/src/cli/internal/clientconfig/clientconfig.go b/src/cli/internal/clientconfig/clientconfig.go index 1b7c9f3d99..2b502366ca 100644 --- a/src/cli/internal/clientconfig/clientconfig.go +++ b/src/cli/internal/clientconfig/clientconfig.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/deckhouse/virtualization/api/client/kubeclient" @@ -49,3 +50,15 @@ func ClientAndNamespaceFromContext(ctx context.Context) (client kubeclient.Clien } return client, namespace, overridden, nil } + +func GetRESTConfig(ctx context.Context) (*rest.Config, error) { + clientConfig, ok := ctx.Value(clientConfigKey).(clientcmd.ClientConfig) + if !ok { + return nil, fmt.Errorf("unable to get client config from context") + } + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + return config, nil +} diff --git a/src/cli/internal/cmd/collectdebuginfo/collectdebuginfo.go b/src/cli/internal/cmd/collectdebuginfo/collectdebuginfo.go new file mode 100644 index 0000000000..389f7074db --- /dev/null +++ b/src/cli/internal/cmd/collectdebuginfo/collectdebuginfo.go @@ -0,0 +1,128 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package collectdebuginfo + +import ( + "context" + "fmt" + "io" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/dynamic" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/src/cli/internal/clientconfig" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +func NewCommand() *cobra.Command { + bundle := &DebugBundle{} + cmd := &cobra.Command{ + Use: "collect-debug-info (VirtualMachine)", + Short: "Collect debug information for VM: configuration, events, and logs. Output is written to stdout.", + Example: usage(), + Args: templates.ExactArgs("collect-debug-info", 1), + RunE: bundle.Run, + } + + cmd.Flags().BoolVar(&bundle.saveLogs, "with-logs", false, "Include pod logs in output") + cmd.Flags().BoolVar(&bundle.debug, "debug", false, "Enable debug output for permission errors") + cmd.SetUsageTemplate(templates.UsageTemplate()) + return cmd +} + +type DebugBundle struct { + saveLogs bool + debug bool + dynamicClient dynamic.Interface + stdout io.Writer + stderr io.Writer + resourceCount int +} + +func usage() string { + return ` # Collect debug info for VirtualMachine 'myvm' (output to stdout): + {{ProgramName}} collect-debug-info myvm + {{ProgramName}} collect-debug-info myvm.mynamespace + {{ProgramName}} collect-debug-info myvm -n mynamespace + + # Include pod logs: + {{ProgramName}} collect-debug-info --with-logs myvm + + # Save compressed output to file: + {{ProgramName}} collect-debug-info --with-logs myvm | gzip > debug-info.yaml.gz` +} + +func (b *DebugBundle) Run(cmd *cobra.Command, args []string) error { + client, defaultNamespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context()) + if err != nil { + return err + } + + namespace, name, err := templates.ParseTarget(args[0]) + if err != nil { + return err + } + if namespace == "" { + namespace = defaultNamespace + } + + config, err := clientconfig.GetRESTConfig(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to get REST config: %w", err) + } + b.dynamicClient, err = dynamic.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create dynamic client: %w", err) + } + + b.stdout = cmd.OutOrStdout() + b.stderr = cmd.ErrOrStderr() + + if err := b.collectResources(cmd.Context(), client, namespace, name); err != nil { + return err + } + + return nil +} + +func (b *DebugBundle) collectResources(ctx context.Context, client kubeclient.Client, namespace, vmName string) error { + if err := b.collectVMResources(ctx, client, namespace, vmName); err != nil { + return fmt.Errorf("failed to collect VM resources: %w", err) + } + + if err := b.collectBlockDevices(ctx, client, namespace, vmName); err != nil { + return fmt.Errorf("failed to collect block devices: %w", err) + } + + if err := b.collectPods(ctx, client, namespace, vmName); err != nil { + return fmt.Errorf("failed to collect pods: %w", err) + } + + return nil +} + +func (b *DebugBundle) handleError(resourceType, resourceName string, err error) bool { + if errors.IsForbidden(err) || errors.IsUnauthorized(err) { + if b.debug { + _, _ = fmt.Fprintf(b.stderr, "Warning: Skipping %s/%s: permission denied\n", resourceType, resourceName) + } + return true + } + return false +} diff --git a/src/cli/internal/cmd/collectdebuginfo/collectors.go b/src/cli/internal/cmd/collectdebuginfo/collectors.go new file mode 100644 index 0000000000..11447a5d55 --- /dev/null +++ b/src/cli/internal/cmd/collectdebuginfo/collectors.go @@ -0,0 +1,496 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package collectdebuginfo + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/yaml" + + "github.com/deckhouse/virtualization/api/client/kubeclient" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +var coreKinds = map[string]bool{ + "Pod": true, + "PersistentVolumeClaim": true, + "PersistentVolume": true, + "Event": true, +} + +// Resource collection functions + +func (b *DebugBundle) collectVMResources(ctx context.Context, client kubeclient.Client, namespace, vmName string) error { + // Get VM + vm, err := client.VirtualMachines(namespace).Get(ctx, vmName, metav1.GetOptions{}) + if err != nil { + if b.handleError("VirtualMachine", vmName, err) { + return nil + } + return err + } + if err := b.outputResource("VirtualMachine", vmName, namespace, vm); err != nil { + return fmt.Errorf("failed to output VirtualMachine: %w", err) + } + + // Get IVVM + ivvm, err := b.getInternalResource(ctx, "internalvirtualizationvirtualmachines", namespace, vmName) + if err == nil { + if err := b.outputResource("InternalVirtualizationVirtualMachine", vmName, namespace, ivvm); err != nil { + return fmt.Errorf("failed to output InternalVirtualizationVirtualMachine: %w", err) + } + } else if !b.handleError("InternalVirtualizationVirtualMachine", vmName, err) { + return err + } + + // Get IVVMI + ivvmi, err := b.getInternalResource(ctx, "internalvirtualizationvirtualmachineinstances", namespace, vmName) + if err == nil { + if err := b.outputResource("InternalVirtualizationVirtualMachineInstance", vmName, namespace, ivvmi); err != nil { + return fmt.Errorf("failed to output InternalVirtualizationVirtualMachineInstance: %w", err) + } + } else if !b.handleError("InternalVirtualizationVirtualMachineInstance", vmName, err) { + return err + } + + // Get VM operations + vmUID := string(vm.UID) + vmops, err := client.VirtualMachineOperations(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("virtualization.deckhouse.io/virtual-machine-uid=%s", vmUID), + }) + if err == nil { + for _, vmop := range vmops.Items { + if err := b.outputResource("VirtualMachineOperation", vmop.Name, namespace, &vmop); err != nil { + return fmt.Errorf("failed to output VirtualMachineOperation: %w", err) + } + } + } else if !b.handleError("VirtualMachineOperation", "", err) { + return err + } + + // Get migrations + migrations, err := b.getInternalResourceList(ctx, "internalvirtualizationvirtualmachineinstancemigrations", namespace) + if err == nil { + for _, item := range migrations { + vmiName, found, _ := unstructured.NestedString(item.Object, "spec", "vmiName") + if found && vmiName == vmName { + name, _, _ := unstructured.NestedString(item.Object, "metadata", "name") + extraInfo := fmt.Sprintf(" (for VMI: %s)", vmiName) + if err := b.outputResourceWithExtraInfo("InternalVirtualizationVirtualMachineInstanceMigration", name, namespace, item, extraInfo); err != nil { + return fmt.Errorf("failed to output InternalVirtualizationVirtualMachineInstanceMigration: %w", err) + } + } + } + } else if !b.handleError("InternalVirtualizationVirtualMachineInstanceMigration", "", err) { + return err + } + + // Get events for VM + b.collectEvents(ctx, client, namespace, "VirtualMachine", vmName) + + return nil +} + +func (b *DebugBundle) collectBlockDevices(ctx context.Context, client kubeclient.Client, namespace, vmName string) error { + vm, err := client.VirtualMachines(namespace).Get(ctx, vmName, metav1.GetOptions{}) + if err != nil { + return err + } + + // Track collected block devices to avoid duplicates + collectedBlockDevices := make(map[string]bool) + + // Helper function to create block device key + bdKey := func(kind v1alpha2.BlockDeviceKind, name string) string { + return string(kind) + ":" + name + } + + // Static block devices + // Note: blockDeviceRefs can only contain block devices (VirtualDisk, VirtualImage, ClusterVirtualImage), + // not VMBDA. VMBDA are collected separately below. + for _, bdRef := range vm.Spec.BlockDeviceRefs { + key := bdKey(bdRef.Kind, bdRef.Name) + if !collectedBlockDevices[key] { + if err := b.collectBlockDevice(ctx, client, namespace, bdRef.Kind, bdRef.Name); err != nil { + if !b.handleError(string(bdRef.Kind), bdRef.Name, err) { + return err + } + } else { + collectedBlockDevices[key] = true + } + } + } + + // Get all VMBDA that reference this VM + // Note: Hotplugged block devices are collected through VMBDA, not from vm.Status.BlockDeviceRefs, + // to avoid duplication. All hotplugged devices have corresponding VMBDA resources. + vmbdas, err := client.VirtualMachineBlockDeviceAttachments(namespace).List(ctx, metav1.ListOptions{}) + if err == nil { + for _, vmbda := range vmbdas.Items { + if vmbda.Spec.VirtualMachineName == vmName { + if err := b.outputResource("VirtualMachineBlockDeviceAttachment", vmbda.Name, namespace, &vmbda); err != nil { + return fmt.Errorf("failed to output VirtualMachineBlockDeviceAttachment: %w", err) + } + b.collectEvents(ctx, client, namespace, "VirtualMachineBlockDeviceAttachment", vmbda.Name) + + // Get associated block device + if vmbda.Spec.BlockDeviceRef.Kind != "" && vmbda.Spec.BlockDeviceRef.Name != "" { + // Convert VMBDAObjectRefKind to BlockDeviceKind + var bdKind v1alpha2.BlockDeviceKind + switch vmbda.Spec.BlockDeviceRef.Kind { + case v1alpha2.VMBDAObjectRefKindVirtualDisk: + bdKind = v1alpha2.VirtualDiskKind + case v1alpha2.VMBDAObjectRefKindVirtualImage: + bdKind = v1alpha2.VirtualImageKind + case v1alpha2.VMBDAObjectRefKindClusterVirtualImage: + bdKind = v1alpha2.ClusterVirtualImageKind + default: + continue + } + key := bdKey(bdKind, vmbda.Spec.BlockDeviceRef.Name) + if !collectedBlockDevices[key] { + if err := b.collectBlockDevice(ctx, client, namespace, bdKind, vmbda.Spec.BlockDeviceRef.Name); err != nil { + if !b.handleError(string(bdKind), vmbda.Spec.BlockDeviceRef.Name, err) { + return err + } + } else { + collectedBlockDevices[key] = true + } + } + } + } + } + } else if !b.handleError("VirtualMachineBlockDeviceAttachment", "", err) { + return err + } + + return nil +} + +func (b *DebugBundle) collectBlockDevice(ctx context.Context, client kubeclient.Client, namespace string, kind v1alpha2.BlockDeviceKind, name string) error { + switch kind { + case v1alpha2.VirtualDiskKind: + vd, err := client.VirtualDisks(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return err + } + if err := b.outputResource("VirtualDisk", name, namespace, vd); err != nil { + return fmt.Errorf("failed to output VirtualDisk: %w", err) + } + b.collectEvents(ctx, client, namespace, "VirtualDisk", name) + + // Get PVC + if vd.Status.Target.PersistentVolumeClaim != "" { + pvc, err := client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, vd.Status.Target.PersistentVolumeClaim, metav1.GetOptions{}) + if err == nil { + if err := b.outputResource("PersistentVolumeClaim", pvc.Name, namespace, pvc); err != nil { + return fmt.Errorf("failed to output PersistentVolumeClaim: %w", err) + } + b.collectEvents(ctx, client, namespace, "PersistentVolumeClaim", pvc.Name) + + // Get PV + if pvc.Spec.VolumeName != "" { + pv, err := client.CoreV1().PersistentVolumes().Get(ctx, pvc.Spec.VolumeName, metav1.GetOptions{}) + if err == nil { + if err := b.outputResource("PersistentVolume", pv.Name, "", pv); err != nil { + return fmt.Errorf("failed to output PersistentVolume: %w", err) + } + } else if !b.handleError("PersistentVolume", pvc.Spec.VolumeName, err) { + return err + } + } + } else if !b.handleError("PersistentVolumeClaim", vd.Status.Target.PersistentVolumeClaim, err) { + return err + } + } + + case v1alpha2.VirtualImageKind: + vi, err := client.VirtualImages(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return err + } + if err := b.outputResource("VirtualImage", name, namespace, vi); err != nil { + return fmt.Errorf("failed to output VirtualImage: %w", err) + } + b.collectEvents(ctx, client, namespace, "VirtualImage", name) + + case v1alpha2.ClusterVirtualImageKind: + cvi, err := client.ClusterVirtualImages().Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return err + } + if err := b.outputResource("ClusterVirtualImage", name, "", cvi); err != nil { + return fmt.Errorf("failed to output ClusterVirtualImage: %w", err) + } + + default: + return fmt.Errorf("unknown block device kind: %s", kind) + } + + return nil +} + +func (b *DebugBundle) collectPods(ctx context.Context, client kubeclient.Client, namespace, vmName string) error { + pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("vm.kubevirt.internal.virtualization.deckhouse.io/name=%s", vmName), + }) + if err != nil { + if b.handleError("Pod", "", err) { + return nil + } + return err + } + + // Collect VM pods and their UIDs for finding dependent pods + vmPodUIDs := make(map[string]bool) + for _, pod := range pods.Items { + vmPodUIDs[string(pod.UID)] = true + if err := b.outputResource("Pod", pod.Name, namespace, &pod); err != nil { + return fmt.Errorf("failed to output Pod: %w", err) + } + b.collectEvents(ctx, client, namespace, "Pod", pod.Name) + + if b.saveLogs { + b.collectSinglePodLogs(ctx, client, namespace, pod.Name) + } + } + + // Collect pods that have ownerReference to VM pods (e.g., hotplug volume pods) + if len(vmPodUIDs) > 0 { + allPods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + // If we can't list all pods, continue without dependent pods + if !b.handleError("Pod", namespace, err) { + return err + } + } else { + for _, pod := range allPods.Items { + // Skip VM pods we already collected + if vmPodUIDs[string(pod.UID)] { + continue + } + // Check if this pod has ownerReference to any VM pod + for _, ownerRef := range pod.OwnerReferences { + if ownerRef.Kind == "Pod" && vmPodUIDs[string(ownerRef.UID)] { + if err := b.outputResource("Pod", pod.Name, namespace, &pod); err != nil { + return fmt.Errorf("failed to output Pod: %w", err) + } + b.collectEvents(ctx, client, namespace, "Pod", pod.Name) + if b.saveLogs { + b.collectSinglePodLogs(ctx, client, namespace, pod.Name) + } + break + } + } + } + } + } + + return nil +} + +// Event collection functions + +func (b *DebugBundle) collectEvents(ctx context.Context, client kubeclient.Client, namespace, resourceType, resourceName string) { + events, err := client.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("involvedObject.name=%s", resourceName), + }) + if err != nil { + if b.handleError("Event", resourceName, err) { + return + } + return + } + + // Add each event individually to preserve TypeMeta + for i := range events.Items { + if err := b.outputResource("Event", fmt.Sprintf("%s-%s-%d", strings.ToLower(resourceType), resourceName, i), namespace, &events.Items[i]); err != nil { + // Log error but continue processing other events + _, _ = fmt.Fprintf(b.stderr, "Warning: failed to output Event: %v\n", err) + } + } +} + +// Log collection functions + +const ( + // logReadTimeout is the maximum time to wait for reading pod logs + logReadTimeout = 30 * time.Second + // maxLogLines limits the number of log lines to prevent hanging on very large logs + maxLogLines = int64(10000) +) + +func (b *DebugBundle) collectSinglePodLogs(ctx context.Context, client kubeclient.Client, namespace, podName string) { + logPrefix := fmt.Sprintf("logs %s/%s", namespace, podName) + tailLines := maxLogLines + + // Get current logs with timeout and line limit + logCtx, cancel := context.WithTimeout(ctx, logReadTimeout) + defer cancel() + + req := client.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ + TailLines: &tailLines, + }) + logStream, err := req.Stream(logCtx) + if err == nil && logStream != nil { + stream := logStream // Capture in closure + defer func() { + if stream != nil { + _ = stream.Close() + } + }() + logContent, err := b.readLogsWithTimeout(logCtx, logStream) + if err == nil { + _, _ = fmt.Fprintf(b.stdout, "\n# %s\n", logPrefix) + _, _ = fmt.Fprintf(b.stdout, "%s\n", string(logContent)) + } + } + + // Get previous logs with timeout and line limit + logCtx, cancel = context.WithTimeout(ctx, logReadTimeout) + defer cancel() + + req = client.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ + Previous: true, + TailLines: &tailLines, + }) + logStream, err = req.Stream(logCtx) + if err == nil && logStream != nil { + stream := logStream // Capture in closure + defer func() { + if stream != nil { + _ = stream.Close() + } + }() + logContent, err := b.readLogsWithTimeout(logCtx, logStream) + if err == nil { + _, _ = fmt.Fprintf(b.stdout, "\n# %s (previous)\n", logPrefix) + _, _ = fmt.Fprintf(b.stdout, "%s\n", string(logContent)) + } + } +} + +// readLogsWithTimeout reads logs from stream with timeout protection +func (b *DebugBundle) readLogsWithTimeout(ctx context.Context, stream io.ReadCloser) ([]byte, error) { + type result struct { + data []byte + err error + } + resultChan := make(chan result, 1) + + go func() { + data, err := io.ReadAll(stream) + resultChan <- result{data: data, err: err} + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-resultChan: + return res.data, res.err + } +} + +// Helper functions + +func (b *DebugBundle) getInternalResource(ctx context.Context, resource, namespace, name string) (*unstructured.Unstructured, error) { + obj, err := b.dynamicClient.Resource(schema.GroupVersionResource{ + Group: "internal.virtualization.deckhouse.io", + Version: "v1", + Resource: resource, + }).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return obj, nil +} + +func (b *DebugBundle) getInternalResourceList(ctx context.Context, resource, namespace string) ([]*unstructured.Unstructured, error) { + list, err := b.dynamicClient.Resource(schema.GroupVersionResource{ + Group: "internal.virtualization.deckhouse.io", + Version: "v1", + Resource: resource, + }).Namespace(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + result := make([]*unstructured.Unstructured, len(list.Items)) + for i := range list.Items { + result[i] = &list.Items[i] + } + return result, nil +} + +func (b *DebugBundle) outputResource(kind, name, namespace string, obj runtime.Object) error { + return b.outputResourceWithExtraInfo(kind, name, namespace, obj, "") +} + +func (b *DebugBundle) outputResourceWithExtraInfo(kind, name, namespace string, obj runtime.Object, extraInfo string) error { + // Output separator if not first resource + if b.resourceCount > 0 { + _, _ = fmt.Fprintf(b.stdout, "\n---\n") + } + b.resourceCount++ + + // Ensure Kind is set from input if missing + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk.Kind == "" { + gvk.Kind = kind + obj.GetObjectKind().SetGroupVersionKind(gvk) + } + + // If GroupVersion is missing/empty, try to get from scheme + if gvk.GroupVersion().Empty() { + gvks, _, err := kubeclient.Scheme.ObjectKinds(obj) + if err == nil && len(gvks) > 0 { + gvk = gvks[0] + obj.GetObjectKind().SetGroupVersionKind(gvk) + } else if coreKinds[kind] { + // Fallback for core Kubernetes resources if scheme doesn't know about them + gvk = schema.GroupVersionKind{Group: "", Version: "v1", Kind: kind} + obj.GetObjectKind().SetGroupVersionKind(gvk) + } + } + + // Marshal to JSON (now with TypeMeta if set) + jsonBytes, err := json.Marshal(obj) + if err != nil { + return fmt.Errorf("failed to marshal %s/%s (namespace: %s) to JSON: %w", kind, name, namespace, err) + } + + // Convert to YAML + yamlBytes, err := yaml.JSONToYAML(jsonBytes) + if err != nil { + return fmt.Errorf("failed to convert %s/%s (namespace: %s) to YAML: %w", kind, name, namespace, err) + } + + // Output with optional extra info + _, _ = fmt.Fprintf(b.stdout, "# %d. %s: %s%s\n%s", b.resourceCount, kind, name, extraInfo, string(yamlBytes)) + + return nil +} diff --git a/src/cli/pkg/command/virtualization.go b/src/cli/pkg/command/virtualization.go index b6ffec1591..7aece0ccbb 100644 --- a/src/cli/pkg/command/virtualization.go +++ b/src/cli/pkg/command/virtualization.go @@ -32,6 +32,7 @@ import ( "github.com/deckhouse/virtualization/api/client/kubeclient" "github.com/deckhouse/virtualization/src/cli/internal/clientconfig" "github.com/deckhouse/virtualization/src/cli/internal/cmd/ansibleinventory" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/collectdebuginfo" "github.com/deckhouse/virtualization/src/cli/internal/cmd/console" "github.com/deckhouse/virtualization/src/cli/internal/cmd/lifecycle" "github.com/deckhouse/virtualization/src/cli/internal/cmd/portforward" @@ -86,6 +87,7 @@ func NewCommand(programName string) *cobra.Command { virtCmd.AddCommand( ansibleinventory.NewCommand(), console.NewCommand(), + collectdebuginfo.NewCommand(), vnc.NewCommand(), portforward.NewCommand(), ssh.NewCommand(),