Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Portainer migration assistant

A self-hosted tool for migrating workloads from OpenShift to Portainer-managed Kubernetes via GitOps. It connects to your OpenShift cluster, discovers workloads, translates them to standard Kubernetes manifests, commits them to a Git repository, and creates Portainer GitOps stacks that poll from that repo : one stack per workload.

The result is not just a migrated application. It is an application managed through Portainer GitOps from day one, with a full audit trail of every migration run.

How it works

The tool runs as a Docker container on any internal host. It talks to three external systems: your OpenShift cluster, a Git repository, and your Portainer instance. All credentials are encrypted at rest. Nothing is stored in plain text.

A migration runs in five steps.

Step 1: Connections. Connect the tool to OpenShift (bearer token), Git (PAT or SSH key), and Portainer (API token). Connections are saved per user and reusable across migrations.

Step 2: Discover. Pick a namespace and select which workloads to migrate. Workloads already managed by ArgoCD, Flux, Tekton, or Helm are automatically excluded and shown with the reason.

Step 3: Pre-flight. The tool runs a capability check against the target cluster across nine categories: operators and CRDs, storage and CSI, network and ingress, load balancers, ExternalDNS, Pod Security Admission, node scheduling, resource quotas, and image pull secrets. Hard failures block migration. Warnings require acknowledgment before proceeding.

Step 4: Translate. Manifests are generated and shown in an inline editor. You can adjust resource limits, fix image references, or make any other changes before anything gets committed. Secret handling is configurable per secret (include in git or redact with a placeholder).

Step 5: Migrate. Optionally run a server-side dry-run validation via the Kubernetes API before committing. Then start the migration: manifests are committed to Git and a Portainer GitOps stack is created per workload, pointed at the committed path on the branch you specified. Portainer polls the repo on the interval you select and applies changes automatically.

What gets translated

DeploymentConfig → Deployment. OpenShift's DeploymentConfig is translated to a standard apps/v1 Deployment. ImageChange triggers are detected and warned about . The equivalent in Kubernetes is an image update controller, which is out of scope for this tool.

Route → Ingress. OpenShift Routes are translated to networking.k8s.io/v1 Ingress resources. The translation is controller-agnostic : only the core Ingress spec (host, path, TLS, backend service) is generated. OpenShift HAProxy router annotations are stripped with warnings noting which features need to be reconfigured in the target Ingress controller. ExternalDNS annotations are preserved as-is. The spec.ingressClassName is set automatically from the IngressClass detected in the pre-flight check.

Services, ConfigMaps, Secrets. Carried across with OpenShift-specific annotations stripped. Service account tokens are not migrated : they are auto-generated in the target cluster. Secrets from the External Secrets Operator or Secrets Store CSI driver are handled separately : the operator CRs are committed and the tool warns that the same operator must be configured in the target cluster.

PersistentVolumeClaims. Handled via static PV re-attach, which assumes both clusters share a common storage backend (Ceph, NFS, EFS, SAN, or cloud block storage). The tool extracts the volumeHandle and CSI driver details from the source PV and generates a static PV manifest for the target cluster pointing at the same backing volume. A static PVC manifest binds to that specific PV, bypassing dynamic provisioning. For stateful workloads, the source must be scaled to zero before migration proceeds . The tool enforces this.

ServiceAccounts. Recreated in the target namespace. SCC bindings are stripped. Explicit security context values (runAsUser, runAsGroup, fsGroup) are carried forward into the pod spec.

ExternalDNS. DNSEndpoint CRs are committed as-is with a warning about the operator dependency.

Resource requests. If the source workload has no resource requests set, placeholder values (100m CPU, 32Mi memory) are injected. No limits are set by default . Configure based on observed usage in the target environment after migration.

OpenShift-specific fields with no Kubernetes equivalent are stripped from all manifests: schedulerName, terminationMessagePath, terminationMessagePolicy, empty securityContext blocks, BuildConfig references. Workloads using images from OpenShift's internal registry (image-registry.openshift-image-registry.svc) are flagged as blocked and cannot be migrated until the image is pushed to an external registry.

Pre-flight checks

Operators and CRDs. Checks for External Secrets Operator and Secrets Store CSI driver if the source workloads use them. Verifies operator readiness via status conditions.

Storage and CSI. Checks StorageClass availability by name, CSI driver presence and version match, and ReadWriteMany support where PVCs require it.

Network and Ingress. Checks for an IngressClass in the target cluster (required for Route translation). Verifies the CNI enforces NetworkPolicy if NetworkPolicies are present in the source.

Load balancer. Checks for MetalLB or a cloud controller manager if LoadBalancer services are present. Identifies which workloads would be affected if no provider is found.

ExternalDNS. Checks that the operator is installed and attempts to detect the configured provider from the ExternalDNS deployment args.

Pod Security Admission. Checks the namespace enforcement level against the privilege requirements of the workloads. Flags workloads that would be rejected under restricted policy.

Node scheduling. Checks that node labels referenced by nodeSelector constraints exist on target nodes. Flags OpenShift-specific role labels that may not exist in the target cluster.

Resource quotas. Checks whether the target namespace enforces CPU and memory limits, and flags workloads that do not have them set. Shows current quota usage vs hard limits.

Image pull secrets. Extracts all private registry references from selected workloads and checks whether matching dockerconfigjson secrets exist in the target namespace.

Dry-run validation

Before committing anything to Git, the Migrate step offers a server-side dry-run. Each manifest is submitted individually to the Kubernetes API through Portainer's transparent proxy with ?dryRun=All. The API runs full admission validation (quota enforcement, PSA policy, admission webhooks, schema validation) and returns the result without persisting anything. Per-file pass/fail results are shown with the exact rejection message from Kubernetes.

Requirements

Docker and Docker Compose. An OpenShift cluster with API access via bearer token (oc whoami -t). A Git repository with write access on GitHub, GitLab, Gitea, or any Git server. A Portainer Business Edition instance (2.39 or later) managing the target Kubernetes cluster.

Quick start

git clone https://github.com/portainer/migration-assistant
cd migration-assistant
cp .env.example .env

Edit .env:

ENCRYPTION_KEY=   # openssl rand -hex 32
JWT_SECRET=       # openssl rand -hex 32
mkdir data
docker compose up -d --build

Open https://localhost:4550. Accept the self-signed certificate warning. Create the admin account on first visit.

Configuration

Variable Required Default Description
ENCRYPTION_KEY Yes none 32+ character key for credential encryption. Losing this means losing access to all saved connections.
JWT_SECRET Yes none Secret for signing session tokens.
PORT No 4550 HTTPS port.
DATA_DIR No /data Path for persistent data inside the container.
SESSION_DURATION No 8h Session token expiry. Accepts any value jsonwebtoken supports: 4h, 24h, 7d.

TLS certificates

The tool generates a self-signed certificate on first start, saves it to ./data/certs/, and reuses it across restarts. The certificate is valid for three years.

To use your own certificate, place tls.crt and tls.key in ./data/certs/ before starting the container. The server detects and uses them automatically on startup.

Data persistence

All persistent data lives under ./data/:

data/
  db/
    migrator.db     SQLite: users, connections, migration history
  certs/
    tls.crt         TLS certificate
    tls.key         TLS private key

Back up ./data to preserve everything. The ENCRYPTION_KEY and the database must be backed up together . The key is required to decrypt the credentials stored in the database.

User management

The first user to register becomes the admin. Additional users must be created by the admin via the Users section. Each user manages their own saved connections : OpenShift clusters, Git targets, and Portainer instances are private per user. Sessions expire after SESSION_DURATION (default 8 hours).

Git authentication

PAT (Personal Access Token). Enter your Git username alongside the token. For GitHub fine-grained PATs, use your actual GitHub username. For GitHub classic PATs, oauth2 works as the username. For GitLab, use oauth2. For Gitea, use the token as both username and password. Classic PATs are more broadly compatible with third-party tooling including Portainer's GitOps stack configuration.

SSH key. Paste the private key in OpenSSH format. The server handles SSH operations. The key is encrypted at rest.

GitOps polling interval

The polling interval is configurable on the Migrate step: 5 minutes, 15 minutes, 30 minutes, 1 hour, or 24 hours. This sets the AutoUpdate.Interval on the Portainer GitOps stack. The default is 5 minutes.

Migration history

Every migration run is recorded with: source namespace, target environment and namespace, Git repository, branch, per-workload commit SHA, and timestamps. Migration records can be inspected, rerun, or deleted from the Migrations page. Deleting a record does not affect anything already deployed.

Known limitations

Internal ImageStream images. Workloads using images from OpenShift's internal registry cannot be migrated automatically. Push the image to an external registry first and update the image reference in the inline editor on the Translate step.

BuildConfig. BuildConfig resources are not migrated. Only the output image reference is extracted.

ImageStream triggers. Detected and warned about, but not translated. The Kubernetes equivalent requires an image update controller (Flux image reflector, Argo CD image updater). Configure this separately after migration.

SCC. OpenShift SCCs have no direct Kubernetes equivalent. SCC bindings are stripped from all ServiceAccounts. Explicit security context values are carried forward. Verify that workloads run correctly under the target namespace's Pod Security Admission policy.

Storage. PVC migration assumes a shared storage backend between source and target clusters. If the clusters use entirely separate storage backends, data must be moved independently. Velero-based data migration is the recommended approach for that case . This tool does not generate Velero manifests.

HAProxy annotations. Route annotations specific to HAProxy (timeouts, rate limiting, session affinity, IP allowlisting) are stripped. Equivalent configuration must be applied in the target Ingress controller manually.

Dry-run scope. Dry-run validation covers Kubernetes API admission : schema, quota, PSA, admission webhooks. It does not validate whether the workload will actually start successfully (image pull, volume availability, application startup).

Docker and Swarm. The tool only supports Kubernetes as a target. Docker and Swarm environments in Portainer are not supported.

Vanilla Kubernetes as source. The tool only supports OpenShift as a source. It uses OpenShift-specific APIs (Projects, Routes, DeploymentConfigs) that do not exist in vanilla Kubernetes.

Source workload behaviour during migration

The tool is intentionally non-destructive on the source OpenShift cluster. Source workloads are not scaled down, modified, or deleted during or after migration. They continue running normally throughout.

This is by design. If a migration fails partway through, the source remains fully operational with no rollback required. The intended cutover sequence is:

  1. Run the migration — source keeps running, target GitOps stack is created in Portainer
  2. Validate the target deployment is healthy and the application is functioning correctly
  3. Update DNS, load balancer, or ingress rules to route traffic to the target
  4. Confirm traffic is flowing correctly on the target
  5. Decommission the source workload manually once you are confident the migration is complete

The one exception is stateful workloads with PVCs using static re-attach. Those require the source to be scaled to zero before migration proceeds, to prevent two pods mounting the same persistent volume simultaneously. The tool enforces this as a hard gate and will not proceed until the source replicas are at zero.

Running both source and target simultaneously during the validation window is intentional — it gives you a safe rollback path if anything is wrong on the target before you cut over traffic.

About

A self-hosted tool for migrating workloads from OpenShift to Portainer-managed Kubernetes via GitOps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages