mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 05:25:14 +09:00
Hidora's customers are leaving VMware now, and a tenant has to be able to import their own machines without a platform administrator. The chart approach could not express that: a tenant can neither create the Secret holding vCenter credentials nor name the VDDK image, so both had to come from somewhere they cannot reach. Two CRDs in a new `forklift.cozystack.io` group, split by lifecycle. A VMImportSource is a long-lived connection -- type, endpoint, credentials, readiness -- and a VMImportTask is the one-shot operation that names machines and produces VMDisks and VMInstances. The outputs carry no owner reference back to the task, so deleting a finished import leaves the machines alone. Konveyor Forklift does the transfer underneath; the controller renders its objects, mirrors its verdicts, and hands each finished volume into a VMDisk without copying it. What a live vCenter migration forced out, and no amount of source reading would have: - A guest copied verbatim keeps the drivers it had under VMware and no virtio, so on a virtio disk it does not fail the import -- it succeeds and then does not boot. Imported disks go on SATA. - VDDK connects straight to the ESXi host at the address vCenter advertises, which is routinely unreachable, and on the first cluster collided with the Service CIDR. `spec.hosts` redirects per host. - Firmware must be carried across or a UEFI guest imports as BIOS and never boots (stacked on #3002). - Deleting a task mid-transfer orphaned its DataVolume; a finalizer now removes the engine's volumes and leaves the outputs. The console gains a Migration section: connections are editable, and the VM field is a picker built from a machine list the controller publishes, since the aggregated API cannot reach the inventory and should not hold a source's credentials to try. Two general mechanisms fall out of that -- an option source may take an argument, and one named in a CRD annotation may reference a sibling field. Design: cozystack/community#62. Docs: cozystack/website#673. Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
97 lines
3.5 KiB
Go
97 lines
3.5 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package migrationcontroller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
|
|
migrationv1alpha1 "github.com/cozystack/cozystack/api/migration/v1alpha1"
|
|
)
|
|
|
|
// The VM picker in the console needs to offer machine names while writing
|
|
// managed-object references, and only Forklift's inventory knows the mapping.
|
|
// The aggregated API that serves dropdowns lists Kubernetes objects with a
|
|
// dynamic client and has no way to speak HTTPS to the inventory, nor should it
|
|
// grow one: that would put the source's credentials path in a second component.
|
|
// So this controller — which already holds an inventory client — publishes the
|
|
// list, and the option provider reads it back as an ordinary object.
|
|
//
|
|
// The list lives in its own ConfigMap rather than on the Source's status. A
|
|
// status carrying thousands of entries is rewritten on every refresh and wakes
|
|
// everything watching the Source; a separate object is read only by the thing
|
|
// that needs it, and an ownerReference still has it deleted with the Source.
|
|
// maxPublishedVMs bounds the object. A vCenter with more machines than this is
|
|
// past the point where a dropdown is the right interface anyway, and an
|
|
// unbounded write is not something to put in etcd on a timer.
|
|
const maxPublishedVMs = 5000
|
|
|
|
// publishInventory refreshes the source's VM list. A failure here never fails
|
|
// the Source: the connection is what the Source reports on, and an inventory
|
|
// that is briefly unavailable should degrade the picker, not the status.
|
|
func (r *VMImportSourceReconciler) publishInventory(ctx context.Context, src *migrationv1alpha1.VMImportSource) error {
|
|
if r.Inventory == nil {
|
|
return nil
|
|
}
|
|
|
|
provider := newObject(providerGVK)
|
|
if err := r.Get(ctx, types.NamespacedName{
|
|
Namespace: src.Namespace, Name: sourceProviderName(src.Name),
|
|
}, provider); err != nil {
|
|
return err
|
|
}
|
|
uid := string(provider.GetUID())
|
|
if uid == "" {
|
|
return fmt.Errorf("source Provider %s/%s has no UID yet", src.Namespace, sourceProviderName(src.Name))
|
|
}
|
|
|
|
vms, err := r.Inventory.VMs(ctx, uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sorted by name so the dropdown is stable across refreshes and the
|
|
// truncation, when it happens, is at least predictable.
|
|
sort.Slice(vms, func(i, j int) bool { return vms[i].Name < vms[j].Name })
|
|
truncated := len(vms) > maxPublishedVMs
|
|
if truncated {
|
|
vms = vms[:maxPublishedVMs]
|
|
}
|
|
|
|
published := make([]migrationv1alpha1.PublishedVM, 0, len(vms))
|
|
for _, vm := range vms {
|
|
published = append(published, migrationv1alpha1.PublishedVM{ID: vm.ID, Name: vm.Name, Path: vm.Path})
|
|
}
|
|
encoded, err := json.Marshal(published)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cm := &corev1.ConfigMap{}
|
|
cm.Name = migrationv1alpha1.InventoryConfigMapName(src.Name)
|
|
cm.Namespace = src.Namespace
|
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error {
|
|
if cm.Annotations == nil {
|
|
cm.Annotations = map[string]string{}
|
|
}
|
|
if truncated {
|
|
cm.Annotations[migrationv1alpha1.InventoryTruncatedAnnotation] = "true"
|
|
} else {
|
|
delete(cm.Annotations, migrationv1alpha1.InventoryTruncatedAnnotation)
|
|
}
|
|
cm.Data = map[string]string{migrationv1alpha1.InventoryVMsKey: string(encoded)}
|
|
return controllerutil.SetControllerReference(src, cm, r.Scheme)
|
|
})
|
|
if err != nil && !apierrors.IsAlreadyExists(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|