Files
cozystack/packages/system/migration-controller/templates/deployment.yaml
T
Andrei Kvapil 54621bc046 feat(migration): tenant self-service VM import from VMware vSphere
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>
2026-09-07 17:03:44 +02:00

86 lines
2.8 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: migration-controller
labels:
app: migration-controller
spec:
replicas: {{ .Values.migrationController.replicas }}
selector:
matchLabels:
app: migration-controller
template:
metadata:
labels:
app: migration-controller
spec:
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
serviceAccountName: migration-controller
{{- /*
A controller whose whole purpose is to keep an import inside the
restricted Pod Security Standard has no business violating it itself.
The image is a static binary in a scratch layer, so there is nothing to
give up here: without these fields the apiserver admits the pod with a
restricted-violation warning, which is both a bad example and noise in
every operator's audit log.
*/}}
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: migration-controller
image: "{{ .Values.migrationController.image }}"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
args:
- --leader-elect
{{- if .Values.migrationController.metrics.enabled }}
- --metrics-bind-address={{ .Values.migrationController.metrics.bindAddress }}
{{- end }}
{{- if .Values.migrationController.debug }}
- --zap-log-level=debug
{{- else }}
- --zap-log-level=info
{{- end }}
env:
{{- /*
The VDDK image reference travels as configuration, not as a tenant
value, and only a reference travels — never a credential. Empty is a
valid, documented state: the vSphere path then reports itself
unavailable on each VMImportSource instead of failing mid-transfer.
*/}}
- name: MIGRATION_VDDK_IMAGE
value: {{ .Values.vddkImage | default "" | quote }}
ports:
- name: metrics
containerPort: {{ splitList ":" .Values.migrationController.metrics.bindAddress | mustLast }}
- name: health
containerPort: 8081
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
{{- with .Values.migrationController.resources }}
resources: {{- . | toYaml | nindent 10 }}
{{- end }}