Extensions Service
The Extensions Service (client.extensions) manages K8s-native extensions deployed as KamiwazaExtension Custom Resources. It replaces the legacy App Garden (client.apps) and Tool Shed (client.tools) services for Kubernetes deployments.
Quick Start
from kamiwaza_sdk import KamiwazaClient
client = KamiwazaClient(base_url="https://kamiwaza.test/api")
# ... authenticate ...
# List all extensions
extensions = client.extensions.list_extensions()
for ext in extensions:
print(f"{ext.name}: {ext.phase} ({ext.type})")
# Get a specific extension
detail = client.extensions.get_extension("my-extension")
print(detail.endpoints.external)
# Delete an extension
client.extensions.delete_extension("my-extension")
Methods
list_extensions() -> List[Extension]
List all extensions visible to the current user.
extensions = client.extensions.list_extensions()
for ext in extensions:
print(f"{ext.name} ({ext.type}): {ext.phase}")
get_extension(name: str) -> Extension
Get a single extension by CR name.
ext = client.extensions.get_extension("kaizen-abc12345")
print(ext.version) # "2.0.0"
print(ext.phase) # "Running"
print(ext.endpoints) # ExtensionEndpoints(external="https://...", internal="http://...")
Raises: NotFoundError if the extension does not exist.
create_extension(request: CreateExtension, *, wait: bool = False) -> Extension
Create a new extension from a specification.
Core answers 201 once the deploy is applied, or 202 ("created, not
ready") when a direct-runtime deploy must wait for a one-shot Job or a
service_healthy dependency. Both return an Extension; a 202 one has
phase="Provisioning", ready=False, is_pending=True, and pending_reason
(for example "waiting for one-shot Job my-tool-migrate to complete").
Follow it with wait_for_extension():
ext = client.extensions.create_extension(request) # may be Provisioning (202)
ext = client.extensions.wait_for_extension(ext, timeout=600) # returns when Running
wait=True sends ?wait=true so Core blocks and returns the final outcome
(including its 503/409 errors) in the create response itself. Cores that
predate the 202 answer always block; their result passes through
wait_for_extension() unchanged.
If Core is already running its maximum number of creates it answers 503 with
Retry-After before starting the create; the SDK retries that (at most 4
attempts and about 70s of jittered backoff; 3 attempts with Core's 30s hint)
and then raises
ExtensionCreateBusyError (an APIError).
TLS verification is configured in each service's environment through
KAMIWAZA_VERIFY_SSL and KAMIWAZA_TLS_REJECT_UNAUTHORIZED. The extension CLI
sets both on every service from the selected connection and its TLS override,
replacing conflicting Compose values in the generated payload. A full redeploy
updates every service. With kz-ext dev --service X, only the selected service
is updated; run a full redeploy when changing TLS policy for the whole extension.
These are Kamiwaza-specific settings; they do not configure arbitrary third-party TLS
clients.
For services using the shared Python runtime with a private CA, mount a readable
PEM bundle in the container and set KAMIWAZA_CA_BUNDLE to its in-container path.
Those clients intentionally ignore SSL_CERT_FILE and SSL_CERT_DIR; trusting
the CA on the workstation does not configure container trust. Keep verification
enabled in production. For an intentional development-only opt-out, set
KAMIWAZA_VERIFY_SSL=false in the shell running the deployment; this also
disables verification for the CLI's own API calls.
The legacy KamiwazaIntegrationSpec.tls_reject_unauthorized attribute remains
readable locally but is excluded from serialized requests: the current platform rejects
that retired integration field with HTTP 422.
Compose URLs referencing the service itself are scoped to its deployed Service
name, just like references from another service. Kubernetes does not provide
the unprefixed Compose alias, including for self-callbacks. Scoped Service
callbacks can reach any ready replica. Use localhost or 127.0.0.1 when a
call must stay in-pod, including startup or readiness calls before the pod is ready.
from kamiwaza_sdk.schemas.extensions import CreateExtension, ExtensionServiceSpec
request = CreateExtension(
name="my-tool",
type="tool",
version="1.0.0",
services=[
ExtensionServiceSpec(
name="backend",
image="myregistry/backend:1.0.0",
primary=True,
ports=[{"container_port": 8000}],
),
],
)
ext = client.extensions.create_extension(request)
print(ext.name) # "my-tool"
print(ext.phase) # "Pending"
wait_for_extension(extension, *, timeout=600, poll_interval=2, max_poll_interval=5, on_progress=None) -> Extension
Block until an extension is ready. extension is the Extension returned by
create_extension() or a name. A non-pending Extension is returned at once;
otherwise GET /extensions/{name}/status is polled, backing off from
poll_interval to max_poll_interval seconds, until the phase is Running
and every service reports ready_replicas >= replicas (a completed one-shot
Job counts as ready).
The result is GET /extensions/{name} with ready=True (a non-pending input
is returned unchanged, so a 201 result keeps ready=None; to follow an
extension fetched with get_extension(), pass its name). on_progress(line)
is called whenever the observed state changes.
Raises:
ExtensionCreateFailed— the create failed after Core accepted it. Failures are matched bycreate_attempt_idfrom the create response (Kamiwaza 1.3.0 and later): aCreateFailedevent or rolled-back 404 with the same id fails the wait at once, and another attempt's record is ignored as stale. Without an id (a name, or an older Core) the record cannot be attributed — Core keeps it per API process, and a replica can still hold a previous same-name attempt's failure — so the event is terminal only with phaseFailedand is otherwise reported in theExtensionWaitTimeoutmessage.reasonis Core's explanation: theCreateFailedstatus event (create_status_codeis the HTTP status the synchronous create would have returned), the rolled-back 404 detail, or the warning events of aFailedphase. If the extension disappears (an untagged 404 that persists for 60 seconds, which rides out Core's post-202 read-back race; a 404 tagged with another attempt's id is stale and never ends the wait) and Core does not say why (the failure record is per API process and kept 15 minutes), the error says so; retry withwait=Trueto get the error directly.ExtensionWaitTimeout— not ready withintimeout; carriesphaseandpending_reason. The deploy is not cancelled.- Other
APIErrors from polling (for example 403). Up to three consecutive 500/502/503/504 or transport errors are tolerated.
from kamiwaza_sdk.exceptions import ExtensionCreateFailed, ExtensionWaitTimeout
try:
ext = client.extensions.wait_for_extension(ext, on_progress=print)
except ExtensionCreateFailed as exc:
print(f"create failed: {exc.reason}")
except ExtensionWaitTimeout as exc:
print(f"still {exc.phase}: {exc.pending_reason}")
delete_extension(name: str) -> bool
Delete an extension by CR name. Returns True on success.
client.extensions.delete_extension("my-tool")
Raises: NotFoundError if the extension does not exist.
Note: K8s CR deletion is asynchronous. The CR may still appear briefly in list_extensions() after deletion.
Schemas
Extension
Response model for an extension.
| Field | Type | Description |
|---|---|---|
name | str | Extension CR name |
type | str | "app" or "tool" |
version | str | Extension version |
phase | str | None | Phase: Pending, Provisioning, Running, Failed, Unknown, Terminating |
services | List[ExtensionServiceStatus] | Per-service status |
endpoints | ExtensionEndpoints | None | Resolved external/internal URLs |
owner_user_id | str | None | User who created the extension |
created_at | datetime | None | Creation timestamp |
ready | bool | None | False on a 202 "created, not ready" create response; True from wait_for_extension(); otherwise unset |
pending_reason | str | None | On a 202 create response, what the deploy is waiting for |
create_attempt_id | str | None | Kamiwaza 1.3.0 and later: the create attempt behind a 201/202 response, used to match its failure |
CreateExtension
Request model for creating an extension.
| Field | Type | Required | Description |
|---|---|---|---|
name | str | Yes | Extension name (K8s DNS label format) |
type | "app" | "tool" | Yes | Extension type |
version | str | Yes | Semver version string |
services | List[ExtensionServiceSpec] | Yes | One or more service specs |
kamiwaza | KamiwazaIntegrationSpec | No | Platform integration settings |
networking | NetworkingSpec | No | Ingress configuration |
security | SecuritySpec | No | Security classification |
ExtensionServiceSpec
Specification for a single service within an extension.
| Field | Type | Default | Description |
|---|---|---|---|
name | str | — | Service name |
image | str | — | Container image |
primary | bool | False | Whether this is the primary ingress service |
ports | List[ExtensionPort] | [] | Exposed ports; a one-shot Job must have none |
depends_on (dependsOn) | List[str] | None | None | Service names to reconcile first |
depends_on_conditions (dependsOnConditions) | Dict[str, str] | None | None | Compose conditions for those dependencies |
run_to_completion (runToCompletion) | bool | None | None | Explicit one-shot Job intent; None preserves legacy inference |
env | List[Dict] | None | Environment variables |
replicas | int | 1 | Number of replicas |
resources | ResourceSpec | None | CPU/memory requests and limits |
command | List[str] | None | Container command override |
The SDK sends the snake_case field names, which Core publishes and accepts.
The camelCase names in parentheses are accepted as input only: older Core
builds published them, and the SDK still reads them, but it never sends them. A
service_completed_successfully dependency fences its consumer until the
dependency Job succeeds. On the direct runtime, a create also waits
for a service_healthy dependency to become healthy before applying its
consumer (Core may answer that create 202 while it waits; see
wait_for_extension). service_started only specifies reconciliation order;
services that require a started dependency to be ready must poll or retry.
run_to_completion: true makes a portless, non-primary service with one replica
and no runtime dependents a Job. CREATE omits absent dependency and one-shot
fields. PATCH can send empty dependency lists/maps to clear old edges; an
absent one-shot field preserves the prior value, while an explicit false clears
it. Core and the extension operator must support these fields before clients
send them.
Migration from App Garden / Tool Shed
| Legacy (App Garden / Tool Shed) | Extensions API |
|---|---|
client.apps.list_deployments() | client.extensions.list_extensions() |
client.apps.deploy(template_id=..., name=...) | client.extensions.create_extension(request) |
client.apps.get_deployment(deployment_id) | client.extensions.get_extension(name) |
client.apps.stop_deployment(deployment_id) | client.extensions.delete_extension(name) |
client.tools.list_deployments() | client.extensions.list_extensions() |
client.tools.deploy(template_id=..., name=...) | client.extensions.create_extension(request) |
Key differences:
- Extensions are identified by name (not UUID)
- Extensions use a declarative spec (
CreateExtension) instead of template IDs - The extensions API talks to K8s CRDs, not Docker Compose