Skip to main content

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 by create_attempt_id from the create response (Kamiwaza 1.3.0 and later): a CreateFailed event 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 phase Failed and is otherwise reported in the ExtensionWaitTimeout message. reason is Core's explanation: the CreateFailed status event (create_status_code is the HTTP status the synchronous create would have returned), the rolled-back 404 detail, or the warning events of a Failed phase. 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 with wait=True to get the error directly.
  • ExtensionWaitTimeout — not ready within timeout; carries phase and pending_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.

FieldTypeDescription
namestrExtension CR name
typestr"app" or "tool"
versionstrExtension version
phasestr | NonePhase: Pending, Provisioning, Running, Failed, Unknown, Terminating
servicesList[ExtensionServiceStatus]Per-service status
endpointsExtensionEndpoints | NoneResolved external/internal URLs
owner_user_idstr | NoneUser who created the extension
created_atdatetime | NoneCreation timestamp
readybool | NoneFalse on a 202 "created, not ready" create response; True from wait_for_extension(); otherwise unset
pending_reasonstr | NoneOn a 202 create response, what the deploy is waiting for
create_attempt_idstr | NoneKamiwaza 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.

FieldTypeRequiredDescription
namestrYesExtension name (K8s DNS label format)
type"app" | "tool"YesExtension type
versionstrYesSemver version string
servicesList[ExtensionServiceSpec]YesOne or more service specs
kamiwazaKamiwazaIntegrationSpecNoPlatform integration settings
networkingNetworkingSpecNoIngress configuration
securitySecuritySpecNoSecurity classification

ExtensionServiceSpec​

Specification for a single service within an extension.

FieldTypeDefaultDescription
namestr—Service name
imagestr—Container image
primaryboolFalseWhether this is the primary ingress service
portsList[ExtensionPort][]Exposed ports; a one-shot Job must have none
depends_on (dependsOn)List[str] | NoneNoneService names to reconcile first
depends_on_conditions (dependsOnConditions)Dict[str, str] | NoneNoneCompose conditions for those dependencies
run_to_completion (runToCompletion)bool | NoneNoneExplicit one-shot Job intent; None preserves legacy inference
envList[Dict]NoneEnvironment variables
replicasint1Number of replicas
resourcesResourceSpecNoneCPU/memory requests and limits
commandList[str]NoneContainer 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