بنقرة واحدة
custom-charm
End-to-end workflow for building ops-framework charms for custom applications (K8s and machine)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
End-to-end workflow for building ops-framework charms for custom applications (K8s and machine)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Known-good Juju bundle shapes (COS Lite, 12-Factor + COS, Identity Platform, Charmed Kubeflow) — canonical app lists and relation edges
Implementing Juju actions for operational tasks in charms. WHEN: add Juju actions to a charm, implement backup / rotate-credentials / restore actions, declare actions in charmcraft.yaml, write action handlers, test actions with Scenario, run actions with juju run.
Adding and validating charm configuration options. WHEN: add charm configuration options, declare config in charmcraft.yaml, validate config values in config-changed, apply config to a Pebble layer, apply config to a machine charm, write Scenario tests for config.
End-to-end workflow for building ops-framework charms for custom applications (K8s and machine). WHEN: build a custom Juju charm, write src/charm.py with Pebble, write a machine charm with systemd, decide K8s vs machine substrate, scaffold a non-paas-charm with charmcraft init, customise the ops framework charm lifecycle.
Workflow for charming infrastructure software (databases, caches, message brokers, proxies, monitoring). WHEN: charm infrastructure software, charm a database / cache / message broker / proxy / monitoring system, implement peer relations, leader election, primary/replica failover, backup and restore actions, clustering.
Expert guidance for developing, building, testing, and publishing Juju charms using charmcraft. WHEN: edit charmcraft.yaml, run charmcraft pack, run charmcraft init, manage charm libraries, publish a charm to Charmhub, set up multi-base builds, configure relations / config options, set up storage or containers.
| name | custom-charm |
| description | End-to-end workflow for building ops-framework charms for custom applications (K8s and machine) |
This skill covers building a Juju charm for an application that does not fit a recognised 12-factor PaaS framework (Flask, Django, FastAPI, Go, Express, Spring Boot). These charms use the full ops framework — you write src/charm.py yourself, managing the workload lifecycle directly via Pebble (K8s) or systemd/apt (machine).
Use this path (Path B) when:
Do not use this path for:
twelve-factor skill insteadChoose the substrate before scaffolding. Ask the user to confirm.
| Signal | Substrate | Reasoning |
|---|---|---|
| Dockerfile or OCI image exists | K8s | Already containerised; natural fit for Pebble |
| Cloud-native / stateless workload | K8s | Designed for orchestration |
| Bare metal / GPU / kernel modules | Machine | Needs direct hardware access |
| systemd service files present | Machine | Already designed for systemd |
| Heavy OS-level integration (PAM, cron, udev) | Machine | Relies on host OS services |
| No strong signal | K8s (default) | Easier to distribute and test |
Run analyse_framework on the source directory. When no PaaS framework is detected, the tool returns workload_hints indicating whether a Dockerfile, docker-compose file, or systemd service files are present, along with a suggested_substrate.
Present the substrate recommendation to the user with reasoning. Wait for confirmation before proceeding — this choice affects everything downstream.
Use charmcraft_init with the appropriate profile:
profile: "kubernetes"profile: "machine"charmcraft init --profile=kubernetes --name=my-app
This creates the base structure: charmcraft.yaml, src/charm.py, tox.ini, and test directories.
charmcraft.yamlAdd containers, resources, and storage:
name: my-app
type: charm
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
containers:
my-app:
resource: oci-image
resources:
oci-image:
type: oci-image
description: OCI image for the application
storage:
data:
type: filesystem
minimum-size: 1G
location: /data
requires:
tracing:
interface: tracing
limit: 1
Machine charms typically need no containers or OCI resources:
name: my-app
type: charm
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
storage:
data:
type: filesystem
minimum-size: 1G
location: /data
requires:
tracing:
interface: tracing
limit: 1
src/charm.py#!/usr/bin/env python3
"""Charm for my-app on Kubernetes."""
import ops
import ops_tracing
class MyAppCharm(ops.CharmBase):
"""Charm the application using Pebble."""
def __init__(self, framework: ops.Framework):
super().__init__(framework)
self._tracing = ops_tracing.Tracing(self, "tracing")
self._container = self.unit.get_container("my-app")
framework.observe(self.on["my-app"].pebble_ready, self._on_pebble_ready)
framework.observe(self.on.config_changed, self._on_config_changed)
def _on_pebble_ready(self, event: ops.PebbleReadyEvent) -> None:
"""Start the workload when the container is ready."""
self._container.add_layer("my-app", self._pebble_layer(), combine=True)
self._container.autostart()
self.unit.status = ops.ActiveStatus()
def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None:
"""Update the workload config and restart if running."""
if not self._container.can_connect():
event.defer()
return
self._container.add_layer("my-app", self._pebble_layer(), combine=True)
self._container.replan()
self.unit.status = ops.ActiveStatus()
def _pebble_layer(self) -> ops.pebble.LayerDict:
"""Return the Pebble layer configuration."""
return {
"summary": "my-app layer",
"description": "Pebble layer for my-app",
"services": {
"my-app": {
"override": "replace",
"summary": "my-app service",
"command": "/usr/bin/my-app",
"startup": "enabled",
"environment": {},
},
},
}
if __name__ == "__main__":
ops.main(MyAppCharm)
Key patterns for K8s charms:
self.unit.get_container("container-name")pebble-ready — this fires when the container's Pebble agent becomes reachableadd_layer() + autostart() on first start; add_layer() + replan() on changesself._container.can_connect() before interacting with Pebble; defer if not readyself._container.push() to write config files into the containerself._container.exec() to run commands inside the container#!/usr/bin/env python3
"""Charm for my-app on machines."""
import subprocess
import ops
import ops_tracing
class MyAppCharm(ops.CharmBase):
"""Charm the application using apt and systemd."""
def __init__(self, framework: ops.Framework):
super().__init__(framework)
self._tracing = ops_tracing.Tracing(self, "tracing")
framework.observe(self.on.install, self._on_install)
framework.observe(self.on.start, self._on_start)
framework.observe(self.on.config_changed, self._on_config_changed)
def _on_install(self, event: ops.InstallEvent) -> None:
"""Install packages and prepare the system."""
self.unit.status = ops.MaintenanceStatus("Installing dependencies")
subprocess.check_call(["apt-get", "update", "-y"])
subprocess.check_call(["apt-get", "install", "-y", "my-app"])
def _on_start(self, event: ops.StartEvent) -> None:
"""Start the workload service."""
subprocess.check_call(["systemctl", "enable", "--now", "my-app"])
self.unit.status = ops.ActiveStatus()
def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None:
"""Update configuration and restart the service."""
self._write_config()
subprocess.check_call(["systemctl", "restart", "my-app"])
self.unit.status = ops.ActiveStatus()
def _write_config(self) -> None:
"""Write the application config file from charm config."""
# Read charm config and write to the appropriate location.
# Example: /etc/my-app/config.yaml
pass
if __name__ == "__main__":
ops.main(MyAppCharm)
Key patterns for machine charms:
install event for package installation (apt-get, snap install)start event to enable and start the systemd service/etc/my-app/, etc.)subprocess.check_call() for system commandssystemctl restart after config changesSet status to communicate the charm's state to the operator:
# During long operations.
self.unit.status = ops.MaintenanceStatus("Installing packages")
# When waiting for a relation.
self.unit.status = ops.WaitingStatus("Waiting for database relation")
# When a required config option is missing.
self.unit.status = ops.BlockedStatus("Missing required config: database-url")
# When everything is working.
self.unit.status = ops.ActiveStatus()
Validate config early and set BlockedStatus on invalid input:
def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None:
port = self.config.get("port")
if not isinstance(port, int) or not (1 <= port <= 65535):
self.unit.status = ops.BlockedStatus("Invalid port number")
return
# Apply the valid config...
Read and write relation data in event handlers:
def _on_database_relation_changed(self, event: ops.RelationChangedEvent) -> None:
if not event.unit:
return
host = event.relation.data[event.unit].get("host")
port = event.relation.data[event.unit].get("port")
if not host or not port:
self.unit.status = ops.WaitingStatus("Waiting for database credentials")
return
# Configure the workload with the database connection...
For detailed relation data patterns, load the relation-data-design skill. For OAuth / OIDC / login flows backed by the Canonical Identity Platform, load the identity-platform skill — it covers the oauth, oauth-cli, oidc-info, and hydra-token-introspect relations and how the secret-backed client credentials flow through to the workload.
Access Juju-managed storage in event handlers:
def _on_storage_attached(self, event: ops.StorageAttachedEvent) -> None:
storage_path = event.storage.location
# Use storage_path for persistent data...
Use Juju secrets for sensitive data:
def _on_secret_changed(self, event: ops.SecretChangedEvent) -> None:
secret = event.secret
content = secret.get_content(refresh=True)
password = content.get("password")
# Apply the new secret...
Custom charms do not need rockcraft. The cycle is:
charmcraft pack
juju deploy ./my-app_amd64.charm
For K8s charms with an OCI image resource:
charmcraft pack
juju deploy ./my-app_amd64.charm --resource oci-image=docker.io/library/my-app:latest
Use the charmcraft_pack and juju_deploy tools. For K8s charms, pass the OCI image via resources={"oci-image": "..."}.
K8s charms need an OCI image for the container resource. There are two options:
Option A: Use an existing registry image
registry_search(query="<workload>")registry_image_info(image="<name>", tag="<version>")juju_deploy(..., resources={"oci-image": "docker.io/library/<name>:<tag>"})Option B: Build a rock
When no suitable image exists or the workload needs custom packaging:
rockcraft.yaml for the applicationrockcraft_pack to build the rockskopeo_registry_push to push to the local registryOption A is the common case for custom charms wrapping well-known software. Option B is needed when the application is custom code or requires modifications to the base image.
For Python source changes only, use the fast path:
src/charm.py (or files in src/ and lib/)charm_sync to push changes to the running unitjuju_dispatch to fire an event (e.g. config-changed, update-status)Use the full charmcraft_pack → juju_refresh cycle when:
charmcraft.yaml changed (new relations, config options, resources)requirements.txt)Unit tests: Load the scenario-tests skill. Cover:
install / start → ActiveStatuspebble-ready (K8s) → Pebble layer applied, service startedconfig-changed → config applied, service restartedIntegration tests: Load the jubilant-tests skill. Cover:
ActiveStatusRun charm_validate before declaring the charm complete. This runs unit tests and charmcraft pack, producing a pass/fail checklist.
charmcraft_pack → juju_refresh --path → juju_wait → verify
No rockcraft or registry push needed for custom charms (unlike 12-factor).
Pebble layer hygiene — add_layer() without combine=True, Pebble methods called without a can_connect() guard, or service entries missing override / command / startup are all checked deterministically by charmlint (PEB001 / PEB002 / PEB003). Run charmlint to surface them — the skill body no longer recites the rules.
Blocking the hook with long-running commands — hooks have a timeout (default 5 minutes). For long installations, consider breaking work across events or increasing the timeout.
Writing config directly in the install hook — config values may not be set yet during install. Use config-changed for applying configuration.
Using kubernetes profile for a PaaS framework — if the app is Flask/Django/etc., use the framework profile with the twelve-factor skill instead. The kubernetes profile gives a bare ops charm.
Missing OCI image resource for K8s charms — the containers section in charmcraft.yaml must reference a resource, and that resource must be declared in the resources section.
| Symptom | Likely Cause | Fix |
|---|---|---|
Unit stuck in waiting with "waiting for Pebble" | Container not started or image pull failed | Check juju debug-log; verify OCI image resource is correct |
pebble-ready never fires | Container name mismatch between charmcraft.yaml and charm code | Ensure the container name in self.on["name"] matches charmcraft.yaml |
ConnectionError from Pebble | Calling Pebble methods before can_connect() returns True | Add a can_connect() guard and defer the event |
Service crashes after replan() | Bad command or missing binary in the container | Check the Pebble layer command; use container.exec() to debug |
apt-get fails during install (machine) | Missing universe repo or network issue | Add add-apt-repository or check proxy settings |
systemctl fails (machine) | Service file not installed or unit not enabled | Verify the package installs a .service file; use systemctl status |
Charm goes to error on relation event | Accessing relation data that does not exist yet | Guard with if not event.unit: return and check for required keys |
| Config change has no effect | Forgot to restart the service after writing new config | Call replan() (K8s) or systemctl restart (machine) after applying config |