> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Convert Kubernetes Manifests

> Migrate from Kubernetes to Control Plane by converting Deployments, StatefulSets, CronJobs, ConfigMaps, Secrets, Ingresses, and more.

The `cpln convert` command transforms Kubernetes manifests into Control Plane resources, enabling seamless migration from Kubernetes environments.

## When to use this

<CardGroup cols={2}>
  <Card title="Kubernetes migration" icon="right-left">
    Migrate existing K8s workloads, secrets, and storage to Control Plane
  </Card>

  <Card title="Reuse existing manifests" icon="recycle">
    Leverage your existing Kubernetes YAML files without rewriting them
  </Card>

  <Card title="Preview conversions" icon="eye">
    See how K8s resources translate to Control Plane before applying
  </Card>

  <Card title="CI/CD integration" icon="code">
    Convert and apply K8s manifests in automated pipelines
  </Card>
</CardGroup>

## Supported resources

### Workload conversions

| Kubernetes Kind       | Control Plane Kind                                | Notes                                   |
| --------------------- | ------------------------------------------------- | --------------------------------------- |
| Deployment            | [Workload](/reference/workload/general)           | Type determined by spec analysis        |
| ReplicaSet            | [Workload](/reference/workload/general)           | Type determined by spec analysis        |
| ReplicationController | [Workload](/reference/workload/general)           | Legacy K8s resource                     |
| StatefulSet           | [Workload](/reference/workload/general)           | Type determined by spec analysis        |
| DaemonSet             | [Workload](/reference/workload/general)           | Type determined by spec analysis        |
| Job                   | [Workload (cron)](/reference/workload/types#cron) | Converted to cron with default schedule |
| CronJob               | [Workload (cron)](/reference/workload/types#cron) | Schedule preserved from spec            |

### How workload type is determined

The converter analyzes your Kubernetes spec to automatically select the appropriate Control Plane workload type:

<AccordionGroup>
  <Accordion title="Cron">
    **Job** and **CronJob** resources always become `cron` workloads.

    * CronJobs preserve their schedule from `spec.schedule`
    * Jobs use a default schedule of `* * * * *` (every minute)
  </Accordion>

  <Accordion title="Stateful (highest priority)">
    The workload becomes `stateful` if any container mounts a **volumeset** (from PersistentVolumeClaim or volumeClaimTemplates).

    Stateful takes precedence over standard when both conditions are met.
  </Accordion>

  <Accordion title="Standard (default)">
    All non-cron workloads start as `standard`. The type remains `standard` unless a stateful condition is detected. Conditions that explicitly confirm `standard`:

    * A container has **no ports** or **more than one port**
    * A container uses **gRPC health probes** (liveness or readiness)
    * The workload has **rollout options** (from K8s `strategy`, `updateStrategy`, `minReadySeconds`, or `podManagementPolicy`)
  </Accordion>
</AccordionGroup>

### Secret and config conversions

| Kubernetes Kind | Control Plane Kind                                  | Notes                       |
| --------------- | --------------------------------------------------- | --------------------------- |
| Secret          | [Secret](/reference/secret)                         | Direct conversion           |
| ConfigMap       | [Secret (dictionary)](/reference/secret#dictionary) | Stored as dictionary secret |

### Storage conversions

| Kubernetes Kind       | Control Plane Kind                | Notes              |
| --------------------- | --------------------------------- | ------------------ |
| PersistentVolumeClaim | [Volumeset](/reference/volumeset) | Persistent storage |

### Domain conversions

| Kubernetes Kind | Control Plane Kind          | Notes                               |
| --------------- | --------------------------- | ----------------------------------- |
| Ingress         | [Domain](/reference/domain) | Routes converted from Ingress rules |

### How secrets are converted

Kubernetes Secrets are converted based on their `type` field:

| K8s Secret Type                  | Control Plane Secret Type | Notes                                                            |
| -------------------------------- | ------------------------- | ---------------------------------------------------------------- |
| `kubernetes.io/dockerconfigjson` | `docker`                  | Extracts `.dockerconfigjson` field                               |
| Any secret with a `payload` key  | `opaque`                  | Checked before type-specific handling                            |
| `kubernetes.io/basic-auth`       | `userpass`                | Extracts `username` and `password` fields                        |
| `kubernetes.io/tls`              | `dictionary`              | Validated for `tls.crt` and `tls.key`, stored as key-value pairs |
| Default                          | `dictionary`              | All other secrets become dictionaries                            |

<Tip>
  ConfigMaps are always converted to `dictionary` secrets, with all key-value pairs preserved. If a secret has both `data` and `stringData` fields, it is always converted to `dictionary` with both merged.
</Tip>

### How volumesets are configured

PersistentVolumeClaims are converted to volumesets with these settings:

| Property              | Source (in priority order)                                                       | Default               |
| --------------------- | -------------------------------------------------------------------------------- | --------------------- |
| **Capacity**          | PersistentVolume `spec.capacity.storage` → PVC `spec.resources.requests.storage` | 10 GB                 |
| **Performance class** | StorageClass parameters (see below)                                              | `general-purpose-ssd` |
| **File system type**  | PV volume source `fsType` → StorageClass `parameters.fsType`                     | `ext4`                |

**High-performance SSD detection:**

If the StorageClass `parameters` contain any of these values, the volumeset uses `high-throughput-ssd`:

* AWS: `io1`, `io2`
* GCP: `pd-extreme`
* Azure: `UltraSSD_LRS`
* VMware: `thick`
* Other: `fast`, `persistent_1`

### How domains are converted

Kubernetes Ingresses are converted to Control Plane [Domains](/reference/domain). The converter maps Ingress rules (host + paths) to domain routes, resolving backend Services to workloads.

#### Field mapping

| Kubernetes Ingress                          | Control Plane Domain       | Notes                                               |
| ------------------------------------------- | -------------------------- | --------------------------------------------------- |
| `metadata.labels`                           | `tags`                     | Labels preserved as domain tags                     |
| `spec.rules[].host`                         | `name`                     | Wildcard prefix (`*.`) stripped for domain name     |
| `spec.rules[].host` (wildcard)              | `spec.acceptAllSubdomains` | Set to `true` when host starts with `*.`            |
| `spec.rules[].http.paths[].path`            | Route `prefix` or `regex`  | Depends on `pathType`                               |
| `spec.rules[].http.paths[].backend.service` | Route `workloadLink`       | Resolved via Service selector → workload pod labels |
| Service `targetPort`                        | Route `port`               | Container port resolved through the Service         |

#### Path type handling

| Kubernetes `pathType`    | Control Plane Route        | Example                                               |
| ------------------------ | -------------------------- | ----------------------------------------------------- |
| `Prefix`                 | `prefix: "/api"`           | Default behavior                                      |
| `Exact`                  | `regex: "^/api/v1/users$"` | Special characters escaped, anchored with `^` and `$` |
| `ImplementationSpecific` | `prefix: "/legacy"`        | Treated as Prefix                                     |

#### Service-to-workload resolution

The converter resolves each Ingress backend to a Control Plane workload through the following steps:

<Steps>
  <Step title="Find the Service">
    The converter locates the Kubernetes Service referenced by the Ingress backend's `service.name`.
  </Step>

  <Step title="Match the workload">
    The Service's `selector` labels are matched against workload pod template labels to find the target workload.
  </Step>

  <Step title="Resolve the port">
    The Service port's `targetPort` is resolved to the actual container port:

    * **Numeric targetPort** — used directly as the container port
    * **Named targetPort** — resolved by matching the port name in the container spec
    * **No targetPort** — falls back to the Service port number
  </Step>
</Steps>

#### Domain defaults

All converted domains use these fixed settings:

| Property                  | Value    |
| ------------------------- | -------- |
| **Port**                  | `443`    |
| **Protocol**              | `http2`  |
| **DNS mode**              | `cname`  |
| **Certificate challenge** | `http01` |

#### Wildcard and apex domain merging

When an Ingress contains rules for both `example.com` and `*.example.com`, the converter merges them into a single domain named `example.com` with `acceptAllSubdomains: true`. Routes from both rules are combined and deduplicated.

<Warning>
  Domains with `acceptAllSubdomains` enabled require a [Dedicated Load Balancer](/reference/gvc#load-balancer) on the target GVC.
</Warning>

#### GVC context

If a `--gvc` is provided, workload links use it directly (e.g., `//gvc/my-gvc/workload/api`). Otherwise, the converter inserts a `{{GVC}}` placeholder that you must replace before applying.

### Auto-generated resources

When workloads reference secrets (via environment variables or volume mounts), the converter automatically creates:

1. **Identity** - Named `identity-{workload-name}`, linked to the workload
2. **Policy** - Named `policy-{workload-name}`, granting `reveal` permission on referenced secrets

This ensures workloads can access their secrets without manual policy configuration.

### Firewall and public exposure

The converter analyzes your Kubernetes Services and Ingresses to determine if a workload should be publicly accessible:

| Kubernetes Configuration                         | Control Plane Firewall                 |
| ------------------------------------------------ | -------------------------------------- |
| Service type `LoadBalancer` matching pod labels  | External inbound allowed (`0.0.0.0/0`) |
| Ingress routing to a Service matching pod labels | External inbound allowed (`0.0.0.0/0`) |
| No public exposure detected                      | External inbound blocked (default)     |

<Info>
  All workloads have external **outbound** traffic allowed by default (`0.0.0.0/0`).
</Info>

### Informational resources

These Kubernetes resources are not directly converted but inform the conversion:

| Resource                    | How it's used                                                          |
| --------------------------- | ---------------------------------------------------------------------- |
| **HorizontalPodAutoscaler** | Sets `minScale`, `maxScale`, `scaleToZeroDelay`, and CPU target        |
| **Service**                 | Protocol inference, public exposure detection, domain route resolution |
| **ServiceAccount**          | Image pull secrets extraction                                          |
| **EndpointSlice**           | Service-to-Pod mapping for selectorless services                       |
| **PersistentVolume**        | Capacity and file system type for volumesets                           |
| **StorageClass**            | Performance class and file system type for volumesets                  |

## Basic usage

```bash theme={null}
cpln convert --file <k8s-file>
```

This outputs the converted Control Plane resources to stdout.

## Options

| Option       | Description                                                               |
| ------------ | ------------------------------------------------------------------------- |
| `--file`     | Path to K8s JSON/YAML file. Use `--file -` for stdin                      |
| `--protocol` | Override port protocol for all containers: `http`, `http2`, `grpc`, `tcp` |
| `--verbose`  | Show original K8s resources with ignored properties highlighted           |

## How port protocol is inferred

When you don't specify `--protocol`, the converter automatically infers the protocol for each container port using a multi-level strategy (in priority order):

<Steps>
  <Step title="Service appProtocol (highest priority)">
    If a Kubernetes Service explicitly declares `appProtocol` on a port that targets this container, that protocol is used.

    ```yaml theme={null}
    spec:
      ports:
        - port: 50051
          targetPort: 50051
          appProtocol: grpc  # Explicitly declares gRPC
    ```
  </Step>

  <Step title="Service port name prefix">
    The converter checks if any Service port targeting this container has a name with a protocol prefix.

    ```yaml theme={null}
    spec:
      ports:
        - name: grpc-api  # "grpc" prefix → gRPC protocol
          port: 50051
    ```
  </Step>

  <Step title="Container port name prefix">
    The container's own port name is checked for protocol prefixes.

    ```yaml theme={null}
    ports:
      - name: http-web  # "http" prefix → HTTP protocol
        containerPort: 8080
    ```
  </Step>

  <Step title="Health probe type">
    If the port has a liveness or readiness probe, the probe type determines the protocol:

    * `grpc` probe → gRPC
    * `httpGet` probe → HTTP
    * `tcpSocket` probe → TCP
  </Step>

  <Step title="Well-known port number (lowest priority)">
    Common port numbers are mapped to their typical protocols. See the tables below.
  </Step>
</Steps>

### Recognized protocol prefixes

Port names starting with these prefixes are automatically mapped:

| Prefix                        | Protocol |
| ----------------------------- | -------- |
| `http`, `https`, `ws`, `wss`  | HTTP     |
| `http2`, `h2`, `h2c`          | HTTP/2   |
| `grpc`, `grpc-web`, `grpcweb` | gRPC     |

### Well-known port mappings

These port numbers are automatically assigned protocols when no other signal is available:

<Tabs>
  <Tab title="HTTP ports">
    | Port                         | Protocol |
    | ---------------------------- | -------- |
    | 80, 81, 443                  | HTTP     |
    | 3000, 3001, 3002             | HTTP     |
    | 8000, 8008, 8080, 8081, 8088 | HTTP     |
    | 8443, 9090                   | HTTP     |
  </Tab>

  <Tab title="gRPC ports">
    | Port                              | Protocol |
    | --------------------------------- | -------- |
    | 50051, 50052, 50053, 50054, 50055 | gRPC     |
    | 6565                              | gRPC     |
  </Tab>
</Tabs>

<Note>
  If no protocol can be inferred, the converter defaults to `tcp`.
</Note>

## Example conversion

<Steps>
  <Step title="Create a Kubernetes manifest">
    Create a file named `k8s.yaml`:

    ```yaml k8s.yaml theme={null}
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: example-deployment
      labels:
        app: nginx
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
            - name: nginx
              image: nginx:latest
              ports:
                - containerPort: 80
    ```
  </Step>

  <Step title="Convert the manifest">
    ```bash theme={null}
    cpln convert --file k8s.yaml
    ```

    <Accordion title="View output">
      ```yaml theme={null}
      kind: workload
      name: example-deployment
      spec:
        type: standard
        containers:
          - name: nginx
            image: 'nginx:latest'
            ports:
              - number: 80
                protocol: http
        defaultOptions:
          capacityAI: false
          autoscaling:
            minScale: 3
            maxScale: 3
      ```
    </Accordion>
  </Step>

  <Step title="Apply the converted resources">
    Convert and apply in one command:

    ```bash theme={null}
    cpln apply --file k8s.yaml --k8s true
    ```

    Or pipe the conversion output:

    ```bash theme={null}
    cpln convert --file k8s.yaml | cpln apply --file -
    ```
  </Step>
</Steps>

## More conversion examples

<Tabs>
  <Tab title="Standard (default)">
    A simple Deployment with one HTTP port becomes standard (the default type):

    ```yaml k8s-deployment.yaml theme={null}
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: api
      template:
        spec:
          containers:
            - name: api
              image: my-api:v1
              ports:
                - containerPort: 8080
    ```

    Converts to a **standard** workload (the default type for all non-cron, non-stateful workloads).
  </Tab>

  <Tab title="Standard (explicit)">
    A Deployment with multiple ports or gRPC probes also becomes standard:

    ```yaml k8s-multiport.yaml theme={null}
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: grpc-service
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: grpc-service
      template:
        spec:
          containers:
            - name: grpc-service
              image: my-grpc:v1
              ports:
                - containerPort: 50051
              readinessProbe:
                grpc:
                  port: 50051
    ```

    Converts to a **standard** workload (gRPC probe triggers standard type).
  </Tab>

  <Tab title="Stateful">
    A StatefulSet with persistent storage becomes stateful:

    ```yaml k8s-statefulset.yaml theme={null}
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: postgres
    spec:
      serviceName: postgres
      replicas: 1
      selector:
        matchLabels:
          app: postgres
      template:
        spec:
          containers:
            - name: postgres
              image: postgres:15
              ports:
                - containerPort: 5432
              volumeMounts:
                - name: data
                  mountPath: /var/lib/postgresql/data
      volumeClaimTemplates:
        - metadata:
            name: data
          spec:
            accessModes: ["ReadWriteOnce"]
            resources:
              requests:
                storage: 10Gi
    ```

    Converts to a **stateful** workload with a volumeset (storage triggers stateful type).
  </Tab>

  <Tab title="Cron">
    CronJobs preserve their schedule:

    ```yaml k8s-cronjob.yaml theme={null}
    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: daily-backup
    spec:
      schedule: "0 2 * * *"
      jobTemplate:
        spec:
          template:
            spec:
              containers:
                - name: backup
                  image: backup-tool:latest
                  command: ["/bin/sh", "-c", "backup.sh"]
              restartPolicy: OnFailure
    ```

    Converts to a **cron** workload with schedule `0 2 * * *`.
  </Tab>

  <Tab title="ConfigMap">
    ConfigMaps become dictionary secrets:

    ```yaml k8s-configmap.yaml theme={null}
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: app-config
    data:
      DATABASE_URL: "postgres://localhost:5432/mydb"
      LOG_LEVEL: "info"
    ```

    Converts to a **dictionary secret** containing the key-value pairs.
  </Tab>

  <Tab title="Domain (Ingress)">
    An Ingress with a Deployment and Service becomes a domain with routes:

    ```yaml k8s-ingress.yaml theme={null}
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api-server
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: api
      template:
        metadata:
          labels:
            app: api
        spec:
          containers:
            - name: api
              image: my-api:v1
              ports:
                - containerPort: 8080
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: api-service
    spec:
      selector:
        app: api
      ports:
        - port: 80
          targetPort: 8080
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: api-ingress
      labels:
        team: backend
    spec:
      rules:
        - host: api.example.com
          http:
            paths:
              - path: /api
                pathType: Prefix
                backend:
                  service:
                    name: api-service
                    port:
                      number: 80
    ```

    Converts to a **workload** and a **domain** with routes pointing to the workload:

    <Accordion title="View output">
      ```yaml theme={null}
      kind: workload
      name: api-server
      spec:
        type: standard
        containers:
          - name: api
            image: 'my-api:v1'
            ports:
              - number: 8080
                protocol: http
        defaultOptions:
          capacityAI: false
          autoscaling:
            minScale: 2
            maxScale: 2
        firewallConfig:
          external:
            inboundAllowCIDR:
              - 0.0.0.0/0
            outboundAllowCIDR:
              - 0.0.0.0/0
      ---
      kind: domain
      name: api.example.com
      description: 'Converted from K8s Ingress: api-ingress'
      tags:
        team: backend
      spec:
        dnsMode: cname
        certChallengeType: http01
        ports:
          - number: 443
            protocol: http2
            routes:
              - prefix: /api
                workloadLink: '//gvc/{{GVC}}/workload/api-server'
                port: 8080
      ```
    </Accordion>

    <Tip>
      Provide `--gvc` to replace the `{{GVC}}` placeholder with an actual GVC name in workload links.
    </Tip>
  </Tab>
</Tabs>

## Override protocol inference

To bypass automatic protocol inference and set a specific protocol for all ports, use the `--protocol` flag:

```bash theme={null}
# Force all ports to use gRPC
cpln convert --file k8s.yaml --protocol grpc

# Force all ports to use TCP
cpln convert --file k8s.yaml --protocol tcp
```

## Verbose mode

Use `--verbose` to see which K8s properties were converted and which were ignored:

```bash theme={null}
cpln convert --file k8s.yaml --verbose
```

Ignored properties are highlighted in yellow and marked with `(ignored)`.

## Delete converted resources

To remove resources that were applied from a K8s manifest:

```bash theme={null}
cpln delete --file k8s.yaml --k8s true
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Resource type not supported">
    Some Kubernetes resources don't have direct Control Plane equivalents. Use `--verbose` to see which properties are ignored during conversion.
  </Accordion>

  <Accordion title="ConfigMap values not appearing">
    ConfigMaps are converted to dictionary secrets. Reference them in your workload using environment variables or volume mounts pointing to the secret.
  </Accordion>

  <Accordion title="HPA settings not applied">
    HorizontalPodAutoscaler settings inform the workload's autoscaling configuration. Check the converted workload's `defaultOptions.autoscaling` section.
  </Accordion>

  <Accordion title="Service ports not exposed">
    Services are used to inform port mappings but don't create standalone resources. Ports are configured directly on the workload's container spec.
  </Accordion>

  <Accordion title="PVC not creating volumeset">
    PersistentVolumeClaims are converted to volumesets. Ensure the PVC is referenced by a StatefulSet's volumeClaimTemplates for proper conversion.
  </Accordion>

  <Accordion title="Ingress not creating a domain">
    Ingress conversion requires the referenced Services and their matching workloads (Deployments, StatefulSets, etc.) to be present in the same file. The converter uses the Service selector to find the target workload by matching pod template labels.
  </Accordion>

  <Accordion title="Service without selector error">
    Ingress backend Services must have a `selector` defined so the converter can resolve which workload the Service targets. Services without selectors (e.g., ExternalName services) are not supported for domain conversion.
  </Accordion>

  <Accordion title="Workload link shows {{GVC}} placeholder">
    When converting Ingresses without specifying `--gvc`, workload links contain a `{{GVC}}` placeholder. Provide `--gvc` during conversion or replace the placeholder before applying.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Apply YAML Manifests" href="/guides/cpln-apply" icon="file-code">
    Learn more about applying resources
  </Card>

  <Card title="Delete Resources" href="/guides/cpln-delete" icon="trash">
    Remove resources from Control Plane
  </Card>

  <Card title="Workload Reference" href="/reference/workload/general" icon="server">
    Understand workload configuration
  </Card>

  <Card title="Convert Command Reference" href="/cli-reference/commands/convert" icon="book">
    Convert command reference
  </Card>
</CardGroup>
