Skip to content

Writing a Capability

A capability is a YAML document describing what your service exposes (REST routes, MCP tools, skill groups, management port) and what it consumes (upstream HTTP APIs, secrets). The ikanos engine turns that spec into a running server — no code required.


Inline Spec

The simplest approach — the full ikanos spec lives directly inside the Capability CR under spec:

apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
  name: hello-world
  namespace: default
  labels:
    naftiko.io/tier: standard
spec:
  ikanos: "1.0.0-alpha3"
  info:
    display: Hello World
    description: "Simple REST capability"
    labels:
      naftiko.io/tier: standard
  capability:
    exposes:
      - type: rest
        port: 3001
        namespace: my-api
        resources:                        # alpha3 — keyed map
          hello:
            path: /hello
            operations:                   # alpha3 — keyed map
              get-hello:
                method: GET
                outputParameters:
                  - name: message
                    type: string
                    value: "Hello, World!"

When to use: quick tests and prototypes. Not recommended for production because updating the spec requires patching the CR directly.


The ikanos spec lives in a ConfigMap. The Capability CR references it via specRef. This decouples the spec lifecycle from the CR lifecycle — you can update the spec without touching the CR.

apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
  name: hello-world
  namespace: default
  labels:
    naftiko.io/tier: standard
spec:
  specRef:
    configMap: hello-world-spec     # ConfigMap in the same namespace
    key: capability.yaml            # default key — can be omitted

Create the ConfigMap from your spec file:

kubectl create configmap hello-world-spec \
  --from-file=capability.yaml=hello-world.yaml \
  -n default

Update the spec without touching the CR:

kubectl create configmap hello-world-spec \
  --from-file=capability.yaml=hello-world.yaml \
  -n default --dry-run=client -o yaml | kubectl apply -f -

# Force reconcile
kubectl annotate capability hello-world \
  reconcile-at=$(date +%s) --overwrite -n default

The operator reads the raw YAML verbatim — no re-serialization through the Java model. Every field you write, including MCP tools, aggregates, prompts, and custom attributes, is preserved exactly.


Expose Types

Type Purpose Port
rest HTTP REST API your choice
mcp MCP tool server (AI agents) your choice
skill Skill discovery endpoint your choice
control Observability — /metrics, /health/* typically 9090

Every expose entry gets its own named Service port. The control port also triggers a ServiceMonitor and Prometheus pod annotations.


Multi-Port Capabilities

A capability can expose multiple adapters on different ports simultaneously. Skipper creates one named Service port per expose entry.

capability:
  exposes:
    - type: mcp
      address: "0.0.0.0"
      port: 3001
      namespace: shipyard-tools
      authentication:
        type: bearer
        token: "{{MCP_SERVER_TOKEN}}"
      tools:
        - name: list-ships
          # ...

    - type: rest
      address: "0.0.0.0"
      port: 3002
      namespace: shipyard-api
      resources:
        - path: /ships
          # ...

    - type: skill
      address: "0.0.0.0"
      port: 3003
      namespace: shipyard-skills
      skills:
        - name: fleet-ops
          # ...

    - type: control
      address: "0.0.0.0"
      port: 9090
      observability:
        enabled: true
        metrics:
          local:
            enabled: true
        traces:
          sampling: 1.0
          propagation: w3c

The resulting Service:

NAME       PORTS
shipyard   mcp:3001, rest:3002, skill:3003, control:9090

Note: always set address: "0.0.0.0" on every expose when running in Kubernetes. Without it, the engine binds to localhost and the port is only reachable from inside the container.


Selecting a CapabilityClass

The naftiko.io/tier label drives resource allocation:

metadata:
  labels:
    naftiko.io/tier: standard   # standard | premium | dev
Tier CPU req/limit Memory req/limit HPA min/max
standard 250m / 500m 256Mi / 512Mi 1 / 4
premium 500m / 1000m 512Mi / 1Gi 2 / 20
dev 50m / 100m 64Mi / 128Mi 1 / 2

If no label is set or no matching class is found, standard defaults apply.


Tier Metadata Labels

Beyond tier, you can attach additional metadata labels for cost attribution and discoverability:

metadata:
  labels:
    naftiko.io/tier: standard
    naftiko.io/domain: platform
    naftiko.io/cost-center: team-x

These labels propagate to the generated Deployment, Service, and pods.


Ingress (Public Exposure)

To expose a capability outside the cluster, add the public tag:

exposes:
  - type: rest
    port: 3001
    namespace: my-api
    tags: [public]        # triggers Ingress creation
    resources:
      # ...

Skipper creates an Ingress targeting the tagged expose port. Only business ports can be tagged public — the control port is never exposed via Ingress.


Full Spec Reference

apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
  name: my-capability
  namespace: default
  labels:
    naftiko.io/tier: standard       # required — selects CapabilityClass
    naftiko.io/domain: platform     # optional metadata
    naftiko.io/cost-center: team-x  # optional metadata
spec:
  # ── Option A: specRef (recommended) ──────────────────────────────────────
  specRef:
    configMap: my-capability-spec   # ConfigMap in the same namespace
    key: capability.yaml            # default — can be omitted

  # ── Option B: inline spec ─────────────────────────────────────────────────
  ikanos: "1.0.0-alpha3"
  info:
    display: "My Capability"
    description: "What this capability does"
    tags: [rest, internal]
    labels:
      naftiko.io/tier: standard
      naftiko.io/domain: platform
  binds:
    - namespace: "my-secrets"
      location: "file:///./shared/secrets.yaml"
      keys:
        API_TOKEN: "my-api-token"
  capability:
    consumes:
      - namespace: upstream
        type: http
        baseUri: "https://api.example.com"
        resources: [ ... ]
    exposes:
      - type: rest
        address: "0.0.0.0"
        port: 3001
        namespace: my-api
        tags: [public]
        resources: [ ... ]
      - type: control
        address: "0.0.0.0"
        port: 9090
        observability:
          enabled: true
          metrics:
            local:
              enabled: true
          traces:
            sampling: 1.0
            propagation: w3c

status:                             # written by the operator — read-only
  phase: Running | Failed
  endpoint: http://my-capability.default.svc.cluster.local:3001
  conditions:
    - type: Ready
      status: "True"

Reconciliation Triggers

The operator reconciles a Capability when:

  • The CR is created or updated
  • A child resource (ConfigMap, Deployment, Service) is modified or deleted
  • The CR is annotated with reconcile-at:
kubectl annotate capability my-capability \
  reconcile-at=$(date +%s) --overwrite -n default

Checking Status

kubectl get capability my-capability -n default
# NAME             PHASE     ENDPOINT
# my-capability    Running   http://my-capability.default.svc.cluster.local:3001

kubectl get capability my-capability \
  -o jsonpath='{.status.phase}'

kubectl describe capability my-capability