Ctrl K
ComunidadGuías…

api/handler/vm_export.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"context"
	"fmt"
	"strings"

	"github.com/goodrain/rainbond/pkg/component/k8s"
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	kubevirtv1 "kubevirt.io/api/core/v1"
)

type VMExportRequest struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

type VMExportStatus struct {
	ExportName     string `json:"export_name"`
	Namespace      string `json:"namespace"`
	SourcePVC      string `json:"source_pvc"`
	TokenSecretRef string `json:"token_secret_ref"`
	DownloadToken  string `json:"download_token,omitempty"`
	Phase          string `json:"phase,omitempty"`
	DownloadURL    string `json:"download_url,omitempty"`
}

var vmExportDynamicClient = func() dynamic.Interface {
	if k8s.Default() == nil {
		return nil
	}
	return k8s.Default().DynamicClient
}

var vmExportGVR = schema.GroupVersionResource{
	Group:    "export.kubevirt.io",
	Version:  "v1beta1",
	Resource: "virtualmachineexports",
}

func (s *ServiceAction) CreateVMExport(serviceID string, req *VMExportRequest) (*VMExportStatus, error) {
	if req == nil || strings.TrimSpace(req.Name) == "" {
		return nil, fmt.Errorf("export name is required")
	}
	exportName := strings.TrimSpace(req.Name)
	logrus.Infof("[vm-export] create request: service_id=%s export_name=%s", serviceID, exportName)
	if s == nil || s.kubevirtClient == nil {
		logrus.Errorf("[vm-export] create failed before lookup: service_id=%s export_name=%s error=kubevirt client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("kubevirt client is not initialized")
	}
	dynamicClient := vmExportDynamicClient()
	if dynamicClient == nil {
		logrus.Errorf("[vm-export] create failed before lookup: service_id=%s export_name=%s error=dynamic client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("dynamic client is not initialized")
	}
	if s.kubeClient == nil {
		logrus.Errorf("[vm-export] create failed before lookup: service_id=%s export_name=%s error=kube client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("kube client is not initialized")
	}
	vms, err := s.kubevirtClient.VirtualMachine("").List(context.Background(), metav1.ListOptions{
		LabelSelector: "service_id=" + serviceID,
	})
	if err != nil {
		logrus.Errorf("[vm-export] list vm failed: service_id=%s export_name=%s error=%v", serviceID, exportName, err)
		return nil, err
	}
	if len(vms.Items) == 0 {
		logrus.Warnf("[vm-export] vm not found: service_id=%s export_name=%s", serviceID, exportName)
		return nil, fmt.Errorf("service id is %v vm is not exist", serviceID)
	}
	vm := &vms.Items[0]
	logrus.Infof("[vm-export] vm resolved: service_id=%s export_name=%s vm_namespace=%s vm_name=%s", serviceID, exportName, vm.Namespace, vm.Name)
	sourcePVC, err := resolveVMRootPVC(vm)
	if err != nil {
		logrus.Errorf("[vm-export] resolve root pvc failed: service_id=%s export_name=%s vm_namespace=%s vm_name=%s error=%v", serviceID, exportName, vm.Namespace, vm.Name, err)
		return nil, err
	}
	tokenSecretRef := exportName + "-token"
	logrus.Infof("[vm-export] root pvc resolved: service_id=%s export_name=%s source_pvc=%s token_secret=%s", serviceID, exportName, sourcePVC, tokenSecretRef)
	if err := ensureVMExportTokenSecret(s.kubeClient, vm.Namespace, tokenSecretRef); err != nil {
		logrus.Errorf("[vm-export] ensure token secret failed: service_id=%s export_name=%s namespace=%s secret=%s error=%v", serviceID, exportName, vm.Namespace, tokenSecretRef, err)
		return nil, err
	}
	downloadToken, err := readVMExportDownloadToken(s.kubeClient, vm.Namespace, tokenSecretRef)
	if err != nil {
		logrus.Errorf("[vm-export] read token secret failed: service_id=%s export_name=%s namespace=%s secret=%s error=%v", serviceID, exportName, vm.Namespace, tokenSecretRef, err)
		return nil, err
	}
	resourceIf := dynamicClient.Resource(vmExportGVR).Namespace(vm.Namespace)
	_ = resourceIf.Delete(context.Background(), exportName, metav1.DeleteOptions{})
	obj := buildVMExport(vm.Namespace, exportName, strings.TrimSpace(req.Description), serviceID, sourcePVC, tokenSecretRef)
	logrus.Infof("[vm-export] creating VirtualMachineExport: service_id=%s export_name=%s namespace=%s source_pvc=%s", serviceID, exportName, vm.Namespace, sourcePVC)
	created, err := resourceIf.Create(context.Background(), obj, metav1.CreateOptions{})
	if err != nil {
		if !k8serrors.IsAlreadyExists(err) {
			logrus.Errorf("[vm-export] create VirtualMachineExport failed: service_id=%s export_name=%s namespace=%s error=%v", serviceID, exportName, vm.Namespace, err)
			return nil, err
		}
		logrus.Warnf("[vm-export] VirtualMachineExport already exists, reading existing object: service_id=%s export_name=%s namespace=%s", serviceID, exportName, vm.Namespace)
		created, err = resourceIf.Get(context.Background(), exportName, metav1.GetOptions{})
		if err != nil {
			logrus.Errorf("[vm-export] get existing VirtualMachineExport failed: service_id=%s export_name=%s namespace=%s error=%v", serviceID, exportName, vm.Namespace, err)
			return nil, err
		}
	}
	status := &VMExportStatus{
		ExportName:     created.GetName(),
		Namespace:      created.GetNamespace(),
		SourcePVC:      sourcePVC,
		TokenSecretRef: tokenSecretRef,
		DownloadToken:  downloadToken,
		Phase:          nestedString(created.Object, "status", "phase"),
		DownloadURL:    extractVMExportDownloadURL(created.Object),
	}
	logrus.Infof("[vm-export] create response: service_id=%s export_name=%s namespace=%s phase=%s has_download_url=%t", serviceID, status.ExportName, status.Namespace, status.Phase, status.DownloadURL != "")
	return status, nil
}

func (s *ServiceAction) GetVMExport(serviceID, exportName string) (*VMExportStatus, error) {
	if strings.TrimSpace(exportName) == "" {
		return nil, fmt.Errorf("export name is required")
	}
	exportName = strings.TrimSpace(exportName)
	logrus.Infof("[vm-export] get request: service_id=%s export_name=%s", serviceID, exportName)
	if s == nil || s.kubevirtClient == nil {
		logrus.Errorf("[vm-export] get failed before lookup: service_id=%s export_name=%s error=kubevirt client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("kubevirt client is not initialized")
	}
	dynamicClient := vmExportDynamicClient()
	if dynamicClient == nil {
		logrus.Errorf("[vm-export] get failed before lookup: service_id=%s export_name=%s error=dynamic client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("dynamic client is not initialized")
	}
	vms, err := s.kubevirtClient.VirtualMachine("").List(context.Background(), metav1.ListOptions{
		LabelSelector: "service_id=" + serviceID,
	})
	if err != nil {
		logrus.Errorf("[vm-export] list vm failed: service_id=%s export_name=%s error=%v", serviceID, exportName, err)
		return nil, err
	}
	if len(vms.Items) == 0 {
		logrus.Warnf("[vm-export] vm not found: service_id=%s export_name=%s", serviceID, exportName)
		return nil, fmt.Errorf("service id is %v vm is not exist", serviceID)
	}
	if s.kubeClient == nil {
		logrus.Errorf("[vm-export] get failed before reading token: service_id=%s export_name=%s error=kube client is not initialized", serviceID, exportName)
		return nil, fmt.Errorf("kube client is not initialized")
	}
	vm := &vms.Items[0]
	export, err := dynamicClient.Resource(vmExportGVR).Namespace(vm.Namespace).Get(context.Background(), exportName, metav1.GetOptions{})
	if err != nil {
		logrus.Errorf("[vm-export] get VirtualMachineExport failed: service_id=%s export_name=%s namespace=%s error=%v", serviceID, exportName, vm.Namespace, err)
		return nil, err
	}
	tokenSecretRef := nestedString(export.Object, "spec", "tokenSecretRef")
	downloadToken, err := readVMExportDownloadToken(s.kubeClient, vm.Namespace, tokenSecretRef)
	if err != nil {
		logrus.Errorf("[vm-export] read token secret failed: service_id=%s export_name=%s namespace=%s secret=%s error=%v", serviceID, exportName, vm.Namespace, tokenSecretRef, err)
		return nil, err
	}
	status := &VMExportStatus{
		ExportName:     export.GetName(),
		Namespace:      export.GetNamespace(),
		SourcePVC:      nestedString(export.Object, "spec", "source", "name"),
		TokenSecretRef: tokenSecretRef,
		DownloadToken:  downloadToken,
		Phase:          nestedString(export.Object, "status", "phase"),
		DownloadURL:    extractVMExportDownloadURL(export.Object),
	}
	logrus.Infof("[vm-export] get response: service_id=%s export_name=%s namespace=%s phase=%s has_download_url=%t", serviceID, status.ExportName, status.Namespace, status.Phase, status.DownloadURL != "")
	return status, nil
}

func resolveVMRootPVC(vm *kubevirtv1.VirtualMachine) (string, error) {
	if vm == nil {
		return "", fmt.Errorf("vm is nil")
	}
	var rootDiskName string
	var bestOrder uint
	for _, disk := range vm.Spec.Template.Spec.Domain.Devices.Disks {
		if disk.BootOrder == nil {
			continue
		}
		if rootDiskName == "" || *disk.BootOrder < bestOrder {
			rootDiskName = disk.Name
			bestOrder = *disk.BootOrder
		}
	}
	if rootDiskName == "" && len(vm.Spec.Template.Spec.Domain.Devices.Disks) > 0 {
		rootDiskName = vm.Spec.Template.Spec.Domain.Devices.Disks[0].Name
	}
	if rootDiskName == "" {
		return "", fmt.Errorf("vm has no disks")
	}
	for _, volume := range vm.Spec.Template.Spec.Volumes {
		if volume.Name != rootDiskName {
			continue
		}
		if volume.DataVolume != nil && strings.TrimSpace(volume.DataVolume.Name) != "" {
			return volume.DataVolume.Name, nil
		}
		if volume.PersistentVolumeClaim != nil && strings.TrimSpace(volume.PersistentVolumeClaim.ClaimName) != "" {
			return volume.PersistentVolumeClaim.ClaimName, nil
		}
	}
	return "", fmt.Errorf("root disk %s has no pvc-backed volume source", rootDiskName)
}

func ensureVMExportTokenSecret(kubeClient kubernetes.Interface, namespace, secretName string) error {
	if kubeClient == nil {
		return fmt.Errorf("kube client is nil")
	}
	secrets := kubeClient.CoreV1().Secrets(namespace)
	existing, err := secrets.Get(context.Background(), secretName, metav1.GetOptions{})
	if err == nil && existing != nil {
		if existing.Data == nil {
			existing.Data = map[string][]byte{}
		}
		if len(existing.Data["token"]) == 0 {
			existing.Data["token"] = []byte(uuid.New().String())
			_, err = secrets.Update(context.Background(), existing, metav1.UpdateOptions{})
		}
		return err
	}
	if err != nil && !k8serrors.IsNotFound(err) {
		return err
	}
	_, err = secrets.Create(context.Background(), &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      secretName,
			Namespace: namespace,
		},
		Data: map[string][]byte{
			"token": []byte(uuid.New().String()),
		},
	}, metav1.CreateOptions{})
	return err
}

func readVMExportDownloadToken(kubeClient kubernetes.Interface, namespace, secretName string) (string, error) {
	if kubeClient == nil {
		return "", fmt.Errorf("kube client is nil")
	}
	if strings.TrimSpace(secretName) == "" {
		return "", fmt.Errorf("token secret is empty")
	}
	secret, err := kubeClient.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
	if err != nil {
		return "", err
	}
	token := string(secret.Data["token"])
	if strings.TrimSpace(token) == "" {
		return "", fmt.Errorf("token secret %s has no token", secretName)
	}
	return token, nil
}

func buildVMExport(namespace, exportName, description, serviceID, sourcePVC, tokenSecretRef string) *unstructured.Unstructured {
	annotations := map[string]interface{}{}
	if description != "" {
		annotations["description"] = description
	}
	return &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "export.kubevirt.io/v1beta1",
			"kind":       "VirtualMachineExport",
			"metadata": map[string]interface{}{
				"name":      exportName,
				"namespace": namespace,
				"labels": map[string]interface{}{
					"service_id": serviceID,
				},
				"annotations": annotations,
			},
			"spec": map[string]interface{}{
				"tokenSecretRef": tokenSecretRef,
				"ttlDuration":    "24h",
				"source": map[string]interface{}{
					"apiGroup": "",
					"kind":     "PersistentVolumeClaim",
					"name":     sourcePVC,
				},
			},
		},
	}
}

func nestedString(obj map[string]interface{}, fields ...string) string {
	current := obj
	for i, field := range fields {
		value, ok := current[field]
		if !ok {
			return ""
		}
		if i == len(fields)-1 {
			if s, ok := value.(string); ok {
				return s
			}
			return ""
		}
		next, ok := value.(map[string]interface{})
		if !ok {
			return ""
		}
		current = next
	}
	return ""
}

func extractVMExportDownloadURL(obj map[string]interface{}) string {
	status, ok := obj["status"].(map[string]interface{})
	if !ok {
		return ""
	}
	links, ok := status["links"].(map[string]interface{})
	if !ok {
		return ""
	}
	internal, ok := links["internal"].(map[string]interface{})
	if !ok {
		return ""
	}
	volumes, ok := internal["volumes"].([]interface{})
	if !ok {
		return ""
	}
	var fallback string
	for _, volume := range volumes {
		volumeMap, ok := volume.(map[string]interface{})
		if !ok {
			continue
		}
		formats, ok := volumeMap["formats"].([]interface{})
		if !ok {
			continue
		}
		for _, formatItem := range formats {
			formatMap, ok := formatItem.(map[string]interface{})
			if !ok {
				continue
			}
			url, _ := formatMap["url"].(string)
			format, _ := formatMap["format"].(string)
			if url == "" {
				continue
			}
			if format == "gzip" {
				return url
			}
			if fallback == "" {
				fallback = url
			}
		}
	}
	return fallback
}