api/handler/vm_export.go
Rama main · réplica de lectura del repositorio original.
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
}