deploy
Deploy a workload to a namespace on an OpenShift or Kubernetes cluster — either a KubeVirt VirtualMachine or a Deployment + Service. Give it the info; it renders an auditable manifest, then applies it with oc (or kubectl). Does not record a billable usage event: cluster apply is local to your kubeconfig. Only usage kind migration is billable.
# A VM from a containerDisk image
vrtmv deploy --kind vm -n prod --name web01 \
--image quay.io/example/rhel9-container-disk:latest --cpu 2 --memory 4Gi
# A VM booting from the migrated disk (imported via CDI)
vrtmv deploy --kind vm -n prod --name web01 \
--disk-url https://mirror.internal/disks/web01.qcow2 --disk-size 40Gi
# A container, exposed with a Service + Route
vrtmv deploy --kind container -n staging --name api \
--image registry.example.com/api:1.4 --replicas 3 --port 8080 --route \
--attestation out/vrtmv-attestation.json
# A container with volume-class data: a claim per path, filled before it starts
vrtmv deploy --kind container -n staging --name site \
--image registry.example.com/site:1 --port 80 \
--attestation out/vrtmv-attestation.json \
--data-manifest plan/vrtmv-data-attestation.json \
--data-claim /srv/site=site-data:10Gi \
--data-source-url https://nyc3.digitaloceanspaces.com/my-bucket/site-plan \
--data-credentials-secret bucket-credentials
| Flag | Purpose |
|---|---|
--kind <vm|container> | What to deploy. |
-n, --namespace <ns> | Target namespace. |
--name <name> | Workload name (DNS-1123). |
--image <ref> | OCI image (container) or containerDisk image (vm). |
--disk-url <url> + --disk-size <q> | VM only: import the migrated disk (CDI DataVolume) instead of a containerDisk. |
--storage-class <name> | VM only: StorageClass for the imported disk. |
--cpu <n>, --memory <q> | vCPU cores and memory (default 1 / 2Gi). |
--replicas <n> | Container only (default 1). |
--port <n>, --route [--host <fqdn>] | Container only: expose via a Service, and optionally an OpenShift Route. |
--server <url>, --token <t> | Cluster API + bearer token. Omit to use your current oc login. |
--insecure-skip-tls-verify | Skip API-server TLS check (labs only). |
-o, --out <path> | Where to write the manifest (default vrtmv-deploy.yaml). |
--dry-run | Render the manifest but don't apply it. |
--kubectl | Apply with kubectl instead of oc. |
--attestation <file> | Container only: the vrtmv-attestation.json that migrate --format container wrote. --kind container needs this or --no-attestation. See A container's data. |
--no-attestation | Container only: render without reading an attestation. The Deployment has no volume and an annotation says nothing was read. |
--data-claim <path>=<claim>:<size> | Container only: the claim for a volume-class data path, and the size it asks for. Repeatable, one for every volume-class path. |
--data-manifest <file> | Container only: the vrtmv-data-attestation.json of vrtmv data plan --dest-claim, for the file hashes. |
--data-source-url <url> | Container only: where in an S3-compatible bucket the archives and lists were uploaded. No query and no @ in the URL. |
--data-source-region <region> | Container only: the region requests are signed for (default us-east-1). |
--data-credentials-secret <name> | Container only: a Secret in the namespace with the keys AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Omit for a bucket that needs none. |
--data-helper-image <image> | Container only: the image the init container runs. Default: registry.access.redhat.com/ubi10/ubi@sha256:27e14f4987d7abe56664d7e1b1dddcd0226d4ca593da5726f0f65697a17d0fee. |
--data-storage-class <name> | Container only: the StorageClass of the claims. Omit for the cluster's default. |
How it works
deploy renders the manifest, keeps it as an auditable artifact (vrtmv-deploy.yaml, on brand with vrtmv's other outputs), then applies it. Cluster access is your current oc login (recommended) or explicit --server/--token. When you pass --token, vrtmv hands it to oc through a private temporary kubeconfig rather than the command line, so the credential never appears in the process list. --dry-run renders without a cluster.
Inputs are validated before the cluster is touched — names as DNS-1123 labels, memory/disk as Kubernetes quantities, the image as a reference, and --disk-url/--host as quote-free values — so a mistake errors with guidance instead of producing a bad object.
A container's data
A Deployment with no volume starts a workload that has data on an empty directory, and the pod reports as running. So deploy --kind container reads the data declarations of the migration (migrate --format container --data <path>=<class>) from --attestation, and does one of three things:
- The attestation declares no volume-class path: the Deployment is rendered with no volume, and the output says so.
- It declares volume-class paths and every one can be given a claim that is filled and checked: the manifest gets a
PersistentVolumeClaimper path and an init container per claim, described below. - Anything else is refused, the paths are named, and no manifest is written. That covers a volume-class path with no
--data-claim, a path the data plan manifest has no file hashes for, a manifest that lists fewer files under a path than the attestation counted on the source, a volume-class path that has another declared path under it, and--replicasabove 1.
With neither --attestation nor --no-attestation it refuses as well. --no-attestation is your statement that nothing is to be read: the Deployment has no volume, the output says vrtmv does not know whether the workload has volume-class data, and the manifest carries the annotation vrtmv.com/data: "not read: rendered with --no-attestation". An attestation written by 2.2.0 or earlier, or by a migrate that was not --format container, has no data declarations and is refused by --attestation.
Secret files the image was built without
migrate --format container leaves secret files out of the image and records each in the attestation's container_secrets. deploy reads that record from --attestation and writes a read-only mount for each file marked delivery: mount, at the file's own path, from one Secret named <name>-vrtmv-secrets. vrtmv does not create that Secret and never reads it. You create it from the source with the script migrate wrote, and deploy prints the command:
sh out/container/stage-secrets.sh /mnt/src kubernetes web-vrtmv-secrets shop
To have the Secret as a manifest file instead, for kubectl apply -f, for committing encrypted, or for a secret store, give manifest and a file name: sh out/container/stage-secrets.sh /mnt/src manifest web-vrtmv-secrets shop web-secrets.yaml. Nothing is created in the cluster, and the file holds the contents of the secret files. See Mounting the files left out.
Until the Secret exists the pod does not start. The manifest carries the annotation vrtmv.com/secrets, which reads one of three ways:
"<n> mounted, <m> left out and not mounted": the attestation has the record. The output names each file that is left out and not mounted, with the reason."not recorded by the attestation": the attestation has nocontainer_secrets, because a build before this one wrote it. vrtmv does not know whether the image lacks a file it needs, says so, and mounts nothing. This is not the same as an image with no secret left out."not read: rendered with --no-attestation".
An attestation whose record names a key this build would not make for the path is refused. See Mounting the files left out.
What the init container does
The data arrives in three steps, and vrtmv runs the last one only by way of the manifest:
vrtmv data plan --copy <path> --dest-claim <path>=<claim>writes a script and, underclaims/, the list of hashes<claim>.sha256. See Kubernetes claims as the destination.- You run the script, which packs
<claim>.tar, and upload both files to one place in an S3-compatible bucket (S3, DigitalOcean Spaces, MinIO). deployrenders the claim and an init container that reads the two files from--data-source-url.
The init container fetches <url>/<claim>.sha256 and compares its sha256 with the one deploy computed from --data-manifest, so a list that is not the manifest's is refused. It then fetches <url>/<claim>.tar, unpacks it into a working directory on the claim, and requires that the regular files it unpacked are exactly the files of the list and that each one's sha256 matches. Only then does it move the files to the claim's data directory, which is what the workload's container mounts over the path, and write a record beside it. Any failure ends the init container with a non-zero exit: nothing is in data, the workload's container does not start, and the pod shows Init:Error. The cluster starts the init container again, so a bucket that is put right is picked up without a new deploy.
A pod's init containers run every time the pod starts. A claim that has its record is not filled again, so what the workload has written since is not replaced by the source's files. The record names the list of hashes the claim was checked against, and the init container compares it with the list this Deployment was rendered from:
- The same list, and
datais there: it exits 0 and the workload starts. - Another list: it exits non-zero and the workload does not start. This is what happens when you plan the data again after the source changed and deploy with the new manifest on a claim that is already filled. A new data plan does not reach a filled claim. The pod spec would otherwise say the files were checked against a manifest they were never compared with. Deploy with the manifest the claim was filled from (
kubectl rollout undoputs the previous pod spec back), or name a new claim and fill that. To fill the same claim from the new plan, delete the Deployment and then the claim (kubectl delete deployment <name>,kubectl delete pvc <claim>) and deploy again with the new manifest; the claim is made and filled afresh, and what the workload wrote on the old one is deleted with it. The init container's message names both lists by sha256 and says the same. A plan of an unchanged source gives the same list, and is not this case. - The record is there and
datais not: it exits non-zero. The cluster would otherwise make an empty directory for the mount.
A claim that holds a data directory and no record is refused too: vrtmv did not put it there.
The Deployment's strategy is Recreate and each claim is ReadWriteOnce. The init container runs as root, because it has to give each file its owner; a cluster that forbids that refuses the pod.
What is checked, and what is not
Checked before the workload starts: the bytes of every regular file, and that no regular file is there that the manifest does not list. Carried by the archive and not checked: owners, modes (a setuid bit included), links, empty directories and special files. A link or a fifo in the archive is placed as it is and is on no list. Not carried: ACLs and extended attributes. A file name with a control character in it is refused by data plan, since the list is read line by line. A name that is not UTF-8 is in the manifest under another spelling, so the init container does not find it and fails.
A database the data plan exports has no file hashes by design. Its claim is created and mounted, no init container fills it, and the output says NOT FILLED. The engine starts on an empty directory and you run the import of the database runbook against it.
The bucket's credential
vrtmv never reads it. You create the Secret; deploy writes its name into the pod spec, and the cluster gives the two values to the init container as environment variables. They are not given to the workload's container. They are in no manifest, report or attestation vrtmv writes. A --data-source-url with a query string or an @ anywhere in it is refused without being repeated, because a pre-signed URL or user:password@ would put a credential into the manifest. That rules out a prefix with an @ in its name too: a password can hold a /, so the end of the host cannot be told.
The default image is Red Hat's UBI 10, about 80 MB to pull, from a registry that needs no login. It is pinned by digest, so it does not change until a vrtmv release changes it. Any image with /bin/sh, tar, find, sha256sum, sed, sort and a recent curl can be named instead; the script checks that each is there, and not that it is the GNU one, which is what was run. The request is signed by curl's --aws-sigv4: curl 8.5 and 8.12 were run against MinIO, and 7.81 was refused by it with 403. vrtmv ships no image of its own.
The init container gives up on a store it cannot connect to in 30 seconds or that sends nothing for five minutes, and the cluster starts it again. Those two limits were not exercised.
The eight flags above are refused with --kind vm.
To check a claim afterwards, see verify --data-claim.
From a migration
vrtmv migrate can emit the deploy manifest alongside the role and attestation when you give it a disk source:
vrtmv migrate --root /mnt --target rocky9 -o out \
--deploy-image quay.io/you/rocky9:latest --deploy-namespace prod --deploy-name web01
# → out/deploy/vrtmv-deploy.yaml (review, then: oc apply -f)
Prerequisites
- The OpenShift CLI
oc(orkubectlwith--kubectl) on yourPATH. - For
--kind vm: OpenShift Virtualization / KubeVirt on the cluster; for--disk-url, the CDI (Containerized Data Importer) operator. - For
--route: an OpenShift cluster (Routes are OpenShift-specific).