kubeportv0.1.0

Your chart works on k3s.
Here is why it dies on OpenShift.

kubeport is a portability linter and translator for Kubernetes workloads. Point it at your manifests or Helm chart, say where they run today and where they need to run next, and it tells you exactly what will break there. Then it rewrites what it safely can.

Install Source on GitHub brew install kubeport/tap/kubeport
$ kubeport check --from k3s:1.31 --to openshift:4.19/vsphere ./deploy
same six manifests · errors·warnings·info per target · every line below is real output of kubeport 0.1.0 on fixtures/k3s-app
kubeport 0.1.0 · 6 objects · k3s:1.31 -> openshift:4.19/vsphere
ERROR  k3s/traefik-crd                  Middleware/strip  apiVersion
       Middleware is a Traefik custom resource; OpenShift 4.19 (vSphere, thin-csi) does not run
       Traefik. Use an Ingress (or Route on OpenShift) or install Traefik on the target
ERROR  sec/allow-privilege-escalation   Deployment/api  spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
       container "api" must set allowPrivilegeEscalation: false
       fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/allow-privilege-escalation
ERROR  sec/capabilities                 Deployment/api  spec.template.spec.containers[0].securityContext.capabilities.drop
       container "api" does not drop ALL capabilities
       fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/capabilities
ERROR  sec/fixed-uid                    Deployment/api  spec.template.spec.securityContext.runAsUser
       pod pins runAsUser=1000; restricted-v2 assigns UIDs from the project range and rejects fixed
       UIDs outside it
       fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/fixed-uid
ERROR  sec/hostpath                     Deployment/api  spec.template.spec.volumes[1].hostPath
       volume "docker" mounts host path /var/run/docker.sock; refused by SCC restricted-v2
ERROR  stor/rwx-unsupported             StatefulSet/uploads  spec.volumeClaimTemplates[0].spec.accessModes
       claim "shared" asks for ReadWriteMany on StorageClass "local-path", which only provides
       single-node volumes
ERROR  stor/storageclass-missing        PersistentVolumeClaim/api-data  spec.storageClassName
       claim "api-data" uses StorageClass "local-path" which OpenShift 4.19 (vSphere, thin-csi) does
       not have; suggested: thin-csi
       fix available: kubeport translate --to openshift:4.19/vsphere --rule stor/storageclass-missing
ERROR  stor/storageclass-missing        StatefulSet/uploads  spec.volumeClaimTemplates[0].spec.storageClassName
       claim "shared" uses StorageClass "local-path" which OpenShift 4.19 (vSphere, thin-csi) does not
       have; suggested: thin-csi
       fix available: kubeport translate --to openshift:4.19/vsphere --rule stor/storageclass-missing
WARN   net/ingress-on-openshift         Ingress/api  metadata.annotations
       2 annotation(s) for another Ingress controller are ignored by OpenShift 4.19 (vSphere,
       thin-csi): traefik.ingress.kubernetes.io/router.middlewares,
       traefik.ingress.kubernetes.io/router.entrypoints. The OpenShift Router serves the Ingress but
       drops these; behaviour such as rewrites, middlewares or rate limits is lost
       fix available: kubeport translate --to openshift:4.19/vsphere --rule net/ingress-on-openshift
WARN   net/ingress-on-openshift         Ingress/api  spec.ingressClassName
       ingressClassName "traefik" targets a traefik controller; OpenShift 4.19 (vSphere, thin-csi)
       default class is "openshift-default"
       fix available: kubeport translate --to openshift:4.19/vsphere --rule net/ingress-on-openshift
WARN   sec/fixed-uid                    Deployment/api  spec.template.spec.securityContext.fsGroup
       pod pins fsGroup=1000; restricted-v2 requires fsGroup in the project range
       fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/fixed-uid
INFO   img/latest-tag                   Deployment/api  spec.template.spec.containers[0].image
       container "api" image "ghcr.io/acme/api:latest" has no pinned tag; different clusters will pull
       different images

12 finding(s) (8 error, 3 warn, 1 info) · 8 autofixable

Severity is decided per target. A fixed runAsUser is fine on k3s, a warning nowhere, and fatal on OpenShift's restricted-v2. local-path exists only on k3s. A Traefik Middleware needs Traefik. kubeport knows which cluster has what.

The API is the same everywhere. The contract around it is not.

Existing tools check API deprecations (kubent, Pluto), schemas (kubeconform) or best practices (kube-score, Polaris). None of them know that the three big distributions disagree on who picks the UID, what serves an Ingress, or whether type: LoadBalancer ever gets an address. These facts are data in kubeport, one YAML file per target version, plus a profile for your own cluster.

factk3s (default install)Kubernetes (kubeadm, kind)OpenShift
Pod securityPSA, nothing enforcedPSA, nothing enforced (Talos: restricted)SCC restricted-v2 on every project
Who picks the UIDthe imagethe imagethe project: an arbitrary UID from a range; fixed runAsUser is rejected
hostPath, privileged, host namespacesallowedallowedrefused without a custom SCC
Ingress controllerTraefik, class traefiknoneOpenShift Router (HAProxy), class openshift-default, plus Route
type: LoadBalancerServiceLB binds node IPspending foreverworks (platform LB)
Default StorageClasslocal-path, RWO onlynoneplatform: thin-csi, gp3-csi, ODF
Extra APIstraefik.io, helm.cattle.iononeroute, image, template, apps.openshift.io and more

check finds it. translate fixes what is deterministic.

Rewrites are applied only when they preserve behaviour: Route to Ingress and back, StorageClass mapping, fixed UID removal, restricted-profile hardening, removed API versions, OpenShift Templates into plain objects, DeploymentConfig into Deployment. Everything else becomes a note for a human. Unchanged objects are written back byte for byte, comments included.

$ kubeport translate --to openshift:4.19/vsphere ./deploy
kubeport 0.1.0 · translate -> openshift:4.19/vsphere
Deployment/api                 sec/allow-privilege-escalation set allowPrivilegeEscalation=false on api
Deployment/api                 sec/capabilities               added capabilities.drop [ALL] to api
Deployment/api                 sec/fixed-uid                  removed pod.runAsUser, pod.fsGroup; kept runAsNonRoot=true (OpenShift assigns the UID). Make sure the image's writable paths are group-writable by GID 0.
PersistentVolumeClaim/api-data stor/storageclass-missing      spec: storageClassName local-path -> thin-csi
Ingress/api                    net/ingress-on-openshift       converted Ingress to route.openshift.io/v1 Route Route/api
StatefulSet/uploads            stor/storageclass-missing      volumeClaimTemplates[0]: storageClassName local-path -> thin-csi

  manual follow-up:
  - Ingress/api [net/ingress-on-openshift]: Route uses the router's default certificate; to keep the certificate from Secret "api-tls", paste it into spec.tls.certificate/key or use cert-manager's openshift-routes integration
  - Ingress/api [net/ingress-on-openshift]: dropped controller annotations with no Route equivalent: traefik.ingress.kubernetes.io/router.entrypoints, traefik.ingress.kubernetes.io/router.middlewares. Re-express rewrites/timeouts with haproxy.router.openshift.io/* annotations

--- a/deploy/deployment.yaml
+++ b/deploy/deployment.yaml
@@ -16,8 +16,7 @@
         app: api
     spec:
       securityContext:
-        runAsUser: 1000
-        fsGroup: 1000
+        runAsNonRoot: true
       containers:
         - name: api
           image: ghcr.io/acme/api:latest
@@ -28,6 +27,11 @@
               mountPath: /data
             - name: docker
               mountPath: /var/run/docker.sock
+          securityContext:
+            allowPrivilegeEscalation: false
+            capabilities:
+              drop:
+                - ALL
       volumes:
         - name: data
           persistentVolumeClaim:
--- a/deploy/service.yaml
+++ b/deploy/service.yaml
@@ -11,29 +11,20 @@
       port: 80
       targetPort: 8080
 ---
-apiVersion: networking.k8s.io/v1
-kind: Ingress
+apiVersion: route.openshift.io/v1
+kind: Route
 metadata:
   name: api
-  annotations:
-    traefik.ingress.kubernetes.io/router.middlewares: default-strip@kubernetescrd
-    traefik.ingress.kubernetes.io/router.entrypoints: websecure
 spec:
-  ingressClassName: traefik
+  host: api.example.com
+  to:
+    name: api
+    kind: Service
+  port:
+    targetPort: 80
   tls:
-    - hosts: [api.example.com]
-      secretName: api-tls
-  rules:
-    - host: api.example.com
-      http:
-        paths:
-          - path: /
-            pathType: Prefix
-            backend:
-              service:
-                name: api
-                port:
-                  number: 80
+    termination: edge
+    insecureEdgeTerminationPolicy: Redirect
 ---
 apiVersion: traefik.io/v1alpha1
 kind: Middleware

  (dry run: add --write to apply)

29 rules, each one about a difference between clusters.

If a check would fire the same way on every target, it belongs in kube-score, not here. Each rule links to the upstream documentation and says which field to change. kubeport explain sec/fixed-uid gives the long version. Rules marked fix have a safe rewrite.

Security context and admission

  • sec/allow-privilege-escalationfixallowPrivilegeEscalation not set to false
  • sec/capabilitiesfixCapabilities not dropped or dangerous capabilities added
  • sec/fixed-uidfixHard-coded runAsUser on an arbitrary-UID target
  • sec/host-namespacesHost network, PID or IPC namespace
  • sec/hostpathhostPath volume
  • sec/privilegedPrivileged container
  • sec/psp-usagePodSecurityPolicy object
  • sec/run-as-rootfixContainer may run as root
  • sec/scc-annotationOpenShift SCC reference on a non-OpenShift target
  • sec/seccomp-profilefixNo seccomp profile

Networking

  • net/foreign-ingress-annotationsfixIngress annotations for a controller the target does not run
  • net/ingress-class-missingIngress without ingressClassName on a target with no default class
  • net/ingress-on-openshiftfixIngress that the OpenShift Router cannot honour
  • net/network-policy-defaultNo NetworkPolicy on a default-deny target
  • net/route-on-non-openshiftfixOpenShift Route on a target without the Route API
  • net/servicelb-assumptionService type LoadBalancer on a target with no LoadBalancer implementation

Storage

  • stor/rwx-unsupportedReadWriteMany on a class that cannot provide it
  • stor/storageclass-missingfixStorageClass not available on the target

Images

  • img/imagestream-on-non-openshiftImageStream reference outside OpenShift
  • img/latest-tagImage uses :latest or no tag

API versions

  • api/crd-missingCustom resource whose API group is not known on the target
  • api/deprecatedAPI version deprecated on the target's Kubernetes release
  • api/removedfixAPI version removed on the target's Kubernetes release

OpenShift objects

  • ocp/deploymentconfigfixDeploymentConfig
  • ocp/service-caOpenShift service-ca annotation
  • ocp/template-objectfixOpenShift Template outside OpenShift

k3s add-ons

  • k3s/traefik-crdTraefik custom resource outside k3s

Resources

  • res/limits-missingContainer without resource requests or limits

Install

One static binary, no dependencies, no cluster access, no network calls. Helm charts and Kustomize overlays are rendered with the helm, kustomize, kubectl or oc already on your PATH.

macOS, Linux
brew install kubeport/tap/kubeport
kubeport check --to openshift:4.19 ./deploy
Go
go install github.com/kubeport/kubeport/cmd/kubeport@latest
kubectl, oc plugin
kubectl krew install port
kubectl port check --to k3s:1.31 ./overlays/edge
oc port check --to openshift:4.19 -f ./chart
Windows, PowerShell
$v = (irm https://api.github.com/repos/kubeport/kubeport/releases/latest).tag_name
curl.exe -LO "https://github.com/kubeport/kubeport/releases/download/$v/kubeport_$($v.TrimStart('v'))_windows_amd64.zip"
Expand-Archive kubeport_*_windows_amd64.zip -DestinationPath $env:LOCALAPPDATA\kubeport
# add $env:LOCALAPPDATA\kubeport to PATH
From a live namespace
kubectl get deploy,sts,svc,ingress,pvc -n shop -o yaml |
  kubeport check --from k8s:1.32/eks --to openshift:4.19 -
Helm chart with values
kubeport check --to openshift:4.19/odf -f prod.yaml ./chart
# or render yourself
helm template shop ./chart | kubeport check --to k3s:1.31 -

Releases are built by GitHub Actions with goreleaser, ship an SBOM, and their checksums are signed with cosign. Verification is in every release's notes.

In CI, before it reaches the cluster.

Exit 1 on errors (configurable), SARIF for GitHub code scanning, a Markdown compatibility matrix for your README, and a shields.io badge endpoint. Also a pre-commit hook and a Tekton task.

Your cluster as a target

Platform teams write a profile once: the StorageClasses, Ingress class, installed CRDs and admission level of the real cluster. Application teams point their pipelines at it. kubeport profile init writes the starter file; schema.

# .github/workflows/portability.yml
- uses: kubeport/kubeport/action@v1
  with:
    path: ./chart
    from: k3s:1.31
    to: openshift:4.19/vsphere,k8s:1.32/eks
    fail-on: error
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: kubeport.sarif
# prod-cluster.yaml
extends: openshift:4.19
profile: prod
storage:
  classes: [thin-csi, ocs-storagecluster-cephfs]
  default_class: thin-csi
  rwx_classes: [ocs-storagecluster-cephfs]
  mapping: {local-path: thin-csi, gp3: thin-csi}
api_groups: [route.openshift.io, monitoring.coreos.com, cert-manager.io, external-secrets.io]
quotas_enforced: true
network_policy_default_deny: true

$ kubeport check --to ./prod-cluster.yaml ./deploy