mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-30 00:27:04 +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>
157 lines
6.0 KiB
Go
157 lines
6.0 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package migrationcontroller reconciles the forklift.cozystack.io API group:
|
|
// VMImportSource connections and the VMImportTask operations that run over them.
|
|
//
|
|
// Forklift, KubeVirt and CDI objects are handled as unstructured rather than
|
|
// through generated types. That is deliberate: those projects' Go modules are
|
|
// heavy transitive dependencies, Cozystack drives only a handful of their
|
|
// fields, and Forklift in particular is an optional package that may not be
|
|
// installed at all — an unstructured client degrades to a clean NotFound
|
|
// instead of forcing the scheme to carry types for CRDs that are absent.
|
|
package migrationcontroller
|
|
|
|
import (
|
|
"strings"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
)
|
|
|
|
// Forklift's API group. Cozystack pins the operator package to v2.11.x, which
|
|
// serves v1beta1 for every kind used here.
|
|
const (
|
|
forkliftGroup = "forklift.konveyor.io"
|
|
forkliftVersion = "v1beta1"
|
|
)
|
|
|
|
// Forklift kinds this controller creates or reads.
|
|
var (
|
|
providerGVK = forkliftGVK("Provider")
|
|
hostGVK = forkliftGVK("Host")
|
|
planGVK = forkliftGVK("Plan")
|
|
migrationGVK = forkliftGVK("Migration")
|
|
networkMapGVK = forkliftGVK("NetworkMap")
|
|
storageMapGVK = forkliftGVK("StorageMap")
|
|
)
|
|
|
|
// KubeVirt and CDI kinds this controller reads and cleans up. The
|
|
// VirtualMachine Forklift creates is never started: it is read for the source
|
|
// VM's shape (CPU, memory, firmware, disk order) and then discarded. The one
|
|
// thing it does not carry is which networks and datastores the VM uses, which
|
|
// is why the maps are built from the inventory instead (see sourceTopology).
|
|
var (
|
|
virtualMachineGVK = schema.GroupVersionKind{Group: "kubevirt.io", Version: "v1", Kind: "VirtualMachine"}
|
|
dataVolumeGVK = schema.GroupVersionKind{Group: "cdi.kubevirt.io", Version: "v1beta1", Kind: "DataVolume"}
|
|
)
|
|
|
|
// Cozystack application kinds the controller produces. These are projections
|
|
// over HelmReleases: creating one through the aggregated API is exactly what a
|
|
// tenant does in the dashboard, so the controller takes the same path rather
|
|
// than writing HelmReleases behind the projection's back.
|
|
var (
|
|
vmDiskGVK = appsGVK("VMDisk")
|
|
vmInstanceGVK = appsGVK("VMInstance")
|
|
)
|
|
|
|
func forkliftGVK(kind string) schema.GroupVersionKind {
|
|
return schema.GroupVersionKind{Group: forkliftGroup, Version: forkliftVersion, Kind: kind}
|
|
}
|
|
|
|
func appsGVK(kind string) schema.GroupVersionKind {
|
|
return schema.GroupVersionKind{Group: "apps.cozystack.io", Version: "v1alpha1", Kind: kind}
|
|
}
|
|
|
|
// newForkliftObject returns an empty unstructured object of the given kind,
|
|
// ready to Get into or to fill in and Create.
|
|
func newObject(gvk schema.GroupVersionKind) *unstructured.Unstructured {
|
|
u := &unstructured.Unstructured{}
|
|
u.SetGroupVersionKind(gvk)
|
|
return u
|
|
}
|
|
|
|
// sourceProviderName is the name of the Forklift Provider that stands for a
|
|
// VMImportSource. It is derived from the Source rather than random so a
|
|
// reconcile after a controller restart finds the same object.
|
|
func sourceProviderName(sourceName string) string {
|
|
return "vmimport-" + sourceName
|
|
}
|
|
|
|
// destinationProviderName is the name of the Forklift Provider that stands for
|
|
// this cluster. Forklift models the local cluster as an "openshift"-type
|
|
// provider with an empty URL; the type name is Forklift's, not a statement
|
|
// about what Cozystack runs on.
|
|
func destinationProviderName(sourceName string) string {
|
|
return "vmimport-" + sourceName + "-destination"
|
|
}
|
|
|
|
// credentialsSecretName is the name of the Secret the controller projects a
|
|
// Source's credentials into for Forklift to consume.
|
|
func credentialsSecretName(sourceName string) string {
|
|
return "vmimport-" + sourceName + "-credentials"
|
|
}
|
|
|
|
// hostOverrideName is the name of the Forklift Host that redirects the disk
|
|
// transfer for one ESXi host, and of the Secret carrying that host's account.
|
|
func hostOverrideName(sourceName, hostID string) string {
|
|
return "vmimport-" + sourceName + "-host-" + sanitizeName(hostID)
|
|
}
|
|
|
|
// planName is the name of the Forklift Plan that migrates one source VM for a
|
|
// task. One Plan per VM keeps a single VM's failure from stalling its siblings.
|
|
func planName(taskName, vmID string) string {
|
|
return taskName + "-" + sanitizeName(vmID)
|
|
}
|
|
|
|
// mapName is the name of the Network/StorageMap a task owns. Both maps are
|
|
// per-task: their content is identical for every VM in the task.
|
|
func mapName(taskName string) string {
|
|
return "vmimport-" + taskName
|
|
}
|
|
|
|
// sanitizeName turns a provider-side identifier (`vm-1234`, and on other
|
|
// providers a UUID or a path) into something usable as a Kubernetes name
|
|
// segment. Managed-object references are already lowercase alphanumerics with
|
|
// dashes, but the API must not depend on that.
|
|
func sanitizeName(in string) string {
|
|
out := make([]rune, 0, len(in))
|
|
for _, r := range in {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-':
|
|
out = append(out, r)
|
|
case r >= 'A' && r <= 'Z':
|
|
out = append(out, r+('a'-'A'))
|
|
default:
|
|
out = append(out, '-')
|
|
}
|
|
}
|
|
// A leading or trailing dash is not a valid name segment.
|
|
trimmed := strings.Trim(string(out), "-")
|
|
if trimmed == "" {
|
|
return "unnamed"
|
|
}
|
|
if len(trimmed) > 40 {
|
|
trimmed = strings.Trim(trimmed[:40], "-")
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
// ownerRef returns a controller owner reference for the given object. Every
|
|
// piece of migration scaffolding carries one, so deleting the owning Source or
|
|
// Task garbage-collects it; the imported VMDisks and VMInstances deliberately
|
|
// carry none, which is what makes them outlive the task that produced them.
|
|
func ownerRef(gvk schema.GroupVersionKind, name string, uid types.UID) metav1.OwnerReference {
|
|
controller := true
|
|
blockOwnerDeletion := true
|
|
return metav1.OwnerReference{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
Name: name,
|
|
UID: uid,
|
|
Controller: &controller,
|
|
BlockOwnerDeletion: &blockOwnerDeletion,
|
|
}
|
|
}
|