> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-jakob-ess-2-0-0.mintlify.site/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="Pinned via annotation (highest priority)">
    Set the `cpln.io/workload-type` annotation on the Deployment, StatefulSet, DaemonSet, ReplicaSet, or its pod template to pin the type explicitly:

    ```yaml theme={null}
    metadata:
      annotations:
        cpln.io/workload-type: serverless  # serverless | standard | stateful
    ```

    The pin overrides the automatic inference below. A workload that mounts a **dedicated** volumeset can only be pinned to `stateful`, and pinning `serverless` on a workload whose shape conflicts with serverless requirements (e.g., multiple ports) emits a warning and is validated by the API. Jobs and CronJobs always convert to `cron` and ignore the annotation.
  </Accordion>

  <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">
    The workload becomes `stateful` if any container mounts a **dedicated volumeset** (from a PersistentVolumeClaim or volumeClaimTemplates with single-node access modes such as `ReadWriteOnce`).

    Claims with multi-node access modes (`ReadWriteMany`, `ReadOnlyMany`) convert to [shared volumesets](#how-volumesets-are-configured), which any workload type can mount — they do not force the stateful type.

    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>

<Warning>
  The workload type is **immutable** after creation. When a converted workload already exists with a different type, `cpln apply --k8s` fails with `Workload type may not be changed` (405). Pin the type with the `cpln.io/workload-type` annotation to keep conversions stable across CLI versions. CLI versions before 3.9.0 inferred `serverless` for some Deployments; from 3.9.0 on, the default is `standard`.
</Warning>

### Conversion annotations

The converter honors these annotations on the workload object metadata (Deployment, StatefulSet, DaemonSet, ReplicaSet, ReplicationController, Job, CronJob) or on its pod template metadata. Object-level annotations take precedence over template-level ones.

| Annotation                            | Values                               | Effect                                                                                                                          |
| ------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `cpln.io/workload-type`               | `serverless`, `standard`, `stateful` | Pins the converted workload type, overriding automatic inference                                                                |
| `cpln.io/inherit-env`                 | `true`, `false`                      | Sets `inheritEnv` on every converted container, so [GVC-level environment variables](/reference/gvc#env) flow into the workload |
| `cpln.io/internal-inbound-allow-type` | `none`, `same-gvc`, `same-org`       | Overrides the [internal firewall inference](#firewall-and-public-exposure) for the converted workload                           |

<Note>
  `cron` and `vm` cannot be pinned. Cron workloads carry a job schedule that only Jobs and CronJobs provide (they always convert to `cron`). [VM workloads](/reference/workload/types) boot from a disk image instead of running containers, so no container-based Kubernetes kind can convert to one.
</Note>

```yaml Example theme={null}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  annotations:
    cpln.io/workload-type: serverless
    cpln.io/inherit-env: 'true'
    cpln.io/internal-inbound-allow-type: same-gvc
```

<Tip>
  With kustomize, apply annotations across a whole stack using `commonAnnotations`, or per-resource with a patch.
</Tip>

### Environment variables

Container environment variables convert as follows:

| Kubernetes source                      | Control Plane result                                                              |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| `env` with `value`                     | Inline value, preserved as-is                                                     |
| `env` with `valueFrom.configMapKeyRef` | `cpln://secret/<configmap-name>.<key>` reference                                  |
| `env` with `valueFrom.secretKeyRef`    | `cpln://secret/<secret-name>.<key>` reference                                     |
| `env` with `valueFrom.fieldRef`        | `cpln://reference/<fieldPath>` reference                                          |
| `envFrom` with `configMapRef`          | One variable per ConfigMap `data` key, referencing the converted secret           |
| `envFrom` with `secretRef`             | One variable per Secret `data`/`stringData` key, referencing the converted secret |

`envFrom` expansion follows Kubernetes semantics:

* An explicit `env` entry **overrides** an `envFrom` variable with the same name.
* A key present in multiple `envFrom` sources takes the value of the **last** source.
* A `prefix` on the source is prepended to every expanded variable name.
* ConfigMap `binaryData` keys are not expanded (Kubernetes does not expose them as environment variables either).

<Warning>
  The converter needs the referenced ConfigMap or Secret **in the conversion input** to know its keys:

  * A missing non-optional `configMapRef` fails the conversion (the pod could not start on Kubernetes either).
  * A missing `optional: true` `configMapRef` is skipped with a warning.
  * A missing `secretRef` (e.g., a Secret that already exists on Control Plane) produces a **warning** because its keys cannot be enumerated — include the Secret manifest in the input or add the variables to `env` manually.
  * A `secretRef` to a Secret that converts to a non-dictionary type (a `payload` key becomes opaque, basic-auth becomes userpass) is skipped with a warning, since its keys are not individually addressable.
  * Expanded variable names starting with `CPLN_` are skipped with a warning; that prefix is reserved.
</Warning>

### 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**  | PVC `accessModes` (see below) → PV volume source `fsType` → StorageClass `parameters.fsType` | `ext4`                |

**Shared volumesets from multi-node access modes:**

A PVC whose `accessModes` include `ReadWriteMany` or `ReadOnlyMany` converts to a **shared** volumeset (`fileSystemType: shared`, `performanceClass: shared`). Shared volumesets can be mounted by workloads of any type and by multiple workloads at once — matching how RWX claims behave on Kubernetes. All other claims convert to dedicated block-storage volumesets, which only `stateful` (or `vm`) workloads can mount, one workload at a time.

**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 how each workload should be reachable:

| 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)                      |
| Any Service selecting the pod (ClusterIP, NodePort, LoadBalancer, headless) | Internal inbound allowed from the same GVC (`same-gvc`) |
| No Service selecting the pod                                                | Internal inbound blocked (`none`, default)              |

On Kubernetes, any pod can reach a Service inside the cluster. The converter preserves that reachability: a workload backed by a Service converts with `firewallConfig.internal.inboundAllowType: same-gvc`, so dependent workloads (databases, message brokers, internal APIs) keep talking to each other after migration. Use the `cpln.io/internal-inbound-allow-type` [annotation](#conversion-annotations) to override the inference per workload — for example, `none` to lock a workload down even though a Service selects it.

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

<Warning>
  When a workload's internal firewall is `none`, calls from other workloads in the GVC fail in ways that look like application errors on the caller side (e.g., `connection reset by peer`); the target workload's access log shows `rbac_access_denied_matched_policy[none]`. If intra-GVC calls fail after a conversion done with an older CLI, set `same-gvc` on the target: `cpln workload update <name> --set spec.firewallConfig.internal.inboundAllowType=same-gvc`.
</Warning>

### 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, internal firewall 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
          internal:
            inboundAllowType: same-gvc
            inboundAllowWorkload: []
      ---
      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="Workload type may not be changed (405)">
    The workload type is immutable. This error means the type inferred by the current conversion differs from the type of the already-existing workload — commonly after upgrading across CLI v3.9.0, which changed the inferred default from a serverless/standard mix to `standard`.

    Pin the type in the K8s manifest so it stays stable across CLI versions:

    ```yaml theme={null}
    metadata:
      annotations:
        cpln.io/workload-type: serverless
    ```

    To intentionally change a workload's type, create a new workload with the target type and migrate traffic to it — the type cannot be changed in place.
  </Accordion>

  <Accordion title="Workloads cannot reach each other after conversion">
    Workloads not selected by any Kubernetes Service convert with the internal firewall closed (`inboundAllowType: none`). Callers see errors like `connection reset by peer` while the target's access log shows `rbac_access_denied_matched_policy[none]`.

    Include the Services in the conversion input so the converter infers `same-gvc`, set the `cpln.io/internal-inbound-allow-type` annotation, or open the firewall after the fact with `cpln workload update <name> --set spec.firewallConfig.internal.inboundAllowType=same-gvc`.
  </Accordion>

  <Accordion title="envFrom variables are missing">
    `envFrom` sources expand only when the referenced ConfigMap or Secret is part of the conversion input, because the converter needs the keys. Check the conversion warnings on stderr: a Secret that lives only on Control Plane cannot be expanded — include its manifest in the input or add the variables to `env` manually.
  </Accordion>

  <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>
