diff --git a/.editorconfig b/.editorconfig index dabc6ed8d..462f92677 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,10 +27,13 @@ tab_width = 2 indent_size = 2 max_line_length = off -[{*.markdown,*.md,*.rst}] +[{*.markdown,*.md}] max_line_length = off ij_visual_guides = none +[{*.rst}] +max_line_length = 88 + [{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] max_line_length = off diff --git a/common.mk b/common.mk index 4f5c9ef49..e13e3c3a6 100644 --- a/common.mk +++ b/common.mk @@ -194,7 +194,7 @@ docs: ## Build documentation .PHONY: docs-auto docs-auto: ## Build and host docs with sphinx-autobuild - uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --ignore *.kate-swp --ignore docs/reference/commands/** --watch $(PROJECT) $(DOCS) $(DOCS)/_build + uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --ignore *.kate-swp --ignore docs/reference/commands/ --watch $(PROJECT) $(DOCS) $(DOCS)/_build .PHONY: pack-pip pack-pip: ##- Build packages for pip (sdist, wheel) diff --git a/docs/conf.py b/docs/conf.py index 3349ec8e0..36d8f06e0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -129,6 +129,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "craft-parts": ("https://canonical-craft-parts.readthedocs-hosted.com/en/latest/", None), + "ops": ("https://ops.readthedocs.io/en/latest/", None), "rockcraft": ("https://documentation.ubuntu.com/rockcraft/en/stable/", None), } # See also: diff --git a/docs/explanation/#index.rst# b/docs/explanation/#index.rst# new file mode 100644 index 000000000..d590cad37 --- /dev/null +++ b/docs/explanation/#index.rst# @@ -0,0 +1,18 @@ +.. _explanation: + +Explanation +*********** + +As Charmcraft is a part of the `Juju Charm SDK `_, most +explanation is hosted on the `Charm SDK docs `_. + +.. toctree:: + :maxdepth: 1 + + /common/craft-parts/explanation/dump_plugin + /common/craft-parts/explanation/filesets + lifecycle + + +`Charm SDK docs `_ +======================================================== diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst index d590cad37..66f1ee64f 100644 --- a/docs/explanation/index.rst +++ b/docs/explanation/index.rst @@ -3,16 +3,4 @@ Explanation *********** -As Charmcraft is a part of the `Juju Charm SDK `_, most -explanation is hosted on the `Charm SDK docs `_. - -.. toctree:: - :maxdepth: 1 - - /common/craft-parts/explanation/dump_plugin - /common/craft-parts/explanation/filesets - lifecycle - - -`Charm SDK docs `_ -======================================================== +TBA diff --git a/docs/howto/index.rst b/docs/howto/index.rst index 9bf000898..c77ebd23b 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -1,11 +1,23 @@ -.. _howto: +.. _how-to-guides: -How-To -****** +How-to guides +============= .. toctree:: :maxdepth: 2 - charm-to-poetry - charm-to-python - shared-cache + Manage the charmcraft CLI + Manage charms + Manage charms (12-factor apps) + Manage extensions + Manage resources + Manage libraries + Manage parts + Manage the current Charmhub user + Manage names + Manage revisions + Manage channels + Manage tracks + Manage icons + Misc + Manage bundles diff --git a/docs/howto/manage-12-factor-app-charms.md b/docs/howto/manage-12-factor-app-charms.md new file mode 100644 index 000000000..5bd612b57 --- /dev/null +++ b/docs/howto/manage-12-factor-app-charms.md @@ -0,0 +1,259 @@ +(manage-12-factor-app-charms)= +# How to manage 12-factor app charms + + + + +> See also: [`juju` | 12-factor-app charms](https://juju.is/docs/juju/charmed-operator) + +## Prepare an OCI image for a 12-factor app charm + +> See more: [`rockcraft` | Build a 12-factor app rock](https://documentation.ubuntu.com/rockcraft/en/latest/how-to/build-a-12-factor-app-rock/#include-extra-files-in-the-oci-image) + + +## Initialise a 12-factor app charm + +Use `charmcraft init` and specify the relevant profile: + +```text +charmcraft init --profile +``` + +Charmcraft automatically creates `charmcraft.yaml`, `requirements.txt` and source code for the charm in your current directory. You will need to check `charmcraft.yaml` and `README.md` and verify that the charm’s name and description are correct. + +> See also: {ref}`ref_commands_init` + +````{dropdown} Example: Flask application + +Specify the `flask-framework` profile: + +```text +charmcraft init --profile flask-framework +``` + +```` + +````{dropdown} Example: Django application + +Specify the `django-framework` profile: + +```text +charmcraft init --profile django-framework +``` + +```` + +````{dropdown} Example: FastAPI application + + +Specify the `fastapi-framework` profile: + +```text +charmcraft init --profile fastapi-framework +``` + +```` + +````{dropdown} Example: Go application + +Specify the `go-framework` profile: + +```text +charmcraft init --profile go-framework +``` + +```` + +## Manage configurations for a 12-factor app charm +A charm configuration can be added if your 12-factor app requires environment variables, for +example, to pass a token for a service. Add the configuration in `charmcraft.yaml`: + +```text +config: + options: + token: + description: The token for the service. + type: string +``` + +```{dropdown} Flask application + +A user-defined configuration option will correspond to an environment variable generated by the `paas-charm` project to expose the configuration to the Flask workload. In general, a configuration option `config-option-name` will be mapped as `FLASK_CONFIG_OPTION_NAME` where the option name will be converted to upper case, dashes will be converted to underscores and the `FLASK_` prefix will be added. In the example above, the `token` configuration will be mapped as the `FLASK_TOKEN` environment variable. In addition to the environment variable, the configuration is also available in the Flask variable `app.config` without the `FLASK_` prefix. + +The configuration can be set on the deployed charm using `juju config token=`. + +> See also: [How to add a configuration to a charm `, [Configuration Handling – Flask Documentation^](https://flask.palletsprojects.com/en/3.0.x/config/) + +``` + +```{dropdown} Django application +A user-defined configuration option will correspond to an environment variable generated by the `paas-charm` project to expose the configuration to the Django workload. In general, a configuration option `config-option-name` will be mapped as `DJANGO_CONFIG_OPTION_NAME` where the option name will be converted to upper case, dashes will be converted to underscores and the `DJANGO_` prefix will be added. In the example above, the `token` configuration will be mapped as the `DJANGO_TOKEN` environment variable. + +The configuration can be set on the deployed charm using `juju config token=`. + +> See also: [How to add a configuration to a charm ` +``` + +```{dropdown} FastAPI application + +A user-defined configuration option will correspond to an environment variable generated by the `paas-charm` project to expose the configuration to the FastAPI workload. In general, a configuration option `config-option-name` will be mapped as `APP_CONFIG_OPTION_NAME` where the option name will be converted to upper case, dashes will be converted to underscores and the `APP_` prefix will be added. In the example above, the `token` configuration will be mapped as the `APP_TOKEN` environment variable. + +The configuration can be set on the deployed charm using `juju config token=`. + +> See also: [How to add a configuration to a charm ` +{ref}`/tab] + +``` + + + +```{dropdown} Go application +A user-defined configuration option will correspond to an environment variable generated by the `paas-charm` project to expose the configuration to the Go workload. In general, a configuration option `config-option-name` will be mapped as `APP_CONFIG_OPTION_NAME` where the option name will be converted to upper case, dashes will be converted to underscores and the `APP_` prefix will be added. In the example above, the `token` configuration will be mapped as the `APP_TOKEN` environment variable. + +The configuration can be set on the deployed charm using `juju config token=`. + +> See also: [How to add a configuration to a charm ` +``` + + +## Manage relations for a 12-factor app charm + +A charm integration can be added to your charmed 12-factor app by providing the integration and endpoint definition in `charmcraft.yaml`: + +```yaml +requires: + : + interface: + optional: false +``` + +Here, `` corresponds to the endpoint of the application with which you want the integration, and `` is the endpoint schema to which this relation conforms. Both the `` and `` must coincide with the structs defined in that particular application’s charm’s `charmcraft.yaml` file. The key `optional` with value `False` means that the charm will get blocked and stop the services if the integration is not provided. + +You can provide the integration to your deployed 12-factor app using `juju integrate <12-factor app charm> `. After the integration has been established, the connection string and other configuration options will be available as environment variables that you may use to configure your 12-factor application. + +For example, if you wish to integrate your 12-factor application with PostgreSQL ([machine](https://charmhub.io/postgresql) or [k8s](https://charmhub.io/postgresql-k8s) charm), add the following endpoint definition to `charmcraft.yaml`: + +```yaml +requires: + postgresql: + interface: postgresql_client + optional: True +``` + +Provide the integration to your deployed 12-factor app with `juju integrate <12-factor app charm> postgresql`. This integration creates the following environment variables you may use to configure your 12-factor application: + +* `POSTGRESQL_DB_CONNECT_STRING` +* `POSTGRESQL_DB_SCHEME` +* `POSTGRESQL_DB_NETLOC` +* `POSTGRESQL_DB_PATH` +* `POSTGRESQL_DB_PARAMS` +* `POSTGRESQL_DB_QUERY` +* `POSTGRESQL_DB_FRAGMENT` +* `POSTGRESQL_DB_USERNAME` +* `POSTGRESQL_DB_PASSWORD` +* `POSTGRESQL_DB_HOSTNAME` +* `POSTGRESQL_DB_PORT` + +> See also: {ref}`How to add an integration to a charm ` + + +## Manage secrets for a 12-factor app charm + +A user secret can be added to a charm and all the keys and values in the secret will be exposed +as environment variables. Add the secret configuration option in charmcraft.yaml: +```yaml +config: + options: + api-token: + type: secret + description: Secret needed to access some API secret information +``` + +Once the charm is deployed, you can add a Juju secret to the model: +``` +juju add-secret my-api-token value=1234 othervalue=5678 +``` +The output from the previous command will look something like: +``` +secret:cru00lvmp25c77qa0qrg +``` + +From the output of the previous command, you can get the Juju secret ID. +Grant the application access to view the value of the secret: + +``` +juju grant-secret my-api-token +``` + +Add the Juju secret ID to the application: + +``` +juju config api-token=secret:cru00lvmp25c77qa0qrg +``` + +```{dropdown} Flask application +The following environment variables are available for the application: + + - FLASK_API_TOKEN_VALUE: "1234" + - FLASK_API_TOKEN_OTHERVALUE: "5678" + +> See also: [How to manage secrets ` +``` + +```{dropdown} Django application +The following environment variables are available for the application: + - DJANGO_API_TOKEN_VALUE: "1234" + - DJANGO_API_TOKEN_OTHERVALUE: "5678" + +> See also: [How to manage secrets ` +``` + +```{dropdown} FastAPI application +The following environment variables are available for the application: + - APP_API_TOKEN_VALUE: "1234" + - APP_API_TOKEN_OTHERVALUE: "5678" + +> See also: [How to manage secrets ` +``` + +```{dropdown} Go application +The following environment variables are available for the application: + - APP_API_TOKEN_VALUE: "1234" + - APP_API_TOKEN_OTHERVALUE: "5678" + +> See also: [How to manage secrets ` +``` + +## Use 12-factor app charms + +### (If your charm is a Django charm) Create an admin user + +Use the `create-superuser` action to create a new Django admin account: + +``` +juju run create-superuser username= email= +``` + +You must provide the username and email address. + +### (If your workload depends on a database) Migrate the database + +If your app depends on a database, it is common to run a database migration +script before app startup which, for example, creates or modifies tables. This +can be done by including the `migrate.sh` script in the root of your project. It +will be executed with the same environment variables and context as the 12-factor +app. + +If the migration script fails, it will retry upon `update-status`. The migration script will run on every unit. The script is +assumed to be idempotent (i.e., can be run multiple times) and that it can be run +on multiple units simultaneously without issue. Handling multiple migration scripts that run concurrently can be achieved by, for example, locking any tables during the migration. + + diff --git a/docs/howto/manage-bundles.rst b/docs/howto/manage-bundles.rst new file mode 100644 index 000000000..fabc83b52 --- /dev/null +++ b/docs/howto/manage-bundles.rst @@ -0,0 +1,46 @@ +.. _manage-charm-bundles: + +How to manage charm bundles +=========================== + + See first: `Juju \| Bundle `__ + +.. important:: + Starting with 1 Jan 2025, bundles are being phased out. + + +Create a bundle +--------------- + +To create a bundle, create a ``.yaml`` file with your desired configuration. + + See more: :ref:`file-bundle-yaml` + +.. tip:: + If you don't want to start from scratch, export the contents of your model to a + ``.yaml`` file via ``juju export-bundle --filename .yaml`` or + download the ``.yaml`` of an existing bundle from Charmhub. + See more: `Juju \| How to compare and export the contents of a model to a bundle + `_. + + +Pack a bundle +------------- + +To pack a bundle, in the directory where you have your ``bundle.yaml`` +file (and possibly other files, e.g., a ``README.md`` file), create a +``charmcraft.yaml`` file suitable for a bundle (at the minimum: +``type: bundle``), then run ``charmcraft pack`` to pack the bundle. The +result is a ``.zip`` file. + + See more: :ref:`ref_commands_pack` + +Publish a bundle on Charmhub +---------------------------- + +The process is identical to that for a simple charm except that, at the +step where you register the name, for bundles the command is +``register-bundle``. + + See more: :ref:`publish-a-charm` diff --git a/docs/howto/manage-channels.rst b/docs/howto/manage-channels.rst new file mode 100644 index 000000000..8923a4606 --- /dev/null +++ b/docs/howto/manage-channels.rst @@ -0,0 +1,89 @@ +.. _manage-channels: + +How to manage channels +====================== + +Create a channel +---------------- + +When you register a name on Charmhub, that automatically creates 4 channels, all +with track ``latest`` but with different risk levels, namely, ``edge``, ``beta``, +``candidate``, ``stable``, respectively. + + See more: :ref:`register-a-name` + +.. raw:: html + + + +View the available channels +--------------------------- + +To view a charm’s channels on Charmhub, run ``charmcraft status`` +followed by the name of the charm. E.g., + +.. code:: text + + $ charmcraft status my-awesome-charm + Track Channel Version Revision + latest stable - - + candidate - - + beta 0.1 1 + edge ↑ ↑ + +(The above output shows 4 channels, all of which have the same track, +``latest``, but different risk levels, namely, ``edge``, ``beta``, +``candidate``, and ``stable``.) + + See more: :ref:`ref_commands_status` + +Customise a channel’s track +--------------------------- + +You can request a track guardrail and create a track. + + See more: :ref:`manage-tracks` + +Open a channel +-------------- + +A channel is opened implicitly when you release a revision to it. + +Close a channel +--------------- + +A channel is opened when you release a revision to that channel. Before +that, the channel is created but not opened. When you’re closing a +channel, e.g., latest/candidate, that means that any deployment requests +that go there will be forwarded to the next most stable risk, e.g., for +beta, latest/stable. If you close stable, you can no longer deploy or +update from that, unless you release again to that channel (because +releasing opens the channel). + +If you add a branch, closing that branch will forward people to the same +track and risk, without a branch. diff --git a/docs/howto/manage-charms.rst b/docs/howto/manage-charms.rst new file mode 100644 index 000000000..845a169e7 --- /dev/null +++ b/docs/howto/manage-charms.rst @@ -0,0 +1,633 @@ +.. _manage-charms: + +How to manage charms +==================== + + See first: `Juju \| Charm `_, + `Juju \| Manage charms `_ + +Initialise a charm +------------------ + +To initialise a charm project, create a directory for your charm, enter +it, then run ``charmcraft init`` with the ``--profile`` flag followed by +a suitable profile name (for machine charms: ``machine``; for Kubernetes +charms: ``kubernetes``, ``simple``, or ``flask-framework``); that will +create all the necessary files and even prepopulate them with useful +content. + +.. code:: text + + charmcraft init --profile + +.. dropdown:: Example session + + .. code:: text + + $ mkdir my-flask-app-k8s + $ cd my-flask-app-k8s/ + $ charmcraft init --profile flask-framework + Charmed operator package file and directory tree initialised + Now edit the following package files to provide fundamental charm metadata + and other information: + + charmcraft.yaml + src/charm.py + README.md + + $ ls -R + .: + charmcraft.yaml requirements.txt src + + ./src: + charm.py + +The command also allows you to not specify any profile (in that case you get the +``simple`` profile -- a Kubernetes profile with lots of scaffolding, suitable for +beginners) and has flags that you can use to specify a different directory to +operate in, a charm name different from the name of the root directory, etc. + + See more: :ref:`ref_commands_revisions`, :ref:`profile`, + :ref:`list-of-files-in-a-charmcraft-project` + + See more: :ref:`manage-extensions` + +Add charm project metadata, an icon, docs +----------------------------------------- + + +Specify that the project is a charm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify that the project is a charm (as supposed to a bundle), in your +``charmcraft.yaml`` file set the ``type`` key to ``charm``: + +.. code:: yaml + + type: charm + + +Specify a name +~~~~~~~~~~~~~~ + +To specify a pack-and-deploy name for your charm, in your charm's +``charmcraft.yaml`` file specify the ``name`` key. E.g., + +.. code:: yaml + + name: traefik-k8s + +.. + + See more: :ref:`file-charmcraft-yaml-name` + +Specify a title +~~~~~~~~~~~~~~~ + +To specify a title for your charm's page on Charmhub, in your charm's +``charmcraft.yaml`` file specify a value for the ``title`` key. E.g., + +.. code:: yaml + + title: | + Traefik Ingress Operator for Kubernetes + +.. + + See more: :ref:`file-charmcraft-yaml-title` + +Add a summary +~~~~~~~~~~~~~ + +To add a summary line for your charm, in your charm's ``charmcraft.yaml`` file +specify a value for the ``summary`` key. E.g., + +.. code:: yaml + + summary: | + A Juju charm to run a Traefik-powered ingress controller on Kubernetes. + +.. + + See more: :ref:`file-charmcraft-yaml-summary` + +Add a description +~~~~~~~~~~~~~~~~~ + +To add a longer description for your charm, in your charm's ``charmcraft.yaml`` +file specify a value for the ``description`` key. E.g., + +.. code:: yaml + + description: | + A Juju-operated Traefik operator that routes requests from the outside of a + Kubernetes cluster to Juju units and applications. + +.. + + + See more: :ref:`file-charmcraft-yaml-description` + +Add contact information +~~~~~~~~~~~~~~~~~~~~~~~ + +To add maintainer contact information for a charm, in your charm's ``charmcraft.yaml`` +file specify a value for the :ref:`links.contact ` +key. E.g., + +.. code:: yaml + + links: + contact: Please send your answer to Old Pink, care of the Funny Farm, Chalfont + +.. + + + See more: :ref:`file-charmcraft-yaml-contact` + +Add a link to source code +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To add a link to the source code for a charm, in your charm's ``charmcraft.yaml`` +file specify an item under the :ref:`links.source ` +key. E.g., + +.. code:: yaml + + links: + source: + - https://github.com/canonical/traefik-k8s-operator + +.. + + See more: :ref:`file-charmcraft-yaml-links` + +Add a link to the bug tracker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To add a link to the bug tracker for a charm, in your charm's ``charmcraft.yaml`` +file specify an item under the :ref:`links.issues ` +key. E.g., + +.. code:: yaml + + links: + issues: + - https://github.com/canonical/traefik-k8s-operator/issues + +.. + + See more: :ref:`file-charmcraft-yaml-links` + +Add a link to the website +~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your charm has a website outside of Charmhub, to add a link to this website, in +your charm's ``charmcraft.yaml`` file specify an item under the +:ref:`links.website ` key. E.g., + +.. code:: yaml + + links: + website: + - https://charmed-kubeflow.io/ + +.. + + See more: :ref:`file-charmcraft-yaml-links` + +Add docs and a link to the docs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you publish your charm on Charmhub, reference documentation about the charm's +resources, actions, configurations, relations, and libraries is extracted +automatically. However, you should also aim to add further docs, e.g., a tutorial, +how-to guides, etc. To provide a link to these docs, in your charm's +``charmcraft.yaml`` file specify a value for the +:ref:`links.documentation ` key. +Note that at present this must be a Discourse page. E.g., + +.. code:: yaml + + links: + documentation: https://discourse.charmhub.io/t/traefik-k8s-docs-index/10778 + + +.. + + See more: :ref:`file-charmcraft-yaml-links` + +Add terms of use +~~~~~~~~~~~~~~~~ + + To add terms of use for your charm, in your charm's ``charmcraft.yaml`` + file specify a value for the ``terms`` key. E.g., + +.. code:: yaml + + terms: + - Butterscotch is regal + - Cara is adorable + +.. + + + See more: :ref:`file-charmcraft-yaml-terms` + + +Add an icon +~~~~~~~~~~~ + + See :ref:`manage-icons`. + + +Add runtime details to a charm +------------------------------ + +Require a specific Juju version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To require a specific Juju version for your charm, in your charm's +``charmcraft.yaml`` specify the ``assumes`` key. E.g., + +.. code:: yaml + + assumes: + - juju >= 3.5 + +.. + + + See more: :ref:`file-charmcraft-yaml-assumes` + +Require a Kubernetes cloud +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To require a Kubernetes cloud for your charm, in your charm's +``charmcraft.yaml`` file specify the ``assumes`` key. E.g., + +.. code:: yaml + + assumes: + - k8s-api + +.. + + + See more: :ref:`file-charmcraft-yaml-assumes` + +Require a specific base and platforms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To require a specific base and platforms for your charm, in your charm's +``charmcraft.yaml`` file specify the ``base``, and ``platforms`` keys. E.g., + +.. note:: + In Charmcraft < 3.0 this was done via a single key: ``bases``. + +.. code:: yaml + + # The run time base, the base format is @, + # accepted bases are: + # - ubuntu@24.04 + base: + # The build time base, if not defined the base is also the build time + # base, in addition to valid bases, the build-base can be "devel" + # which would use the latest in development Ubuntu Series. + build-base: + + platforms: + # The supported platforms, may omit build-for if platform-name + # is a valid arch, valid architectures follow the Debian architecture names, + # accepted architectures are: + # - amd64 + # - arm64 + # - armhf + # - ppc64el + # - riscv64 + # - s390x + : + # The build time architecture + build-on: | + # The run time architecture + build-for: | + +.. + + See more: :ref:`file-charmcraft-yaml-base`, :ref:`file-charmcraft-yaml-build-base` + +Specify container requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify container requirements, in your charm's ``charmcraft.yaml`` file +specify the ``containers`` key. + + + See more: :ref:`file-charmcraft-yaml-containers` + + +Specify associated resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify the resources associated with the charm, in your charm's +``charmcraft.yaml`` file specify the ``resources`` key. + + See :ref:`manage-resources`. + +Specify device requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify device requirements, in your charm's ``charmcraft.yaml`` file specify +the ``devices`` key. + + See more: :ref:`file-charmcraft-yaml-devices` + +Specify storage requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify storage requirements, in your charm's ``charmcraft.yaml`` file specify +the ``storage`` key. + + See more: :ref:`file-charmcraft-yaml-storage` + +Specify extra binding requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To specify extra binding requirements, in your charm's ``charmcraft.yaml`` file +specify the ``extra-bindings`` key. + + See more: :ref:`file-charmcraft-yaml-extra-bindings` + +Require subordinate deployment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To require subordinate deployment for your charm (i.e., for it to be deployed +to the same machine as another charm, called its 'principal'), in your charm's +``charmcraft.yaml`` file specify the ``subordinate`` key. + + See more: :ref:`file-charmcraft-yaml-subordinate` + + +Manage actions +~~~~~~~~~~~~~~ + + See first: `Juju \| Action `_, + `Juju \| Manage actions `_ + + +To declare an action in your charm, in your charm's ``charmcraft.yaml`` file +specify the ``actions`` key. + + See more: :ref:`file-charmcraft-yaml-actions` + + See next: `Ops \| Manage actions + `_ + + +Manage configurations +~~~~~~~~~~~~~~~~~~~~~ + + See first: `Juju \| Application configuration + `_, + `Juju \| Manage applications > Configure + `_ + +To declare a configuration option for your charm, in your charm's ``charmcraft.yaml`` +specify the ``config`` key. + + + See more: :ref:`file-charmcraft-yaml-config` + + See next: `Ops \| Manage configurations + `_ + + + +Manage relations (integrations) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + See first: `Juju \| Relation `_, + `Juju \| Manage relations `_ + + To declare a relation endpoint in your charm, in your charm's ``charmcraft.yaml`` + specify the ``peers``, ``provides``, or ``requires`` key. + + See more: :ref:`file-charmcraft-yaml-peers` + + See more: `Ops \| Manage relations (integrations) + `_ + +Specify necessary libs +~~~~~~~~~~~~~~~~~~~~~~ + +.. See first: `Juju \| Library <>`_ + + + See more: :ref:`manage-libraries` + +Manage secrets +~~~~~~~~~~~~~~ + + See first: `Juju \| User secret `_ + +To make your charm capable of accepting a user secret, in your charm's +``charmcraft.yaml`` specify the ``config`` key with the ``type`` subkey set to +``secret``. + + See more: :ref:`file-charmcraft-yaml-config` + + See next: `Ops \| Manage secrets + `_ + +Specify necessary parts +~~~~~~~~~~~~~~~~~~~~~~~ + + See more: :ref:`manage-parts` + +Pack a charm +------------ + +To pack a charm directory, in the charm's root directory, run the command below: + +.. code:: text + + charmcraft pack + + +This will fetch any dependencies (from PyPI, based on ``requirements.txt``), +compile any modules, check that all the key files are in place, and produce a +compressed archive with the extension ``.charm``. As you can verify, this archive +is just a zip file with metadata and the operator code itself. + +.. dropdown:: Example session for a charm called microsample-vm + + .. code:: text + + # Pack the charm: + ~/microsample-vm$ charmcraft pack + Created 'microsample-vm_ubuntu-22.04-amd64.charm'. + Charms packed: + microsample-vm_ubuntu-22.04-amd64.charm + + # (Optional) Verify that this has created a .charm file in your charm's root directory: + ~/microsample-vm$ ls + CONTRIBUTING.md charmcraft.yaml requirements.txt tox.ini + LICENSE microsample-vm_ubuntu-22.04-amd64.charm src + README.md pyproject.toml tests + + # (Optional) Verify that the .charm file is simply a zip file that contains + # everything you've packed plus any dependencies: + /microsample-vm$ unzip -l microsample-vm_ubuntu-22.04-amd64.charm | { head; tail;} + Archive: microsample-vm_ubuntu-22.04-amd64.charm + Length Date Time Name + --------- ---------- ----- ---- + 815 2023-12-05 12:12 README.md + 11337 2023-12-05 12:12 LICENSE + 250 2023-12-05 12:31 manifest.yaml + 102 2023-12-05 12:31 dispatch + 106 2023-12-01 14:59 config.yaml + 717 2023-12-05 12:31 metadata.yaml + 921 2023-12-05 12:26 src/charm.py + 817 2023-12-01 14:44 venv/setuptools/command/__pycache__/upload.cpython-310.pyc + 65175 2023-12-01 14:44 venv/setuptools/command/__pycache__/easy_install.cpython-310.pyc + 4540 2023-12-01 14:44 venv/setuptools/command/__pycache__/py36compat.cpython-310.pyc + 1593 2023-12-01 14:44 venv/setuptools/command/__pycache__/bdist_rpm.cpython-310.pyc + 6959 2023-12-01 14:44 venv/setuptools/command/__pycache__/sdist.cpython-310.pyc + 2511 2023-12-01 14:44 venv/setuptools/command/__pycache__/rotate.cpython-310.pyc + 2407 2023-12-01 14:44 venv/setuptools/extern/__init__.py + 2939 2023-12-01 14:44 venv/setuptools/extern/__pycache__/__init__.cpython-310.pyc + --------- ------- + 20274163 1538 files + +The command has a number of flags that allow you to specify a different charm +directory to pack, whether to force pack if there are linting errors, etc. + + See more: :ref:`ref_commands_pack` + +.. caution:: + **If you’ve declared any resources :** This will *not* pack the resources. + This means that, when you upload your charm to Charmhub (if you do), you will + have to upload the resources separately. See more: :ref:`manage-resources`. + + + +.. important:: + When the charm is packed, a series of analyses and lintings will happen, + you may receive warnings and even errors to help improve the quality of the + charm. See more: + :ref:`Charmcraft analyzers and linters ` + +.. + + See next: `Juju \| Manage charms `_ + +.. _publish-a-charm: + +Publish a charm on Charmhub +--------------------------- + +1. Log in to Charmhub: + +.. code:: text + + charmcraft login + +.. + + See more: :ref:`manage-the-current-charmhub-user` + +2. Register your charm’s name (the one you specified in ``charmcraft.yaml`` > ``name``): + +.. code:: text + + charmcraft register my-awesome-charm + +.. + + See more: :ref:`manage-names` + +.. note:: + This automatically creates 4 channels, all with track ``latest`` but with different + risk levels, namely, edge, beta, candidate, stable, respectively. + See more: :ref:`manage-channels`. + +3. Upload the charm to Charmhub: Use the ``charmcraft upload`` command followed + by the your charm’s filepath. E.g., if you are in the charm’s root directory, + +.. code:: text + + charmcraft upload my-awesome-charm.charm + Revision 1 of my-awesome-charm created + +.. + + See more: :ref:`ref_commands_upload` + +.. note:: + Each time you upload a charm to Charmhub, that creates a revision (unless + you upload the exact same file again). See more: :ref:`manage-charm-revisions`. + +4. If your charm has associated resources: These are not packed with the + rest of the charm project, so you must upload them explicitly to + Charmhub as well. For example: + +.. code:: text + + $ charmcraft upload-resource my-super-charm someresource --filepath=/tmp/superdb.bin + Revision 1 created of resource 'someresource' for charm 'my-super-charm' + +.. + + See more: :ref:`manage-resources` + +.. note:: + Each time you upload a resource to Charmhub, that creates a revision (unless + you upload the exact same file again). See more: :ref:`manage-resource-revisions`. + +5. Release the charm: To release a charm, release your revision of + choice to the target release channel. E.g., + +.. code:: text + + $ charmcraft release my-awesome-charm --revision=1 --channel=beta + Revision 1 of charm 'my-awesome-charm' released to beta + +.. + + See more: :ref:`manage-charm-revisions` + +.. note:: + This automatically opens the channel. See more: :ref:`manage-channels`. + +.. + + See next: + `Juju \| Deploy a Charmub charm + `_, + `Juju \| Update a Charmhub charm + `_ + +.. tip:: + To update the charm on Charmhub, repeat the upload and release steps. + +.. important:: + Releasing a charm on Charmhub gives it a public URL. However, the charm will + not appear in the Charmhub search results until it has passed formal review. + To request formal review, reach out to the community to announce your charm + and ask for a review by an experienced community member. See more: `Discourse \| + review requests `_. + + Also, the point of publishing and having a charm publicly listed on Charmhub + is so others can reuse it and potentially contribute to it as well. To publicise + your charm: + + - `Write a Discourse post to announce your release. + `_ + + - `Schedule a community workshop to demo your charm’s capabilities. + `_ + + - `Chat about it with your charmer friends. + `_ + +.. diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst new file mode 100644 index 000000000..66057eedd --- /dev/null +++ b/docs/howto/manage-extensions.rst @@ -0,0 +1,212 @@ +.. _manage-extensions: + +How to manage extensions +======================== + + See also: :ref:`extension` + +View all the available extensions +--------------------------------- + +To view all the available Rockcraft / Charmcraft extensions, run the +``rockcraft list-extensions`` / ``charmcraft list-extensions`` command. For example: + +.. code:: text + + $ charmcraft list-extensions + Extension name Supported bases Experimental bases + ---------------- ----------------- -------------------- + flask-framework ubuntu@22.04 + +.. + + See more: `Rockcraft \| rockcraft list-extensions `_, :ref:`ref_commands_list-extensions` + +View details about the extension in use +--------------------------------------- + +Suppose you’ve initialised a rock / charm with a profile that comes with +an extension (currently, ``flask-framework``), and your +``rockcraft.yaml`` / ``charmcraft.yaml > extensions`` lists this +extension. + +.. dropdown:: Example + + .. code:: text + + $ mkdir my-flask-app-k8s + $ cd my-flask-app-k8s/ + $ charmcraft init --profile flask-framework + Charmed operator package file and directory tree initialised. + + Now edit the following package files to provide fundamental charm metadata + and other information: + + charmcraft.yaml + src/charm.py + README.md + + user@ubuntu:~/my-flask-app-k8s$ ls -R + .: + charmcraft.yaml requirements.txt src + + ./src: + charm.py + + $ cat charmcraft.yaml + # This file configures Charmcraft. + # See https://juju.is/docs/sdk/charmcraft-config for guidance. + + name: my-flask-app-k8s + + type: charm + + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + # (Required) + summary: A very short one-line summary of the flask application. + + # (Required) + description: | + A comprehensive overview of your Flask application. + + extensions: + - flask-framework + + # Uncomment the integrations used by your application + # requires: + # mysql: + # interface: mysql_client + # limit: 1 + # postgresql: + # interface: postgresql_client + # limit: 1 + + +To view details about what that extension is adding to your charm, set the +``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` environment variable to ``1``, +then run ``charmcraft expand-extensions``. For example: + + +.. code:: text + + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + + +.. dropdown:: See effect given example context + + + .. code:: text + + $ CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + *EXPERIMENTAL* extension 'flask-framework' enabled + name: my-flask-app-k8s + summary: A very short one-line summary of the flask application. + description: | + A comprehensive overview of your Flask application. + parts: + charm: + source: . + charm-entrypoint: src/charm.py + charm-binary-python-packages: [] + charm-python-packages: [] + charm-requirements: + - requirements.txt + charm-strict-dependencies: false + plugin: charm + type: charm + bases: + - build-on: + - name: ubuntu + channel: '22.04' + run-on: + - name: ubuntu + channel: '22.04' + actions: + rotate-secret-key: + description: Rotate the flask secret key. Users will be forced to log in again. + This might be useful if a security breach occurs. + assumes: + - k8s-api + containers: + flask-app: + resource: flask-app-image + peers: + secret-storage: + interface: secret-storage + provides: + metrics-endpoint: + interface: prometheus_scrape + grafana-dashboard: + interface: grafana_dashboard + requires: + logging: + interface: loki_push_api + ingress: + interface: ingress + limit: 1 + resources: + flask-app-image: + type: oci-image + description: flask application image. + config: + options: + webserver-keepalive: + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive + connection. + webserver-threads: + type: int + description: Run each webserver worker with the specified number of threads. + webserver-timeout: + type: int + description: Time in seconds to kill and restart silent webserver workers. + webserver-workers: + type: int + description: The number of webserver worker processes for handling requests. + flask-application-root: + type: string + description: Path in which the application / web server is mounted. This configuration + will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-debug: + type: boolean + description: Whether Flask debug mode is enabled. + flask-env: + type: string + description: What environment the Flask app is running in, by default it's 'production'. + flask-permanent-session-lifetime: + type: int + description: Time in seconds for the cookie to expire in the Flask application + permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME + environment variable. Run app.config.from_prefixed_env() in your Flask application + in order to receive this configuration. + flask-preferred-url-scheme: + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context + in the Flask application. By default, it's "HTTPS". This configuration will + set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-secret-key: + type: string + description: The secret key used for securely signing the session cookie and + for any other security related needs by your Flask application. This configuration + will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-session-cookie-secure: + type: boolean + description: Set the secure attribute in the Flask application cookies. This + configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. + Run app.config.from_prefixed_env() in your Flask application in order to + receive this configuration. + +.. + + See more: `Rockcraft \| rockcraft expand-extensions `_, :ref:`ref_commands_expand-extensions` diff --git a/docs/howto/manage-icons-create-1.png b/docs/howto/manage-icons-create-1.png new file mode 100644 index 000000000..b1460b0d8 Binary files /dev/null and b/docs/howto/manage-icons-create-1.png differ diff --git a/docs/howto/manage-icons-create-2.png b/docs/howto/manage-icons-create-2.png new file mode 100644 index 000000000..aecca2dc0 Binary files /dev/null and b/docs/howto/manage-icons-create-2.png differ diff --git a/docs/howto/manage-icons-create-3.png b/docs/howto/manage-icons-create-3.png new file mode 100644 index 000000000..5411552ed Binary files /dev/null and b/docs/howto/manage-icons-create-3.png differ diff --git a/docs/howto/manage-icons-validate.png b/docs/howto/manage-icons-validate.png new file mode 100644 index 000000000..6576de8fe Binary files /dev/null and b/docs/howto/manage-icons-validate.png differ diff --git a/docs/howto/manage-icons.rst b/docs/howto/manage-icons.rst new file mode 100644 index 000000000..3703b4ecf --- /dev/null +++ b/docs/howto/manage-icons.rst @@ -0,0 +1,90 @@ +.. _manage-icons: + +How to manage icons +=================== + +Learn about icon requirements and best practices +------------------------------------------------ + + See more: :ref:`file-icon-svg` + +Create an icon +-------------- + +Before you start you will need: + +- A vector graphic editor. We strongly recommend the cross-platform and + most excellent `Inkscape `__ for all your + vector graphic needs. +- `The template + file. `__ + (right-click > Save link as…) +- An existing logo you can import, or the ability to draw one in + Inkscape. + +Once you have those, fire up Inkscape and we can begin! + +1. Open the template +~~~~~~~~~~~~~~~~~~~~ + +From Inkscape load the ``icon.svg`` file. Select the Layer called +“Background Circle”, either from the drop down at the bottom, or from +the layer dialog. + +.. figure:: manage-icons-create-1.png + :alt: Open the template + + Open the template + +3. Add colour +~~~~~~~~~~~~~ + +In the menu, select **Object** and then **Fill and Stroke** to adjust +the colour. + +.. figure:: manage-icons-create-2.png + :alt: Add color + + +2. Draw something +~~~~~~~~~~~~~~~~~ + +Draw your shape within the circle. If you already have a vector logo, +you can import it and scale it within the guides. Inkscape also has +plenty of drawing tools for creating complex images. + +If you import a bitmap image to use, be sure to convert it into a vector +file and delete the bitmap. + +.. figure:: manage-icons-create-3.png + :alt: Draw something + +*Cloud icon: “Cloud by unlimicon from the Noun Project” [CC BY]* + +Validate an icon +---------------- + +You can validate your icon at +`charmhub.io/icon-validator `_. The +page checks the most basic issues that prevent icons working. + +.. figure:: manage-icons-validate.png + :alt: Validate + +Add an icon to its charm's Charmhub page +---------------------------------------- + +To add the icon to the charm's Charmhub page, save it as ``icon.svg``, place it +in the root directory of the charm, and then publish the charm to a channel of the +form ``/stable`` (e.g., ``latest/stable``). + +.. note:: + That is because Charmhub only updates the metadata for a charm on stable channel + releases (`by design + `_). + So either release the revision with the icon to a ``stable`` channel and then + roll it back, or wait until your charm is ready for a "stable" ``stable`` release. + +.. + + See more: :ref:`publish-a-charm` diff --git a/docs/howto/manage-libraries.rst b/docs/howto/manage-libraries.rst new file mode 100644 index 000000000..a64e6a290 --- /dev/null +++ b/docs/howto/manage-libraries.rst @@ -0,0 +1,137 @@ +.. _manage-libraries: + +How to manage libraries +======================= + + +.. See first: `Juju \| Library <>`_ + +Initialise a library +-------------------- + + See also: :ref:`ref_commands_create-lib` + +In your charm’s root directory, run ``charmcraft create-lib``: + +.. code:: text + + # Initialise a charm library named 'demo' + $ charmcraft create-lib demo + +.. note:: + Before creating a library, you must first register ownership of your charm’s + name. See more: :ref:`publish-a-charm`. + + +This will create a template file at ``$CHARMDIR/lib/charms/demo/v0/demo.py``. + + See more: :ref:`ref_commands_create-lib`, :ref:`file-libname-py` + +Edit this file to write your library. + +.. important:: + A library must comprise a single Python file. If you write a library that + feels too "big" for a single file, it is likely that the library should be + split up, or that you are actually writing a full-on charm. + +.. + + See next: `Ops \| Manage libraries `_ + +.. _publish-a-library: + +Publish a library on Charmhub +----------------------------- + +.. caution:: + On Charmhub, a library is always associated with the charm that it was first + created for. When you publisht it to Charmhub, it’s published to the page of + that charm. To be able to publish it, you need to be logged in to Charmhub as + a user who owns the charm (see more: :ref:`publish-a-charm`) or as a user who + is registered as a contributor to the charm (a status that can be requested via + `Discourse`_). + + +To publish a library on Charmhub, in the root directory of the charm that holds +the library, run ``charmcraft publish-lib`` followed by the full library path on +the template ``charms..v.``. For example: + +.. code:: text + + $ charmcraft publish-lib charms.demo.v0.demo + Library charms.demo.v0.demo sent to the store with version 0.1 + +This will upload the library’s content to Charmhub. + +To update the library on Charmhub, update the ``LIBAPI``/``LIBPATCH`` metadata +fields inside the library file, then repeat the publish procedure. + + + See more: :ref:`ref_commands_publish-lib` + + +.. caution:: **About the metadata fields:** + Most times it is enough to just increment ``LIBPATCH`` but, if you’re introducing + breaking changes, you must work with the major API version. + Additionally, be mindful of the fact that users of your library will update it + automatically to the latest PATCH version with the same API version. To avoid + breaking other people’s library usage, make sure toincrement the ``LIBAPI`` + version but reset ``LIBPATCH`` to ``0``. Also, before adding the breaking + changes and updating these values, make sureto copy the library to the new path; + this way you can maintain different major API versions independently, being able + to update, for example, your v0 after publishing v1. + See more: :ref:`file-libname-py`. + +.. + + +To share your library with other charm developers, navigate to the host charm's +Charmhub page, go to Libraries tab, then copy and share the URL at the top of the +page. + + +View the libs published for a charm +----------------------------------- + +The easiest way to find an existing library for a given charm is via +``charmcraft list-lib``, as shown below. This will query Charmhub and show which +libraries are published for the specified charm, along with API/patch versions. + +.. code:: + + jdoe@machine:/home/jane/autoblog$ charmcraft list-lib blogsystem + Library name API Patch + superlib 1 0 + +The listing will not show older API versions; this ensures that new users always start +with the latest version. + +Another good way to search for libraries is to explore the charm collection on +`Charmhub`_. + + See more: :ref:`ref_commands_list-lib` + + +Use a library +------------- + +In your charm's ``charmcraft.yaml``, specify the ``charm-libs`` key with the +desired libraries. + + See more: :ref:`file-charmcraft-yaml-charm-libs` + + +In your charm's root directory, run ``charmcraft fetch-libs``. Charmcraft will +download the libraries to your charm's directory. + + See more: :ref:`ref_commands_fetch-libs` + + +To use a library in your ``src/charm.py``, import it using its fully-qualified +path minus the ``lib`` part: + +.. code:: python + + import charms.demo.v0.demo + +To update your lib with the latest published version, repeat the process. diff --git a/docs/howto/manage-names.rst b/docs/howto/manage-names.rst new file mode 100644 index 000000000..8ab44ba51 --- /dev/null +++ b/docs/howto/manage-names.rst @@ -0,0 +1,46 @@ +.. _manage-names: + +How to manage names +=================== + +.. _register-a-name: + +Register a name on Charmhub +--------------------------- + +To register a name for your charm on Charmhub, use the ``charmcraft register`` +command followed by your desired name. E.g., + +.. code:: text + + $ charmcraft register my-awesome-charm + Congrats! You are now the publisher of 'my-awesome-charm' + +.. + + See more: :ref:`ref_commands_register` + +This also automatically creates four channels, all with track ``latest`` +but risk level ``edge``, ``beta``, ``candidate``, and ``stable``, +respectively. + + See more: :ref:`manage-channels` + +View registered names +--------------------- + +To view the names you’ve registered on Charmhub, run +``charmcraft names``. + + See more: :ref:`ref_commands_names` + +Unregister a name +----------------- + +.. caution:: + A name can be unregistered only if you haven't yet uploaded anything to it. + +To unregister a name, run ``charmcraft unregister`` followed by the +name. + + See more: :ref:`ref_commands_unregister` diff --git a/docs/howto/manage-parts.rst b/docs/howto/manage-parts.rst new file mode 100644 index 000000000..38a8ea17d --- /dev/null +++ b/docs/howto/manage-parts.rst @@ -0,0 +1,48 @@ +.. _manage-parts: + +How to manage parts +=================== + + See first: :ref:`part` + +Remove a part’s assets +---------------------- + +TBA + + See more: :ref:`ref_commands_clean` + +Download or retrieve artifacts defined for a part +------------------------------------------------- + +TBA + + See more: :ref:`ref_commands_pull` + +Build artifacts defined for a part +---------------------------------- + +TBA + + See more: :ref:`ref_commands_build` + +Stage built artifacts into a common staging area +------------------------------------------------ + +TBA + + See more: :ref:`ref_commands_build` + +Prime artifacts defined for a part +---------------------------------- + +TBA + + See more: :ref:`ref_commands_prime` + +Build the charm or bundle +------------------------- + +TBA + + See more: :ref:`ref_commands_pack` diff --git a/docs/howto/manage-resources.rst b/docs/howto/manage-resources.rst new file mode 100644 index 000000000..dbac97b16 --- /dev/null +++ b/docs/howto/manage-resources.rst @@ -0,0 +1,125 @@ +.. _manage-resources: + +How to manage resources +======================= + + See first: `Juju \| Charm resource `_, + `Juju \| Manage resources `_ + +Declare a resource +------------------ + +To declare a resource required by your charm, in your charm’s +:doc:`/reference/files/file-charmcraft-yaml` specify the ``resources`` key. + + See more: :ref:`file-charmcraft-yaml-resources` + + See next: + :external+ops:doc:`Ops \| How to manage resources ` + +.. tip:: + During development, it may be useful to specify the resource at deploy time to + facilitate faster testing without the need to publish a new charm/resource in + between minor fixes. For example, assuming the resource is a ``/tmp/somefile.txt`` + file, you could pack and the deploy with ``juju deploy … –-resource``: + +.. code:: text + + echo "TEST" > /tmp/somefile.txt + charmcraft pack + juju deploy ./my-charm.charm --resource my-resource=/tmp/somefile.txt + + +.. _publish-a-resource: + +Publish a resource on Charmhub +------------------------------ + +.. note:: You must have already published the charm. See more: :ref:`publish-a-charm`. + +To publish a resource on its charm's Charmhub page, run ``charmcraft upload-resource`` +followed by the name of the charm, the name of the resource (cf. ``charmcraft.yaml``), +and ``--filepath=`` / ``--image=``. For example: + +.. note:: + The option ``--image`` must indicate an OCI image's digest, being it in the + short or long form (e.g.: ``70aa8983ec5c`` or + ``sha256:64aa8983ec5cea7bc143af18829836914fa405184d56dcbdfd9df672ade85249``). + When using the "short form" of the digest, the image needs to be present + locally so its proper ID (the "long form") can be retrieved. + +.. code:: text + + $ charmcraft upload-resource my-super-charm someresource --filepath=/tmp/superdb.bin + Revision 1 created of resource 'someresource' for charm 'my-super-charm' + +.. code:: text + + $ charmcraft upload-resource my-super-charm redis-image --image=sha256:64aa8983ec5cea7bc143af18829836914fa405184d56dcbdfd9df672ade85249 + Revision 1 created of resource 'redis-image' for charm 'my-super-charm' + +Charmcraft will first check if that specific image is available in Canonical’s +Registry, and just use it if that’s the case. If not, it will try to get it from +the developer’s local OCI repository (needs ``dockerd`` to be installed and +running), push it to the Canonical’s Registry, and then use it. Either way, when +the upload has completed, you end up with a resource revision. + +To update a pre-uploaded resource, run the ``upload-resource`` command again. +The result will be a new revision. + + See more: :ref:`ref_commands_upload-resource` + +View all the resources published on Charmhub +-------------------------------------------- + +To view all the resources published on Charmhub for a charm, run +``charmcraft resources`` followed by the charm name: + +.. important:: **If you’re not logged in to Charmhub:** + The command will open up a browser window and ask you to log in. + +.. code:: text + + $ charmcraft resources mycharm + +.. + + See more: :ref:`ref_commands_resources` + +.. _manage-resource-revisions: + +Manage resource revisions +------------------------- + +List all the available resource revisions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To view all the revisions for a resource associated with a charm you’ve +uploaded to Charmhub, run ``charmcraft resource-revisions`` followed by +the charm name and the resource name. For example: + +.. code:: text + + $ charmcraft resource-revisions mycharm myresource + +.. + + See more: :ref:`ref_commands_resource-revisions` + +Set the architectures for a resource revision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To set the architectures for a revision of a resource associated with a +charm you’ve uploaded to Charmhub, run +``charmcraft set-resource-architectures`` followed by the name of the +charm, the name of the resource, and the architecture(s), using the +``--resources`` flag to specify the target resource revision. For +example: + +.. code:: text + + $ charmcraft set-resource-architectures mycharm myresource --revision=1 arm64,armhf + +.. + + See more: :ref:`ref_commands_set-resource-architectures` diff --git a/docs/howto/manage-revisions.rst b/docs/howto/manage-revisions.rst new file mode 100644 index 000000000..62cb4ca9b --- /dev/null +++ b/docs/howto/manage-revisions.rst @@ -0,0 +1,87 @@ +.. _manage-charm-revisions: + +How to manage charm revisions +============================= + +.. raw:: html + + + +Create a charm revision +----------------------- + +A charm revision is created implicitly every time you upload a charm to +Charmhub (unless you’re uploading the exact same file again). + + See more: :ref:`publish-a-charm` + +View the existing charm revisions +--------------------------------- + +To inspect the existing charm revisions, run ``charmcraft revisions`` +followed by the name of the charm. + + See more: :ref:`ref_commands_revisions` + +Promote a charm revision to a better risk level +----------------------------------------------- + +To promote a charm revision to a higher-ranking risk level, use the GitHub +``promote-charm`` action. + + See more: `GitHub | canonical/charming-actions/promote-charm + `_ + +.. dropdown:: Example outcome + + For example, in the (partial) output of juju info mongodb below, + revision 100 has been promoted from ``3.6/edge`` through ``3.6/beta`` + and ``3.6/candidate`` all the way to ``3.6/stable``. (The up arrow next + to ``3.6/beta`` indicates that that channel has been closed and, if you + try ``juju deploy --channel 3.6/beta``, what you’ll get is the next + higher-ranking risk level of the same track, that is, + ``3.6/candidate``.) + + .. code:: text + + channels: | + 5/stable: 117 2023-04-20 (117) 12MB amd64 ubuntu@22.04 + 5/candidate: 117 2023-04-20 (117) 12MB amd64 ubuntu@22.04 + 5/beta: ↑ + 5/edge: 118 2023-05-03 (118) 13MB amd64 ubuntu@22.04 + 3.6/stable: 100 2023-04-28 (100) 860kB amd64 ubuntu@20.04, ubuntu@18.04 + 3.6/candidate: 100 2023-04-13 (100) 860kB amd64 ubuntu@20.04, ubuntu@18.04 + 3.6/beta: ↑ + 3.6/edge: 100 2023-02-03 (100) 860kB amd64 ubuntu@20.04, ubuntu@18.04 + + + +.. _release-a-revision-into-a-channel: + +Release a charm revision into a channel +--------------------------------------- + +To release a specific charm revision to a channel, run ``charmcraft release`` followed +by the name of the charm and flags specifying the revision and its target channel. E.g., + +.. code:: text + + $ charmcraft release my-awesome-charm --revision=1 --channel=beta + Revision 1 of charm 'my-awesome-charm' released to beta + +.. + + See more: :ref:`ref_commands_release` + +This opens the channel you’re releasing to. + + See more: :ref:`manage-channels` + +Following the release, Charmhub will display the charm’s information at +``charmhub.io/``. (The default information displayed is obtained from the +most stable channel.) Your charm will also become available for download. + + See more: `Juju | Manage charms`_ + + +.. _`Juju | Manage charms`: https://juju.is/docs/juju/manage-charms-or-bundles diff --git a/docs/howto/manage-the-charmcraft-cli.rst b/docs/howto/manage-the-charmcraft-cli.rst new file mode 100644 index 000000000..9ca1d52a7 --- /dev/null +++ b/docs/howto/manage-the-charmcraft-cli.rst @@ -0,0 +1,146 @@ +.. _manage-the-charmcraft-cli: + +How to manage the ``charmcraft`` CLI +==================================== + + See first: :ref:`charmcraft-cli` + +Install the ``charmcraft`` CLI +------------------------------ + +On Linux +~~~~~~~~ + +The recommended way to install Charmcraft on Linux is from the +``stable`` channel via snap: + +.. code:: text + + sudo snap install charmcraft --classic + +There are multiple channels other than ``stable``. See the full list +with ``snap info charmcraft``. + +We recommend either ``latest/stable`` or ``latest/candidate`` for +everyday charming. With the snap you will always be up to date as +Charmhub services and APIs evolve. Charmcraft supports Kubernetes +operator development. + +In Linux, Charmcraft defaults to LXD to build the charms in a container +matching the target base(s) (Multipass can also be used). Charmcraft +will offer to install LXD if required, but here are steps to set it up +manually: + +.. code:: text + + $ sudo snap install lxd + $ sudo adduser $USER lxd + $ newgrp lxd + $ lxd init --auto + +You can also install Charmcraft in an isolated environment. + + See more: :ref:`install-in-an-isolated-environment` + +On macOS +~~~~~~~~ + +An unofficial Charmcraft package is +`available on homebrew `_. + +Installation should be straightforward if using homebrew (if not already set up, +refer to `these instructions `_). + +.. code:: text + + $ brew install charmcraft + ==> Downloading https://ghcr.io/v2/homebrew/core/charmcraft/manifests/1.3.2 + ######################################################################## 100.0% + ==> Downloading https://ghcr.io/v2/homebrew/core/charmcraft/blobs/sha256:ebe7aac3dcfa401762faaf339a28e64bb5fb277a7d96bbcfb72bdc + ==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:ebe7aac3dcfa401762faaf339a28e64bb5fb277a7d + ######################################################################## 100.0% + ==> Pouring charmcraft--1.3.2.mojave.bottle.tar.gz + 🍺 /usr/local/Cellar/charmcraft/1.3.2: 2,205 files, 17.2MB + +Charmhub commands work natively: + +.. code:: text + + $ charmcraft whoami + name: John Doe + username: jdoe + id: xxxxxxxxxxxxxxxxxxxxxxxxx + +In macOS, Charmcraft defaults to Multipass to build the charms in a container +matching the target base(s). Running pack asks to setup Multipass if not already +installed, and continues with the packing process: + +.. code:: text + + $ charmcraft pack + Multipass is required, but not installed. Do you wish to install Multipass and configure it with the defaults? [y/N]: y + ==> Downloading https://github.com/canonical/multipass/releases/download/v1.7.2/multipass-1.7.2+mac-Darwin.pkg + Already downloaded: /Users/jdoe/Library/Caches/Homebrew/downloads/4237fcef800faa84459a2911c3818dfa76f1532d693b151438f1c8266318715b--multipass-1.7.2+mac-Darwin.pkg + ==> Installing Cask multipass + ==> Running installer for multipass; your password may be necessary. + Package installers may write to any location; options such as `--appdir` are ignored. + installer: Package name is multipass + installer: Installing at base path / + installer: The install was successful. + 🍺 multipass was successfully installed! + Packing charm 'test-charm_ubuntu-20.04-amd64.charm'... + Starting charmcraft-test-charm-12886917363-0-0-amd64 ... + +You can also install Charmcraft in an isolated environment. + + See more: :ref:`install-in-an-isolated-environment` + +On Windows +~~~~~~~~~~ + +There is no previously packaged way to install Charmcraft in Windows, +please refer to how to install it in an `isolated +environment <#heading--isolated>`_. + +.. _install-in-an-isolated-environment: + +In an isolated environment on Linux, macOS, or Windows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another way to install Charmcraft is via `Multipass`_. +This is a good way to install it on any platform, as it will give you an isolated +development environment. + +First, `install Multipass `_. + +Second, use Multipass to provision a virtual machine. The following command will +launch a fresh new VM with 4 cores, 8GB RAM and a 20GB disk and the name ‘charm-dev’: + +.. code:: text + + $ multipass launch --cpus 4 --memory 8G --disk 20G --name charm-dev + +Last, open a shell in your new Ubuntu virtual machine, and install +Charmcraft there: + +.. code:: text + + $ multipass shell charm-dev + ... + ubuntu@charm-dev:~$ sudo snap install charmcraft --classic + charmcraft 2.2.0 from Canonical✓ installed + +That’s it. You can now start typing in Charmcraft commands. + +Check the installed version of the ``charmcraft`` CLI +----------------------------------------------------- + +To check the installed version, run: + +.. code:: text + + charmcraft version + +.. + + See more: :ref:`ref_commands_version` diff --git a/docs/howto/manage-the-current-charmhub-user.rst b/docs/howto/manage-the-current-charmhub-user.rst new file mode 100644 index 000000000..e0e55fa59 --- /dev/null +++ b/docs/howto/manage-the-current-charmhub-user.rst @@ -0,0 +1,129 @@ +.. _manage-the-current-charmhub-user: + +How to manage the current Charmhub user +======================================= + + See first: `Charmhub`_ + +Log in to Charmhub +------------------ + +Local environments +~~~~~~~~~~~~~~~~~~ + +To log in to Charmhub, run ``charmcraft login``: + +.. code:: text + + $ charmcraft login + Opening an authorization web page in your browser. + If it does not open, please open this URL: + ... + +.. + + See more: :ref:`ref_commands_login` + +Remote environments +~~~~~~~~~~~~~~~~~~~ + + Introduced in Charmcraft 1.3 + +When working locally with Charmcraft, the developer will use +``charmcraft login`` to get authentication tokens from Charmhub, which +would be stored in the local keyring and used on all operations that +need them. + +This is fine for local environments, but for remote ones (e.g. a CI/CD +system) on one hand it’s desirable to not login using the standard +method (a browser opening an authentication web page to insert user, +password, and *2FA*), and on the other hand the authentication tokens +should be limited in different ways. + +The alternative login method is implemented through the +``CHARMCRAFT_AUTH`` environment variable, which needs to be set to +useful credentials (which are obtained using the ``--export`` option, +see below). + +If that variable is set Charmcraft will use it for all operations that +needs authentication against Charmhub. Note that in this case the +``login`` and ``logout`` commands can not be used. + +To obtain credentials to be used in ``CHARMCRAFT_AUTH``, the ``login`` +command has the ``--export`` option, which accepts a file path. If +specified, it will override the regular behaviour of storing the +credentials in the user’s keyring, and those will be exported to the +given file path. The content of this file is what should be used +verbatim in the environment variable. + +As mentioned at the beginning, it’s also a good idea to use restricted +credentials in a remote system. For this situation, the Charmcraft’s +``login`` command provides different options to attenuate the obtained +authentication tokens: + +- ``--charm``: the charm name on which the permission will apply (can + be specified multiple times) +- ``--bundle``: the bundle name on which the permission will apply (can + be specified multiple times) +- ``--permission``: what action can be done on the specified package(s) + (see below for a list; can be specified multiple times) +- ``--channel``: the channel on which the package can be released (can + be specified multiple times) +- ``--ttl``: the time, in seconds, that the granted token will be + useful (defaults to 30 days) + +All these indications are optional, and default to no restrictions +applied on each category (except indicated the time-to-live, as +indicated above). Note also that these restrictions can only be used if +the credentials are exported to a file with the ``--export`` option. + +The available permissions are: - ``account-register-package``: +register/request a new package name - ``account-view-packages``: list +packages owned by the account or for which this account has collaborator +rights - ``package-manage``: meta permission that includes all the +``package-manage-*`` ones - ``package-manage-acl``: add/invite/remove +collaborators - ``package-manage-metadata``: edit metadata, add/remove +media, etc. - ``package-manage-releases``: release revisions and close +channels - ``package-manage-revisions``: upload new blobs, check for +upload status - ``package-view``: meta permission that includes all the +``package-view-*`` ones - ``package-view-acl``: list the collaborators +for a package, return privacy settings - ``package-view-metadata``: view +the metadata for a package, including media - ``package-view-metrics``: +view the metrics of a package - ``package-view-releases``: list the +current releases (channel map) and the history of releases for a package +- ``package-view-revisions``: list the existing revisions for a package, +along with status information + +So, an example sequence for requesting/using credentials to set up a +CI/CD system that will push and release a charm could be: + +- get the specific credentials in a file: + ``bash $ charmcraft login --export=secrets.auth --charm=my-super-charm --permission=package-manage --channel=edge --ttl=2592000 Login successful. Credentials exported to 'secrets.auth'.`` + +- test that all is fine; for this get the content: + :literal:`bash $ CHARMCRAFT_AUTH=`cat test1\` charmcraft whoami name: J. Doe username: jdoe-superdev id: VTLZAToLcdaIPtisVBjfiQYCXbpKwbCc charms: - my-super-charm permissions: - package-manage channels: - edge time to live (s): 2592000` + +- to use this authorization token on a CI/CD system set the environment + variable CHARMCRAFT_AUTH with the content of ``secrets.auth`` file, + and use Charmcraft as normal: + + .. code:: text + + export CHARMCRAFT_AUTH= + ... + charmcraft upload my-super-charm.charm --release edge + + +Check the currently logged in user +---------------------------------- + +To check the currently logged in user, run ``charmcraft whoami``. + + See more: :ref:`ref_commands_whoami` + +Log out of Charmhub +------------------- + +To log out of Charmhub, run ``charmcraft logout``. + + See more: :ref:`ref_commands_logout` diff --git a/docs/howto/manage-tracks.rst b/docs/howto/manage-tracks.rst new file mode 100644 index 000000000..210d5f210 --- /dev/null +++ b/docs/howto/manage-tracks.rst @@ -0,0 +1,131 @@ +.. _manage-tracks: + +How to manage tracks +==================== + +.. See also: :ref:`track` Add link to Juju docs > charm > channel > track + +When you register a charm name on Charmhub, you automatically get 4 +channels, all with track ``latest``. However, as your charm evolves, +you’ll likely want to customise the shape of this track (e.g., to align +with the workload) and then create new tracks in the new pattern. This +document shows you how. + +.. _request-a-track-guardrail: + +Request a track guardrail +------------------------- + +.. See also: :ref:`guardrail` (link to new Juju docs > charm > channel > track > guardrail) + +To request a track guardrail, contact a Charmhub admin by creating a +post on Discourse under the **charmhub requests** category, that is, +here: :literalref:`https://discourse.charmhub.io/c/charmhub-requests`. + +.. _create-a-track: + +Create a track +-------------- + +Once you’ve requested a track guardrail, there are two ways to create a +new track for your charm – you can keep contacting a Charmhub admin +every time or you can self-service. For most cases the latter option is +likely to be more convenient and faster. + +Ask a Charmhub admin +~~~~~~~~~~~~~~~~~~~~ + +To create a new track by contacting a Charmhub admin, create a post on +Discourse under the **charmhub requests** category, that is, here: +https://discourse.charmhub.io/c/charmhub-requests/46 . The admin will +create the new track that fits within the track guardrail you’ve set up +for your charm. + +Create it yourself +~~~~~~~~~~~~~~~~~~ + +To create a new track yourself, follow the steps below: + +.. important:: + As you might notice, this path is currently a little hacky. In the long-term it + should become a lot smoother as there are plans to support it through the + Charmcraft CLI. + +.. important:: + As you will see, this method currently relies on ``charmcraft`` + ``curl``. + We recommend the Charmcraft bit because Charmcraft already understands the + authentication mechanism used by Charmhub and can generate a suitable + authentication token (macaroon) that will make it possible to then use ``curl`` + directly to interact with the Charmhub API. This method also has the advantage + that it can be adapted to use any HTTP client or library as long as it can pass + custom headers. + +1. Enable ``curl`` access to the Charmhub API. + +First, install ``curl`` and ``jq``. + +.. note:: + You might already have both. + + +Then, use Charmcraft to log in to Charmhub and export your Charmhub +credentials / token (macaroon) to a file: + +.. code:: text + + charmcraft login --export charmhub-creds.dat + +Next, decode and extract the macaroon from the .dat file and place it in a header +in an environment variable: + +.. code:: text + + export CHARMHUB_MACAROON_HEADER="Authorization: Macaroon $(cat charmhub-creds.dat | base64 -d | jq -r .v)" + +At this point you can use this variable in ``curl`` commands – just make sure to +specify the correct ``Content-Type``. + +**2. Use curl to view the existing guardrails and tracks.** To view the +guardrails and tracks associated with your charm, issue an HTTP ``GET`` request to +``/v1//``. For example, for a charm named ``hello-world-charm``: + +.. code:: text + + curl https://api.charmhub.io/v1/charm/hello-world-charm -H'Content-type: application/json' -H "$CHARMHUB_MACAROON_HEADER" + +The guardrails and tracks of the package will be under the ``track-guardrails`` +and ``tracks`` keys of ``metadata``. Now you know what the new track may look like. + + See more: `Charmhub API docs > package\_metadata + `_ + +.. important:: + + **If you want to view the guardrails and tracks for all published charms:** + + Issue an HTTP ``GET`` request to ``/v1/``, as below. + See more: `Charmhub API docs > list_registered_names + `_. + + .. code:: text + + curl https://api.charmhub.io/v1/charm -H'Content-type: application/json' -H "$CHARMHUB_MACAROON_HEADER" + + +3. Use ``curl`` to create a new track. Finally, to create a new track for your + charm, issue an HTTP ``POST`` request to ``/v1///tracks``, + where ``name`` and ``namespace`` refer to the name and type of the package + respectively. For example, given a charm named ``hello-world-charm``, one can + create two tracks ``v.1`` and ``v.2`` as follows: + +.. code:: text + + curl https://api.charmhub.io/v1/charm/hello-world-charm/tracks -X POST -H'Content-type: application/json' -H "$CHARMHUB_MACAROON_HEADER" -d '[{"name": "v.1"}, {"name": "v.2"}]' + +Of course, the tracks must conform to the existing guardrail for the +charm. + + See more: `Charmhub API docs > create_tracks + `_ + +That’s it, you now have a new track for your charm! diff --git a/docs/howto/shared-cache.rst b/docs/howto/misc/#shared-cache.rst# similarity index 100% rename from docs/howto/shared-cache.rst rename to docs/howto/misc/#shared-cache.rst# diff --git a/docs/howto/charm-to-poetry.rst b/docs/howto/misc/charm-to-poetry.rst similarity index 100% rename from docs/howto/charm-to-poetry.rst rename to docs/howto/misc/charm-to-poetry.rst diff --git a/docs/howto/charm-to-python.rst b/docs/howto/misc/charm-to-python.rst similarity index 100% rename from docs/howto/charm-to-python.rst rename to docs/howto/misc/charm-to-python.rst diff --git a/docs/howto/misc/index.rst b/docs/howto/misc/index.rst new file mode 100644 index 000000000..d2ffd3a1f --- /dev/null +++ b/docs/howto/misc/index.rst @@ -0,0 +1,13 @@ +.. _howto-misc: + +Miscellaneous +============= + +.. toctree:: + :maxdepth: 2 + + Migrate to poetry + Migrate to python + Cache intermediate build artefacts + Pack a hook-based charm with Charmcraft + Pack a reactive charm with Charmcraft diff --git a/docs/howto/misc/pack-a-hooks-based-charm-with-charmcraft.rst b/docs/howto/misc/pack-a-hooks-based-charm-with-charmcraft.rst new file mode 100644 index 000000000..a3f952fc4 --- /dev/null +++ b/docs/howto/misc/pack-a-hooks-based-charm-with-charmcraft.rst @@ -0,0 +1,112 @@ +.. _pack-a-hooks-based-charm-with-charmcraft: + +How to pack a hooks-based charm with Charmcraft +=============================================== + + Introduced in Charmcraft 1.4 + + See first: + :external+ops:doc:`Ops | Turn a hooks-based charm into an Ops charm + ` + +Suppose you have a legacy hooks-only charm, for example, +`tiny-bash `_, which you can obtain as follows: + +.. code:: text + + $ git clone https://github.com/erik78se/tiny-bash + + +To make it packable by Charmcraft, all you need to do is navigate inside the charm +directory and create a ``charmcraft.yaml`` file with the part definition for a +hooks-based charm, as shown below: + +.. code:: yaml + + type: charm + + bases: + - build-on: + - name: "ubuntu" + channel: "20.04" + run-on: + - name: "ubuntu" + channel: "20.04" + + parts: + tiny-bash: + plugin: dump + source: . + prime: + - LICENSE + - README.md + - config.yaml + - copyright + - hooks + - icon.svg + - metadata.yaml + + +After this, you can pack your charm with ``charmcraft pack``, as usual: + +.. code:: text + + $ charmcraft pack + Charms packed: + tiny-bash_ubuntu-20.04-amd64.charm + +If successful, the result should look as below, i.e., the charm file +should contain all the files listed in the ``prime`` section of the +``tiny-bash`` part and the charm manifest. + +.. code:: text + + $ unzip -l tiny-bash_ubuntu-20.04-amd64.charm + Archive: tiny-bash_ubuntu-20.04-amd64.charm + Length Date Time Name + --------- ---------- ----- ---- + 423 2021-11-12 19:37 metadata.yaml + 431 2021-11-12 19:37 README.md + 12 2021-11-12 19:37 config.yaml + 3693 2021-11-12 19:37 icon.svg + 38 2021-11-12 19:37 copyright + 261 2021-11-12 20:08 manifest.yaml + 34523 2021-11-12 19:37 LICENSE + 381 2021-11-12 19:37 hooks/update-status + 346 2021-11-12 19:37 hooks/start + 1294 2021-11-12 19:37 hooks/shared-fs-relation-changed + 563 2021-11-12 19:37 hooks/stop + 497 2021-11-12 19:37 hooks/leader-elected + 447 2021-11-12 19:37 hooks/install + 417 2021-11-12 19:37 hooks/leader-settings-changed + 811 2021-11-12 19:37 hooks/upgrade-charm + 625 2021-11-12 19:37 hooks/config-changed + --------- ------- + 44762 16 files + + +And you can also deploy your application with ``juju deploy``, as usual: + +.. code:: text + + $ juju deploy ./tiny-bash_ubuntu-20.04-amd64.charm + Located local charm "tiny-bash", revision 0 + Deploying "tiny-bash" from local charm "tiny-bash", revision 0 + +If successful, the result should look as below, i.e., with the application status +active. + +.. code:: text + + $ juju status + Model Controller Cloud/Region Version SLA Timestamp + default localhost-localhost localhost/localhost 2.9.12 unsupported 17:23:23-03:00 + + App Version Status Scale Charm Store Channel Rev OS Message + tiny-bash active 1 tiny-bash local 0 ubuntu update-status ran: 20:22 + + Unit Workload Agent Machine Public address Ports Message + tiny-bash/0* active idle 0 10.2.17.31 update-status ran: 20:22 + + Machine State DNS Inst id Series AZ Message + 0 started 10.2.17.31 juju-55481c-0 focal Running diff --git a/docs/howto/misc/pack-a-reactive-charm-with-charmcraft.rst b/docs/howto/misc/pack-a-reactive-charm-with-charmcraft.rst new file mode 100644 index 000000000..e64dbb7de --- /dev/null +++ b/docs/howto/misc/pack-a-reactive-charm-with-charmcraft.rst @@ -0,0 +1,35 @@ +.. _pack-a-reactive-charm-with-charmcraft: + +How to pack a reactive charm with Charmcraft +================================================== + + Introduced in Charmcraft 1.4. + + +.. See also: - +.. {ref}\ ``How to set up a charm project `` +.. - +.. {ref}\ ``How to pack your charm using Charmcraft `` +.. - {ref}\ ``About charm types, by creation type `` + +To pack a legacy reactive charm with Charmcraft, in the charm directory create a +``charmcraft.yaml`` file with the part definition for a reactive-based charm: + +.. code:: yaml + + type: "charm" + bases: + - build-on: + - name: "ubuntu" + channel: "20.04" + run-on: + - name: "ubuntu" + channel: "20.04" + parts: + charm: + source: . + plugin: reactive + build-snaps: [charm] + +Done. Now you can go ahead and pack your reactive-based charm with Charmcraft +in the usual way using ``charmcraft pack``. diff --git a/docs/howto/misc/shared-cache.rst b/docs/howto/misc/shared-cache.rst new file mode 100644 index 000000000..9fe793c68 --- /dev/null +++ b/docs/howto/misc/shared-cache.rst @@ -0,0 +1,78 @@ +.. _howto-shared-cache: + +Cache intermediate build artefacts +================================== + +Because Charmcraft builds Python packages from source rather than using pre-built +wheels, the initial builds of charms can take a while. The intermediate artefacts +get cached, which significantly speeds up subsequent builds. + +When installed as a snap, Charmcraft automatically caches these wheels in the +``~/snap/charmcraft/common/cache`` directory. However, in some cases, it may be +beneficial to change this directory. + +This can be especially useful in CI, where you may wish to specify a directory that +gets cached between CI runs. + +Local usage +----------- + +When packing locally, you can change where Charmcraft caches build artefacts by setting +the ``CRAFT_SHARED_CACHE`` environment variable to the path of an existing directory to +use instead:: + + mkdir -p /tmp/charmcraft + CRAFT_SHARED_CACHE=/tmp/charmcraft charmcraft pack + +On GitHub +--------- + +While it's recommended that you use the ``charmcraft/pack`` action from +`craft-actions`_ where possible, the following workflow will manually pack a charm, +caching the intermediate files: + +.. code-block:: yaml + + name: Pack charm + on: + pull_request: + jobs: + pack: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: canonical/craft-actions/charmcraft/setup + - uses: actions/cache@v4 + with: + path: ${{ runner.temp }} + key: charmcraft-cache-${{ hashfiles('requirements.txt') }} + restore-keys: | + charmcraft-cache- + - env: + CRAFT_SHARED_CACHE: ${{ runner.temp } + run: | + charmcraft pack + +On GitLab +--------- + +The following example ``gitlab-ci.yml`` will install and run Charmcraft to pack your +charm, caching the intermediate artefacts: + +.. code-block:: yaml + + pack-charm: + cache: + - key: + files: + - requirements.txt + paths: + - .charmcraft_cache/ + variables: + CRAFT_SHARED_CACHE: .charmcraft_cache/ + script: + - mkdir -p .charmcraft_cache + - snap install charmcraft + - charmcraft pack + +.. _craft-actions: https://github.com/canonical/craft-actions diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..5d41194ce --- /dev/null +++ b/docs/index.md @@ -0,0 +1,81 @@ +# Charmcraft (`charmcraft`) + +```{toctree} +:maxdepth: 2 +:hidden: true + +tutorial/index +howto/index +reference/index +explanation/index +``` + +Charmcraft (`charmcraft`) is a tool designed to simplify the creation, building, and sharing of a [`juju` charm](https://juju.is/docs/juju/charmed-operator). + +When you initialise a charm with the `charmcraft` CLI, you automatically get all the crucial project files, pre-populated with helpful template content. These files are such that they can be `charmcraft`-packed right away; however, to make them meaningul for the application you are charming, you'll want to customise the YAML and [`ops`](https://juju.is/docs/sdk/ops)-powered Python in these files. For certain types of applications (Django, FastAPI, Flask, Go), if you initialise with a suitable `charmcraft` extension, things are even easier -- just tweak a few values in the YAML and you get a fully functioning charm. Either way, once you're pleased with what you've got, you can again use `chamcraft` to publish your charm on [Charmhub](https://charmhub.io/). + +You can create, build, and share a charm any way you want, but with `charmcraft` you get state-of-the-art results in record time. + +If you're a charm author, you *must* use `charmcraft`! + + +--------- + +## In this documentation + +````{grid} 1 1 2 2 + +```{grid-item-card} [Tutorial](/index) +:link: tutorial/index +:link-type: doc + +**Start here**: a hands-on introduction to Example Product for new users +``` + +```{grid-item-card} [How-to guides](/index) +:link: howto/index +:link-type: doc + +**Step-by-step guides** covering key operations and common tasks +``` + +```` + +````{grid} 1 1 2 2 +:reverse: + +```{grid-item-card} [Reference](/index) +:link: reference/index +:link-type: doc + +**Technical information** - specifications, APIs, architecture +``` + +```{grid-item-card} [Explanation](/index) +:link: explanation/index +:link-type: doc + +**Discussion and clarification** of key topics +``` + +```` + +--------- + + +## Project and community + +Charmcraft is a member of the Canonical family. It's an open source project +that warmly welcomes community projects, contributions, suggestions, fixes +and constructive feedback. + +* [Ubuntu Code of Conduct](https://ubuntu.com/community/code-of-conduct). +* [Canonical contributor licenses agreement](https://ubuntu.com/legal/contributors). + + diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 46bf6025c..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. Charmcraft documentation root file - -Charmcraft -========== - -Charmcraft is part of the `Juju Charm SDK `_. -Most of Charmcraft's documentation is available there. - -.. toctree:: - :maxdepth: 1 - :hidden: - - tutorial/index - howto/index - reference/index - explanation/index - -.. grid:: 1 1 2 2 - - .. grid-item-card:: `Tutorial `_ - - **Get started** with a hands-on introduction to Charmcraft - - .. grid-item-card:: :ref:`How-to guides ` - - **Step-by-step guides** covering key operations and common tasks - -.. grid:: 1 1 2 2 - :reverse: - - .. grid-item-card:: :ref:`Reference ` - - **Technical information** about Charmcraft - - .. grid-item-card:: :ref:`Explanation ` - - **Discussion and clarification** of key topics - -Project and community -===================== - -Charmcraft is a member of the Canonical family. It's an open source project -that warmly welcomes community projects, contributions, suggestions, fixes -and constructive feedback. - -* `Ubuntu Code of Conduct `_. -* `Canonical contributor licenses agreement - `_. - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/docs/reference/charmcraft-analyzers-and-linters.md b/docs/reference/charmcraft-analyzers-and-linters.md new file mode 100644 index 000000000..ab12eba10 --- /dev/null +++ b/docs/reference/charmcraft-analyzers-and-linters.md @@ -0,0 +1,88 @@ +(charmcraft-analyzers-and-linters)= +# `charmcraft` analyzers & linters + +The following are the different checks that Charmcraft will run explicitly (when the user executes its `analyse` method) or implicitly (when packing charms). + +Any linter or analysis can be set in [the config](https://juju.is/docs/sdk/charmcraft-config) to be excluded from the normal execution. Also note that if any linter ends in error it will block the charm packing (you can pack it anyway using the `--force` option). + +You can read more about these checks in the [Charmcraft Analyze Specification](https://discourse.charmhub.io/t/proposal-charmcraft-analyze/4792). + + +## Language attribute + +If through analysis, the charm can be detected as being a Python based charm, then language shall be set to `python`. If not, it shall be set to `unknown`. + +When working with Python, it is possible to only publish byte-code. By doing so, troubleshooting is a harder task. Charms with Python sources delivered are preferred. + +This attribute meets the requirements to be set to `python` when: + +- the charm has a text dispatch which executes a .py +- the charm has a .py entry point +- the entry point file is executable + +## Framework attribute + + +When using `ops`, it is best to import it from a common path and not make customisation or package forks from it. If `ops` code is detected in the charm sources, this attribute's value shall be set to `operator`. If not, the charm may be using the Reactive Framework, and in this case the attribute value will be `reactive`. Else, it shall be set to `unknown`. + +This check hint meets the requirements for `ops` when: + +- language attribute is set to `python` +- the charm contains `venv/ops` +- the charm imports `ops` in the entry point + +The requirements for the Reactive Framework are: + +...or the Reactive Framework is used, if the charm... + +- has a metadata.yaml with `name` in it +- has a `reactive/.py` file that imports `charms.reactive` +- has a file name that starts with `charms.reactive-` inside the `wheelhouse` directory + + +## Juju metadata linter + +This linter verifies that the `metadata.yaml` file exists and is sane. + +The charm is considered to have a valid metadata if the following checks are true: + +- the `metadata.yaml` is present +- it is a valid YAML file +- it has at least the following fields: `name`, `summary`, and `description` + + +## Juju actions linter + +*(new in 1.4)* + +This linter verifies that the `actions.yaml` file, if exists, is a valid YAML file. The file is optional. The file contents are not verified. + + +## Juju config linter + +*(new in 1.4)* + +This linter verifies that the `config.yaml` file, if exists, is valid. This file is optional. + +If the file exists, it is considered valid if the following checks are true: + +- it has the `options` key +- it is a dictionary +- each item inside has the mandatory `type` key + +Check how to [create config.yaml and configure charms](https://discourse.charmhub.io/t/creating-config-yaml-and-configuring-charms/1039) for more information. + + +## Charm entrypoint linter + +*(new in 2.1)* + +Check the entry point is correct. Note that even if most modern charms has a typical `src/charm.py` entry point, not all charms have one, as Juju has different ways to deliver its events. + +This linter validates that, if an entry point is called from the `dispatch` file, that entry point... + +- exists +- is a file +- is executable + +The entry point content is *not* validated. diff --git a/docs/reference/charmcraft-cli.rst b/docs/reference/charmcraft-cli.rst new file mode 100644 index 000000000..8206fbf19 --- /dev/null +++ b/docs/reference/charmcraft-cli.rst @@ -0,0 +1,9 @@ +.. _charmcraft-cli: + +``charmcraft`` CLI +================== + +.. toctree:: + :maxdepth: 2 + + list-of-charmcraft-cli-commands diff --git a/docs/reference/charmcraft.md b/docs/reference/charmcraft.md new file mode 100644 index 000000000..2f91ac28c --- /dev/null +++ b/docs/reference/charmcraft.md @@ -0,0 +1,7 @@ +(charmcraft)= +# `charmcraft` + +```{toctree} +:maxdepth: 2 +changelog +``` diff --git a/docs/reference/extension.md b/docs/reference/extension.md new file mode 100644 index 000000000..1f45b5f17 --- /dev/null +++ b/docs/reference/extension.md @@ -0,0 +1,15 @@ +(extension)= +# Extension + +> See also: {ref}`How to manage extensions ` + +In the context of building a charm, in Rockcraft and Charmcraft, an **extension** is a name you can pass to the `extensions` key of a rock's `rockcraft.yaml` file / a charm's `charmcraft.yaml` file that will include the usual keys, however, customised for a particular purpose. + +An extension is usually associated with a particular {ref}`profile `. + + +```{toctree} +:maxdepth: 2 + +extensions/index +``` diff --git a/docs/reference/extensions/charmcraft-extension-django-framework.md b/docs/reference/extensions/charmcraft-extension-django-framework.md new file mode 100644 index 000000000..3574fdb58 --- /dev/null +++ b/docs/reference/extensions/charmcraft-extension-django-framework.md @@ -0,0 +1,225 @@ +(charmcraft-extension-django-framework)= +# Charmcraft extension 'django-framework' + +The `django-framework` Charmcraft {ref}`extension ` includes configuration options customised for a Django application. This document describes all the keys that a user may interact with. + +```{tip} + +**If you'd like to see the full contents contributed by this extension:**
See {ref}`How to manage extensions `. + +``` + +## Database requirement + +Django requires a database to function. When generating a new project, the default is to make use of [SQLite^](https://www.sqlite.org/). Using SQLite is not recommended for production, especially on Kubernetes deployments, because the database is not shared across units and any contents will be removed upon a new container being deployed. The `django-framework` extension therefore requires a database integration for every application, such as [PostgreSQL^](https://www.postgresql.org/) or [MySQL^](https://www.mysql.com/). See {ref}`the how-to guide ` for how to deploy a database and integrate the Django application with it. + +## `charmcraft.yaml` > `config` > `options` + +You can use the predefined options (run `charmcraft expand-extensions` for details) but also add your own, as needed. + +In the latter case, any option you define will be used to generate environment variables; a user-defined option `config-option-name` will generate an environment variable named `DJANGO_CONFIG_OPTION_NAME` where the option name is converted to upper case, dashes will be converted to underscores and the `DJANGO_` prefix will be added. + +In either case, you will be able to set it in the usual way by running `juju config
+ # (Optional): A string describing the option. Also appears on charmhub.io + description: + +If ``type`` is ``secret``, this is a string that needs to correspond to the +secret URI. + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: config: + :end-before: containers: + +.. _file-charmcraft-yaml-containers: + +``containers`` +============== + +**Status:** Required for Kubernetes charms (except for proxy charms running on +Kubernetes). + +**Purpose:** The ``containers`` key allows you to define a map of containers to be +created adjacent to the charm (as a sidecar, in the same pod). + +**Structure:** This key consists of a list of containers along with their +specification. Each container can be specified in terms of ``resource``, ``bases``, +``uid``, ``gid`` and ``mounts``, where one of either the ``resource`` or the +``bases`` subkeys must be defined, and ``mounts`` is optional. ``resource`` stands +for the OCI image resource used to create the container; to use it, specify an OCI +image resource name (that you will then define further in the `resources`_ block). +``bases`` is a list of bases to be used for resolving a container image, in descending +order of preference; to use it, specify a base name (for example, ``ubuntu``, +``centos``, ``windows``, ``osx``, ``opensuse``), a +`channel `_, and an architecture. ``mounts`` is a +list of mounted storages for this container; to use it, specify the name of the +storage to mount from the charm storage and, optionally, the location where to mount +the storage. Starting with Juju 3.5.0, ``uid`` and ``gid`` are the UID and, +respectively, GID to run the Pebble entry process for this container as; they can +be any value from 0-999 or any value from 10,000 (values from 1000-9999 are reserved +for users) and the default is 0 (root). + +.. code:: + + containers: + : + resource: + bases: + - name: + channel: + architectures: + - + mounts: + - storage: + location: + uid: + gid: + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: containers: + :end-before: description: + +.. _file-charmcraft-yaml-description: + +``description`` +=============== + +**Status:** Required if the `type`_ key is set to ``charm``. Recommended otherwise. + +**Example:** + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: description: | + :end-before: devices: + +.. _file-charmcraft-yaml-devices: + +``devices`` +=========== + +**Status:** Optional + +**Purpose:** Defines the device requests for the charm, for example a GPU. + +**Structure:** + +.. code:: + + devices: + # Each key represents the name of the device + : + # (Required) The type of device requested + type: gpu | nvidia.com/gpu | amd.com/gpu + # (Optional) Description of the requested device + description: + # (Optional) Minimum number of devices required + countmin: + # (Optional) Maximum number of devices required + countmax: + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: devices: + :end-before: extra-bindings: + +.. _file-charmcraft-yaml-extra-bindings: + +``extra-bindings`` +================== + +**Status:** Optional. + +**Purpose:** Extra bindings for the charm. For example binding extra network interfaces. + +**Structure:** A key-only map; key represents the name of the binding: + +.. code:: + + extra-bindings: + : + +**Example:** + +.. literalinclude:: charmcraft-sample-charm.yaml + :start-at: extra-bindings: + :end-before: links: + +.. _file-charmcraft-yaml-links: +.. _file-charmcraft-yaml-links-contact: +.. _file-charmcraft-yaml-links-source: +.. _file-charmcraft-yaml-links-issues: +.. _file-charmcraft-yaml-links-website: +.. _file-charmcraft-yaml-contact: +.. _file-charmcraft-yaml-documentation: + +``links`` +========= + +**Status:** Optional. Recommended. + +**Purpose:** Links to various additional information, to be displayed on Charmhub. + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: links: + :end-before: name: + +.. _file-charmcraft-yaml-name: + +``name`` +======== + +**Status:** Required if the `type`_ key is set to ``charm``. + +**Purpose:** The name of the charm. Determines the ``.charm`` file name and, if the +charm is published on Charmhub, the charm page URL in Charmhub. As a result, it also +determines the name administrators will ultimately use to deploy the charm. E.g. +``juju deploy ``. + +**Structure:** + +.. code:: + + name: + +**Example:** + +.. literalinclude:: charmcraft-sample-charm.yaml + :start-at: name: + :end-before: parts: + +.. _file-charmcraft-yaml-parts: + +``parts`` +========= + +**Status:** Recommended. + +**Purpose:** Configures the various mechanisms to obtain, process and prepare +data from different sources that end up being a part of the final charm. + +**Structure:** Mapping. Keys are user-defined part names. The value of each key +is a map where keys are part properties. + + See more: :ref:`part_properties` + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: parts: + :end-before: peers: + +.. dropdown:: Details + + .. TODO: These should be moved to their own plugin pages. + + Charmcraft offers three custom parts plugins specifically for writing charms: + + **The** ``charm`` **plugin** + + Used to pack a Charm that is based on the `Operator framework`_. + + Supports the following configuration: + + ..code:: + + parts: + my-charm: + plugin: charm + charm-entrypoint: + charm-requirements: + charm-python-packages: + charm-binary-python-packages: + prime: + + In detail: + + - ``charm-entrypoint``: The charm entry point, relative to the project directory. It is optional if not defined defaults to ``src/charm.py``. + - ``charm-requirements``: A list of requirements files specifying Python dependencies. It is optional; if not defined, defaults to a list with one ``requirements.txt`` entry if that file is present in the project directory. + - ``charm-python-packages``: A list of Python packages to install before installing requirements. These packages will be installed from sources and built locally at packing time. It is optional, defaults to empty. + - ``charm-binary-python-packages``: A list of python packages to install before installing requirements and regular Python packages. Binary packages are allowed, but they may also be installed from sources if a package is only available in source form. It is optional, defaults to empty. + + + **The** ``bundle`` **plugin** + + Used to pack a `charm bundle `_, a collection of charms which have been carefully combined and configured in order to automate a multi-charm solution. + + Supports the following configuration: + + .. code:: + + parts: + my-bundle: + plugin: bundle + + **The** ``reactive`` **plugin** + + Used to pack charms using the reactive framework. + + Note that this is a framework that has now been superseded by the `Operator framework`_. Please use that framework instead of reactive. Support for reactive in Charmcraft is only to ease the transition of old charms into the new framework. + + Supports the following configuration: + + .. code:: + + parts: + charm: + source: . + plugin: reactive + build-snaps: [charm] + reactive-charm-build-arguments: + + The ``reactive_charm_build_arguments`` allows to include extra command line arguments in the underlying ``charm build`` call. + +.. _file-charmcraft-yaml-peers: +.. _file-charmcraft-yaml-provides: +.. _file-charmcraft-yaml-requires: + +``peers``, ``provides``, ``requires`` +===================================== + + See also: `Juju | Relation (integration) `_ + +.. dropdown:: Expand to view an example featuring all three relation keys (peers, provides, or requires) + + .. code:: + + peers: + friend: + interface: life + limit: 150 + optional: true + scope: container + provides: + self: + interface: identity + requires: + parent: + interface: birth + limit: 2 + optional: false + scope: global + + +.. dropdown:: Expand to view the full schema for a chosen endpoint role (peers, provides, or requires) + + ..code:: + + : # 'peers', 'provides', or 'requires' + # Each key represents the name of the endpoint as known by this charm + : + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container + +```` +------------------- + +**Status:** If you want to define any kind of integration, required. + +**Purpose:** To define an integration endpoint. + +**Structure:** + +*Name:* Depending on what kind of an integration you are trying to define: +``peers``, ``provides``, or ``requires``. + +*Type:* Map. + +*Value:* One or more key-value pairs denoting a relation and its associated +properties. + +``.`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Status:** Required. + +**Purpose:** To define the name of the relation as known by this charm. + +**Structure:** + +*Name: User-defined.* + +*Type:* string. + +``..interface`` +````````````````````````````````````````````` + +**Status:** Required. + +**Purpose:** To define the interface schema that this relation conforms to. + +**Structure:** + +*Type:* String. + +*Value:* The name of the interface. Usually defined by the author of the charm +providing the interface. Cannot be ``juju``. Cannot begin with ``juju-``. Must +only contain characters ``a-z`` and ``-`` and cannot start with ``-``. + +.. warning:: + + The interface name is the only means of establishing whether two charms are + compatible for integration; and carries with it nothing more than a mutual + promise that the provider and requirer know the communication protocol implied + by the name. + +``..limit`` +````````````````````````````````````````` + +**Status:** Optional. + +**Purpose:** To define the maximum number of supported connections to this relation +endpoint. + +**Structure:** + +*Type:* Integer. + +``..optional`` +```````````````````````````````````````````` + +**Status:** Optional. + +**Purpose:** To define if the relation is required. Informational only. + +**Structure:** + +*Type:* Boolean. + +*Default value:* ``false`` + +``..scope`` +````````````````````````````````````````` + +**Status:** Optional. + +**Purpose:** To define the scope of the relation, that is, the set of units from +integrated applications that are reported to the unit as members of the integration. + +**Structure:** + +*Type:* String. + +**Possible values:** ``container``, ``global``. + +Container-scoped integrations are restricted to reporting details of a single +principal unit to a single subordinate, and vice versa, while global integrations +consider all possible remote units. Subordinate charms are only valid if they have +at least one ``requires`` integration with ``container`` scope. + +**Default value:** ``global``. + + +.. _file-charmcraft-yaml-resources: + +``resources`` +============= + + See first: `Juju | Charm resource `_ + +**Status:** Optional. + +**Purpose:** To define a `resource `_ for your charm. + +.. note:: + Kubernetes charms must declare an ``oci-image`` resource for each container + they define in the `containers`_ mapping. + +**Structure:** + +.. code:: + + resources: + : + type: or + description: + filename: + +.. dropdown:: File resource example + + + .. code:: + + resources: + water: + type: file + filename: /dev/h2o + +.. dropdown:: OCI image example + + .. code:: + + resources: + super-app-image: + type: oci-image + description: OCI image for the Super App (hub.docker.com/_/super-app) + + +.. _file-charmcraft-yaml-storage: + +``storage`` +=========== + +**Status:** Optional. + +**Purpose:** Storage requests for the charm. + +**Structure:** + +.. code:: + + storage: + # Each key represents the name of the storage + : + + # (Required) Type of the requested storage + # The filesystem type requests a directory in which the charm may store files. + # If the storage provider only supports provisioning disks, then a disk will be + # created, attached, partitiioned, and a filesystem created on top, and the + # filesystem will be presented to the charm as normal. + # The block type requests a raw block device, typically disks or logical volumes. + type: filesystem | block + + # (Optional) Description of the storage requested + description: + + # (Optional) The mount location for filesystem stores. For multi-stores + # the location acts as the parent directory for each mounted store. + location: + + # (Optional) Indicates if all units of the application share the storage + shared: true | false + + # (Optional) Indicates if the storage should be made read-only (where possible) + read-only: true | false + + # (Optional) The number of storage instances to be requested + multiple: + range: | - | - | + + + # (Optional) Minimum size of requested storage in forms G, GiB, GB. Size + # multipliers are M, G, T, P, E, Z or Y. With no multiplier supplied, M + # is implied. + minimum-size: | + + # (Optional) List of properties, only supported value is "transient" + properties: + - transient + + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: storage: # Possible storage for the charm + :end-before: subordinate: + +.. _file-charmcraft-yaml-subordinate: + +``subordinate`` +=============== + +**Status:** Optional. + +**Purpose:** Configures whether the charm is meant to be deployed as a subordinate +to a principal charm. + +**Structure:** + +.. code:: + + subordinate: true | false + +**Example:** + +.. code:: + + subordinate: false + +.. _file-charmcraft-yaml-summary: + +``summary`` +=========== + +**Status:** Required if the :ref:`file-charmcraft-yaml-type` key is set to ``charm``. + +**Structure:** A short, one-line description of the charm. No more than 78 characters. + +.. dropdown:: Example + + .. code:: + + summary: A Juju charm to run a Traefik-powered ingress controller on Kubernetes. + +.. _file-charmcraft-yaml-terms: + +``terms`` +========= + +**Status:** Optional. + +**Purpose:** Lists the terms that any charm user agree to when they're using the charm. + +**Structure:** The list of terms: + +.. code:: + + terms: + - + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: terms: + :end-before: title: My awesome charm + +.. _file-charmcraft-yaml-title: + +``title`` +========= + +**Status:** Optional, but recommended + +**Purpose:** A human-readable name for your charm + +.. dropdown:: Example + + .. literalinclude:: charmcraft-sample-charm.yaml + :start-at: title: + :end-before: type: + +.. _file-charmcraft-yaml-type: + +``type`` +======== + +**Status:** Required. + +**Purpose:** Indicates whether charmcraft will pack a charm or a bundle. + +**Structure:** + +**Type:** String. + +**Value:** ``charm`` or ``bundle``. + +.. dropdown:: Example + + .. code:: + + type: charm + +.. _juju-bundles: https://juju.is/docs/olm/bundles +.. _juju-configuration: https://juju.is/docs/juju/configuration +.. _juju-resource: https://juju.is/docs/juju/charm-resource diff --git a/docs/reference/files/file-config-yaml.rst b/docs/reference/files/file-config-yaml.rst new file mode 100644 index 000000000..7526e437a --- /dev/null +++ b/docs/reference/files/file-config-yaml.rst @@ -0,0 +1,75 @@ +.. _file-config-yaml: + +.. highlight:: yaml + +``config.yaml`` +*************** + +.. important:: + Starting with Charmcraft 2.5, this file is created automatically from information + you provide in :ref:`charmcraft.yaml`. For backwards + compatibility, Charmcraft will continue to allow the use of this file, but you may + not duplicate keys across the two files. + +The ``config.yaml`` in a charm's root directory is an optional file that may be used +to define the configuration options supported by a charm. + +The definitions are collected under a single YAML map called ``options``. The rest of +this doc gives details about this map. + +``options`` +=========== + +**Status:** Required if the file exists. + +**Purpose:** The ``options`` key allows charm authors to declare the configuration +options that they have defined for a charm. + +**Structure:** The key contains a definition block for each option, where each +definition consists of a charm-author-defined option name and an option description, +given in 3 fields -- type, description, and default value: + +.. code:: + + options: + + +# (Required) A full description of the configuration layer +description: | + + +# (Optional) A list of maintainers in the format "First Last " +maintainers: + - + +# (Optional) A string (or a list of strings) containing a link (or links) to project websites. +# In general this is likely to be the upstream project website, or the formal website for the +# charmed offering. +website: | [] + +# (Optional) A string (or a list of strings) containing a link (or links) to the charm source code. +source: | [] + +# (Optional) A string (or a list of strings) containing a link (or links) to the charm bug tracker. +issues: | [] + +# (Optional) A link to a documentation cover page on Discourse +# More details at https://juju.is/docs/sdk/add-docs-to-your-charmhub-page +docs: + +# (Optional) A list of terms that any charm user must agree with +terms: + - + +# (Optional) True if the charm is meant to be deployed as a subordinate to a +# principal charm +subordinate: true | false + +# (Optional) A map of containers to be created adjacent to the charm. This field +# is required when the charm is targeting Kubernetes, where each of the specified +# containers will be created as sidecars to the charm in the same pod. +containers: + # Each key represents the name of the container + : + # Note: One of either resource or bases must be specified. + + # (Optional) Reference for an entry in the resources field. Specifies + # the oci-image resource used to create the container. Must not be + # present if a base/channel is specified + resource: + + # (Optional) A list of bases in descending order of preference for use + # in resolving a container image. Must not be present if resource is + # specified. These bases are listed as base (instead of name) and + # channel as in the Base definition, as an unnamed top-level object list + bases: + # Name of the OS. For example ubuntu/centos/windows/osx/opensuse + - name: + + # Channel of the OS in format "track[/risk][/branch]" such as used by + # Snaps. For example 20.04/stable or 18.04/stable/fips + channel: + + # List of architectures that this particular charm can run on + architectures: + - + + # (Optional) List of mounted storages for this container + mounts: + # (Required) Name of the storage to mount from the charm storage + - storage: + + # (Optional) In the case of filesystem storages, the location to + # mount the storage. For multi-stores, the location acts as the + # parent directory for each mounted store. + location: + + # (Optional) UID to run the pebble entry process for this container as. + # Can be any value from 0-999 or any value from 10,000 (values from 1000-9999 are reserved for users). + # Default is 0 (root). Added in Juju 3.5.0. + uid: + + # (Optional) GID to run the pebble entry process for this container as. + # Can be any value from 0-999 or any value from 10,000 (values from 1000-9999 are reserved for user's primary groups). + # Default is 0 (root). Added in Juju 3.5.0. + gid: + +# (Optional) Additional resources that accompany the charm +resources: + # Each key represents the name of the resource + : + + # (Required) The type of the resource + type: file | oci-image + + # (Optional) Description of the resource and its purpose + description: + + # (Required: when type:file) The filename of the resource as it should + # appear in the filesystem + filename: + +# (Optional) Map of relations provided by this charm +provides: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container + +# (Optional) Map of relations required by this charm +requires: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container + +# (Optional) Mutual relations between units/peers of this charm +peers: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container + +# (Optional) Storage requests for the charm +storage: + # Each key represents the name of the storage + : + + # (Required) Type of the requested storage + type: filesystem | block + + # (Optional) Description of the storage requested + description: + + # (Optional) The mount location for filesystem stores. For multi-stores + # the location acts as the parent directory for each mounted store. + location: + + # (Optional) Indicates if all units of the application share the storage + shared: true | false + + # (Optional) Indicates if the storage should be made read-only (where possible) + read-only: true | false + + # (Optional) The number of storage instances to be requested + multiple: | - | - | + + + # (Optional) Minimum size of requested storage in forms G, GiB, GB. Size + # multipliers are M, G, T, P, E, Z or Y. With no multiplier supplied, M + # is implied. + minimum-size: | + + # (Optional) List of properties, only supported value is "transient" + properties: + - transient + +# (Optional) Device requests for the charm, for example a GPU +devices: + # Each key represents the name of the device + : + + # (Required) The type of device requested + type: gpu | nvidia.com/gpu | amd.com/gpu + + # (Optional) Description of the requested device + description: + + # (Optional) Minimum number of devices required + countmin: + + # (Optional) Maximum number of devices required + countmax: + +# (Optional) Extra bindings for the charm. For example binding extra network +# interfaces. Key only map, value must be blank. Key represents the name +extra-bindings: + # Key only map; key represents the name of the binding + : + +# (Optional) A set of features that must be provided by the Juju model to ensure that the charm can be successfully deployed. +# See https://juju.is/docs/olm/supported-features for the full list. +assumes: + - + - any-of: + - + - + - all-of: + - + - + +# (Optional) What kind of user is required to run the charm code. +# It can be one of root, sudoer or non-root. +# Added in Juju 3.6.0. If not specified, root is assumed. +charm-user: +``` + + +``` +---------------------------- + + +**Contents:** + +- [`assumes`](#heading--assumes) +- [`charm-user`](#heading--charm-user) +- [`containers`](#heading--containers) +- [`description`](#heading--description) +- [`devices`](#heading--devices) +- [`display-name`](#heading--display-name) +- [`docs`](#heading--docs) +- [`extra-bindings`](#heading--extra-bindings) +- [`issues`](#heading--issues) +- [`maintainers`](#heading--maintainers) +- [`name`](#heading--name) +- [`peer`](#heading--peer) +- [`provides`](#heading--provides) +- [`requires`](#heading--requires) +- [`resources`](#heading--resources) +- [`source`](#heading--source) +- [`storage`](#heading--storage) +- [`subordinate`](#heading--subordinate) +- [`summary`](#heading--summary) +- [`terms`](#heading--terms) +- [`website`](#heading--website) +- [``](#heading--other-arbitrary-keywords) + + +

`assumes`

+ + + +**Status:** Optional. Recommended for Kubernetes charms. + +**Purpose:** The `assumes` key allows charm authors to explicitly state in the metadata of a charm various features that a Juju model must be able to provide to ensure that the charm can be successfully deployed on it. When a charm comes preloaded with such requirements, this enables Juju to perform a pre-deployment check and to display user-friendly error messages if a feature requirement cannot be met by the model that the user is trying to deploy the charm to. If the assumes section of the charm metadata is omitted, Juju will make a best-effort attempt to deploy the charm, and users must rely on the output of `juju status` to figure out whether the deployment was successful. The `assumes` key is available since 2.9.23. + +**Structure:** The key consists of a list of features that can be given either directly or, depending on the complexity of the condition you want to enforce, nested under one or both of the boolean expressions `any-of` or `all-of`, as shown below. In order for a charm to be deployed, all entries in the `assumes` block must be satisfied. + +```yaml +assumes: + - + - any-of: + - + - + - all-of: + - + - +``` + + The supported feature names are as below: + +|||| +|-|-|-| +|`juju `

E.g., `juju < 3.0`.
E.g., juju >= 2.9` | The charm deploys if and only if the model runs agent binaries with the specified Juju version(s). |Since 2.9.23| +|`k8s-api` | The charm deploys if and only if the underlying substrate for the model is Kubernetes. |Since 2.9.23| + +The Boolean expressions are defined as below: + +||| +|-|-| +|`any-of`| The sub-expression is satisfied if any of the provided child expressions is satisfied.| +|`all-of` | The sub-expression is satisfied if all of the provided child expressions are satisfied.| + +**Examples:** + +---- +```{dropdown} Expand to see a simple example + + +```yaml +assumes: + - juju >= 2.9.23 + - k8s-api +``` + +``` + +------------- + +```{dropdown} Expand to see an example with a nested expression + +```yaml +assumes: + - any_of: + - juju >= 2.9 + - k8s-api +``` + +``` + +------------- + +

`charm-user`

+ +```{important} + +`charm-user` was added in Juju 3.6.0. Currently is only supported on Kubernetes charms and has no effect on machine charms. + +``` + +**Status:** Optional. Recommended for Kubernetes charms. + +**Purpose:** The `charm-user` key allows charm authors to specify that their charm hook code does not need to be run as root. This key, in addition to `uid` + `uid` fields in `containers`, allows charms to be run rootless. The value of `root` ensures the charm runs as root. Both `sudoer` and `non-root` will run as a user other than root. In the case of the value `sudoer`, the charm will be run as a user with access to sudo to elevate it's privileges. + +**Structure:** The key consists of a single value. One of `root`, `sudoer` or `non-root`. + +```yaml +# (Optional) What kind of user is required to run the charm code. +# It can be one of root, sudoer or non-root. +# Added in Juju 3.6.0. If not specified, root is assumed. +charm-user: +``` + +--------- + + +

`containers`

+> See also: [`resources`](#heading--resources) + +**Status:** Required for Kubernetes charms (except for proxy charms running on Kubernetes). + +**Purpose:** The `containers` key allows you to define a map of containers to be created adjacent to the charm (as a sidecar, in the same pod). + +**Structure:** This key consists of a list of containers along with their specification. Each container can be specified in terms of `resource`, `bases`, `uid`, `gid` and `mounts`, where one of either the `resource` or the `bases` subkeys must be defined, and `mounts` is optional. `resource` stands for the OCI image resource used to create the container; to use it, specify an OCI image resource name (that you will then define further in the [`resources`](#heading--resources) block). `bases` is a list of bases to be used for resolving a container image, in descending order of preference; to use it, specify a base name (for example, `ubuntu`, `centos`, `windows`, `osx`, `opensuse`), a [channel](https://snapcraft.io/docs/channels), and an architecture. `mounts` is a list of mounted storages for this container; to use it, specify the name of the storage to mount from the charm storage and, optionally, the location where to mount the storage. And, starting with Juju 3.5.0, `uid` and `gid` are the UID and, respectively, GID to run the Pebble entry process for this container as; they can be any value from 0-999 or any value from 10,000 (values from 1000-9999 are reserved for users) and the default is 0 (root). + +```yaml +containers: + : + resource: + bases: + - name: + channel: + architectures: + - + mounts: + - storage: + location: + uid: + gid: +``` + + + +**Examples:** + +```{dropdown} Expand to see an example with `resource` and `mounts` + +```yaml +containers: + super-app: + resource: super-app-image + mounts: + - storage: logs + location: /logs +``` + +``` + + +

`description`

+ +**Status:** Required. + +**Purpose:** The `description` key is where you provide a full description of the configuration layer. + +**Structure:** + +```yaml +description: | + +``` + + + +

`devices`

+ +```yaml + +# (Optional) Device requests for the charm, for example a GPU +devices: + # Each key represents the name of the device + : + + # (Required) The type of device requested + type: gpu | nvidia.com/gpu | amd.com/gpu + + # (Optional) Description of the requested device + description: + + # (Optional) Minimum number of devices required + countmin: + + # (Optional) Maximum number of devices required + countmax: + +``` + +

`display-name`

+ +```{important} + +In [`charmcraft.yaml` ` this is now the `title` key. + +``` + +```yaml +display-name: | + +``` + +

`docs`

+> See also: {ref}`How to create an effective README file for your charm ` + +```{important} + +In {ref}``charmcraft.yaml` ` this is now the `documentation` subkey under the `links` key. + +``` + + +```yaml +# (Optional) A link to a documentation cover page on Discourse: +docs: +``` + +

`extra-bindings`

+ +```yaml +# (Optional) Extra bindings for the charm. For example binding extra network +# interfaces. Key only map, value must be blank. Key represents the name. +extra-bindings: + # Key only map; key represents the name of the binding + : +``` + +

`issues`

+ +```{important} + +In {ref}``charmcraft.yaml` ` this is now under the `links` key. + +``` + + +```yaml +# (Optional) A string (or a list of strings) containing a link (or links) to the charm bug tracker. +issues: | {ref}`] +``` +

`maintainers`

+ +```{important} + +In [`charmcraft.yaml` ` this is now the `contact` subkey under the `links` key. + +``` + + +```yaml +# (Optional) A list of maintainers in the format "First Last " +maintainers: + - +``` + +

`name`

+ +```yaml +# (Required) The name of the charm. Determines URL in Charmhub and the name administrators +# will ultimately use to deploy the charm. E.g. `juju deploy ` +name: +``` + +

`peer`

+ +```yaml +# (Optional) Mutual relations between units/peers of this charm +peer: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container +``` + +

`provides`

+ +```yaml +# (Optional) Map of relations provided by this charm +provides: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container +``` + +

`requires`

+ +```yaml +# (Optional) Map of relations required by this charm +requires: + # Each key represents the name of the relation as known by this charm + : + + # (Required) The interface schema that this relation conforms to + interface: + + # (Optional) Maximum number of supported connections to this relation + # endpoint. This field is an integer + limit: + + # (Optional) Defines if the relation is required. Informational only. + optional: true | false + + # (Optional) The scope of the relation. Defaults to "global" + scope: global | container + +``` + +

`resources`

+> See also: {ref}`Resource <5213md>` + + +**Purpose:** The `resources` key is where you define the resources mentioned under the `resource` key of the [`containers`](#heading--containers) key. + +**Structure:** + + + +```yaml +# (Optional) Additional resources that accompany the charm +resources: + # Each key represents the name of a resource + # mentioned in the 'resource' subkey of the 'containers' key. + : + + # (Required) The type of the resource + type: file | oci-image + + # (Optional) Description of the resource and its purpose + description: + + # (Required: when type:file) The filename of the resource as it should + # appear in the filesystem + filename: +``` + +**Examples:** + +------ +```{dropdown} Expand to see an example with an OCI-image resource + +```yaml +resources: + super-app-image: + type: oci-image + description: OCI image for the Super App (hub.docker.com/_/super-app) +``` + +``` +---- + +

`source`

+ +```{important} + +In {ref}``charmcraft.yaml` ` this is now under the `links` key. + +``` + + +```yaml +# (Optional) A string (or a list of strings) containing a link (or links) to the charm source code. +source: | {ref}`] +``` + +

`storage`

+ +```yaml +# (Optional) Storage requests for the charm +storage: + # Each key represents the name of the storage + : + + # (Required) Type of the requested storage + type: filesystem | block + + # (Optional) Description of the storage requested + description: + + # (Optional) The mount location for filesystem stores. For multi-stores + # the location acts as the parent directory for each mounted store. + location: + + # (Optional) Indicates if all units of the application share the storage + shared: true | false + + # (Optional) Indicates if the storage should be made read-only (where possible) + read-only: true | false + + # (Optional) The number of storage instances to be requested + multiple: + range: | - | - | + + + # (Optional) Minimum size of requested storage in forms G, GiB, GB. Size + # multipliers are M, G, T, P, E, Z or Y. With no multiplier supplied, M + # is implied. + minimum-size: | + + # (Optional) List of properties, only supported value is "transient" + properties: + - transient +``` + + +

`subordinate`

+ +```yaml +# (Optional) True if the charm is meant to be deployed as a subordinate to a +# principal charm +subordinate: true | false +``` + +

`summary`

+ +```yaml +# (Required) A short, one-line description of the charm +summary: +``` + +

`terms`

+ +```yaml +# (Optional) A list of terms that any charm user must agree with +terms: + - +``` + +

`website`

+ +```{important} + +In [`charmcraft.yaml` ` this is now under the `links` key. + +``` + + +```yaml +# (Optional) A string (or a list of strings) containing a link (or links) to project websites. +# In general this is likely to be the upstream project website, or the formal website for the +# charmed offering. +website: | [] +``` + +

``

+ +In addition to the official fields and keywords mentioned above, a `metadata.yaml` file may also contain other arbitrary keywords. These can serve to keep track of other choices a charmer might make. In some cases these become semi-official, being adopted by many charmers and even incorporated into CI processes. An example is [`upstream-source`](https://github.com/canonical/charmcraft/blob/b22fcdba3b894004468abfbf45caa54d93fbf7d0/charmcraft/templates/init-simple/metadata.yaml.j2#L40-L43). diff --git a/docs/reference/files/file-pyproject-toml.md b/docs/reference/files/file-pyproject-toml.md new file mode 100644 index 000000000..e6327840e --- /dev/null +++ b/docs/reference/files/file-pyproject-toml.md @@ -0,0 +1,56 @@ +(file-pyproject-toml)= +# File 'pyproject.toml' + +The `pyproject.toml` file in your charm's root directory is a typical Python `pyproject.toml` file. + +> See more: [`pip` | `pyproject.toml`](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) + +This file is generated automatically by `charmcraft init` with the contents below: + +```text +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Formatting tools configuration +[tool.black] +line-length = 99 +target-version = ["py38"] + +# Linting tools configuration +[tool.ruff] +line-length = 99 +select = ["E", "W", "F", "C", "N", "D", "I001"] +extend-ignore = [ + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +ignore = ["E501", "D107"] +extend-exclude = ["__pycache__", "*.egg_info"] +per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] +``` diff --git a/docs/reference/files/file-readme-md.md b/docs/reference/files/file-readme-md.md new file mode 100644 index 000000000..14f0159c4 --- /dev/null +++ b/docs/reference/files/file-readme-md.md @@ -0,0 +1,7 @@ +(file-readme-md)= +# File 'README.md' + + > {ref}`List of files in the charm project ` > `README.md` + + +The `README.md` file should describe the charm’s behaviour and provide instructions on to deploy the charm as well as links to resources for the supported application. diff --git a/docs/reference/files/file-requirements-dev-txt.md b/docs/reference/files/file-requirements-dev-txt.md new file mode 100644 index 000000000..466594651 --- /dev/null +++ b/docs/reference/files/file-requirements-dev-txt.md @@ -0,0 +1,6 @@ +(file-requirements-dev-txt)= +# File 'requirements-dev.txt' + +> {ref}`List of files in the charm project ` > `requirements-dev.txt` + +The `requirements-dev.txt` is a standard Python [requirements file](https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format) that specifies only those dependencies that are used during development. Examples of this might include testing libraries, linters, etc. These dependencies will not be bundled with the charm when it is built. diff --git a/docs/reference/files/file-requirements-txt.md b/docs/reference/files/file-requirements-txt.md new file mode 100644 index 000000000..e1ab41fdb --- /dev/null +++ b/docs/reference/files/file-requirements-txt.md @@ -0,0 +1,8 @@ +(file-requirements-txt)= +# File 'requirements.txt' + +> {ref}`List of files in the charm project ` > `requirements.txt` + + + +The `requirements.txt` file is a standard Python [requirements file](https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format) used to declare and pin the version of any Python libraries required by a charm in production. This will be pre-populated with {ref}``ops` `. Any dependencies specified here will be bundled with the charm when it is built with {ref}``charmcraft pack` `. diff --git a/docs/reference/files/file-src-charm-py.md b/docs/reference/files/file-src-charm-py.md new file mode 100644 index 000000000..c66629bfd --- /dev/null +++ b/docs/reference/files/file-src-charm-py.md @@ -0,0 +1,7 @@ +(file-src-charm-py)= +# File 'src/charm.py' + +> {ref}`List of files in the charm project ` > `src/charm.py` + + +The `src/charm.py` is the default entry point for a charm. This file must be executable, and should include a {ref}`shebang <7150md>`>) to indicate the desired interpreter. For many charms, this file will contain the majority of the charm code. It is possible to change the name of this file, but additional changes are then required to enable the charm to be built with {ref}``charmcraft` `. diff --git a/docs/reference/files/file-tests-integration-test-charm-py.md b/docs/reference/files/file-tests-integration-test-charm-py.md new file mode 100644 index 000000000..43b3065e2 --- /dev/null +++ b/docs/reference/files/file-tests-integration-test-charm-py.md @@ -0,0 +1,48 @@ +(file-tests-integration-test-charm-py)= +# File ‘tests/integration/test_charm.py’ + +The `tests/integration/test_charm.py` file is the companion to `src/charm.py` for integration testing. + +This file is created automatically by `charmcraft init` and it is pre-populated with standard constructs used by `pytest-operator`, similar to the below: + +```text +#!/usr/bin/env python3 +# Copyright 2023 Ubuntu +# See LICENSE file for licensing details. + +import asyncio +import logging +from pathlib import Path + +import pytest +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +APP_NAME = METADATA["name"] + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest): + """Build the charm-under-test and deploy it together with related charms. + + Assert on the unit status before any relations/configurations take place. + """ + # Build and deploy charm from local source folder + charm = await ops_test.build_charm(".") + resources = { + "some-container-image": METADATA["resources"]["some-container-image"]["upstream-source"] + } + + # Deploy the charm and wait for active/idle status + await asyncio.gather( + ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME), + ops_test.model.wait_for_idle( + apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + + +``` diff --git a/docs/reference/files/file-tests-unit-test-charm-py.md b/docs/reference/files/file-tests-unit-test-charm-py.md new file mode 100644 index 000000000..afce39153 --- /dev/null +++ b/docs/reference/files/file-tests-unit-test-charm-py.md @@ -0,0 +1,37 @@ +(file-tests-unit-test-charm-py)= +# File 'tests/unit/test_charm.py' + +> {ref}`List of files in the charm project ` > `tests/test_charm.py` +> +> See also: {ref}`How to test a charm ` + +The `tests/unit/test_charm.py` file is the companion to `src/charm.py` for unit testing. It is pre-populated with standard constructs used by `unittest` and Harness. + +This file is created automatically by `charmcraft init` and it is pre-populated with standard constructs used by `unittest` and `Harness`, along the lines below: + +```text + +# Copyright 2023 Ubuntu +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + +import unittest + +import ops +import ops.testing +from charm import MyK8SCharmCharm + + +class TestCharm(unittest.TestCase): + def setUp(self): + self.harness = ops.testing.Harness(MyK8SCharmCharm) + self.addCleanup(self.harness.cleanup) + self.harness.begin() + + def test_pebble_ready(self): + # Simulate the container coming up and emission of pebble-ready event + self.harness.container_pebble_ready("some-container") + # Ensure we set an ActiveStatus with no message + self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus()) +``` diff --git a/docs/reference/files/file-tox-ini.md b/docs/reference/files/file-tox-ini.md new file mode 100644 index 000000000..fb740a92d --- /dev/null +++ b/docs/reference/files/file-tox-ini.md @@ -0,0 +1,97 @@ +(file-tox-ini)= +# File 'tox.ini' + +The `tox.ini` file in your charm’s root directory is a typical Tox configuration file. + +> See more: [Tox | Configuration](https://tox.wiki/en/latest/user_guide.html#configuration) + +This file is generated automatically by `charmcraft init` with the contents below: + +```text + Copyright 2023 Ubuntu +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static, unit +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +tests_path = {tox_root}/tests +;lib_path = {tox_root}/lib/charms/operator_name_with_underscores +all_path = {[vars]src_path} {[vars]tests_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + black + ruff +commands = + black {[vars]all_path} + ruff --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + black + ruff + codespell +commands = + # if this charm owns a lib, uncomment "lib_path" variable + # and uncomment the following line + # codespell {[vars]lib_path} + codespell {tox_root} + ruff {[vars]all_path} + black --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration +``` diff --git a/docs/reference/files/index.rst b/docs/reference/files/index.rst new file mode 100644 index 000000000..7724f563a --- /dev/null +++ b/docs/reference/files/index.rst @@ -0,0 +1,31 @@ +.. _list-of-files-in-a-charmcraft-project: +.. _list-of-files-in-a-charm-or-a-bundle: + +List of files in a Charmcraft project +===================================== + + +.. toctree:: + :maxdepth: 1 + + + file-bundle-yaml + file-actions-yaml + file-charmcraft-yaml + file-config-yaml + file-contributing-md + file-dispatch + file-icon-svg + file-libname-py + file-license + file-lxd-profile-yaml + file-manifest-yaml + file-metadata-yaml + file-pyproject-toml + file-readme-md + file-requirements-dev-txt + file-requirements-txt + file-src-charm-py + file-tests-unit-test-charm-py + file-tests-integration-test-charm-py + file-tox-ini diff --git a/docs/reference/index.rst b/docs/reference/index.rst index ccb31aa63..f8e277e7d 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -3,15 +3,14 @@ Reference ********* -Charmcraft is a part of the `Juju Charm SDK `_. This -documentation only contains references about Charmcraft itself. Overall SDK reference -data can be found in the `Charm SDK docs `_. - .. toctree:: :maxdepth: 2 - commands - models/index - parts - plugins/index - changelog + charmcraft + charmcraft-analyzers-and-linters + charmcraft-cli + extension + file + part + profile + diff --git a/docs/explanation/lifecycle.rst b/docs/reference/lifecycle.rst similarity index 99% rename from docs/explanation/lifecycle.rst rename to docs/reference/lifecycle.rst index eb45430ef..9bd1a44fb 100644 --- a/docs/explanation/lifecycle.rst +++ b/docs/reference/lifecycle.rst @@ -43,3 +43,4 @@ Further Information ------------------- Further information can be found in the `Craft-parts`_ documentation. + diff --git a/docs/reference/commands.rst b/docs/reference/list-of-charmcraft-cli-commands.rst similarity index 92% rename from docs/reference/commands.rst rename to docs/reference/list-of-charmcraft-cli-commands.rst index e03b4e629..199942f78 100644 --- a/docs/reference/commands.rst +++ b/docs/reference/list-of-charmcraft-cli-commands.rst @@ -1,7 +1,7 @@ .. _commands: -Commands -******** +List of ``charmcraft`` CLI commands +*********************************** .. Use a hidden table of contents to ensure that documentation is read. @@ -48,3 +48,4 @@ Other Commands ============== .. include:: commands/other-commands.rst + diff --git a/docs/reference/parts.rst b/docs/reference/part.rst similarity index 75% rename from docs/reference/parts.rst rename to docs/reference/part.rst index 4ea8ae85e..c661baaaa 100644 --- a/docs/reference/parts.rst +++ b/docs/reference/part.rst @@ -1,7 +1,10 @@ +.. _part: .. _parts: -Parts -***** +**** +Part +**** + Parts, powered by :external+craft-parts:ref:`craft-parts `, power the build system that charmcraft uses. @@ -12,3 +15,6 @@ system that charmcraft uses. /common/craft-parts/reference/part_properties /common/craft-parts/reference/parts_steps /common/craft-parts/reference/step_execution_environment + /common/craft-parts/explanation/filesets + lifecycle + plugins/index diff --git a/docs/reference/plugins/index.rst b/docs/reference/plugins/index.rst index dce9a13a3..60b739c5f 100644 --- a/docs/reference/plugins/index.rst +++ b/docs/reference/plugins/index.rst @@ -1,7 +1,7 @@ .. _plugins: -Parts plugins -************* +List of part plugins +******************** Most charms only need one, maybe two parts, typically consisting of one of Charmcraft's application-specific plugins such as the `charm plugin`_ or the `reactive plugin`_ and @@ -11,6 +11,7 @@ potentially the addition of further files using the :ref:`craft_parts_dump_plugi :maxdepth: 1 /common/craft-parts/reference/plugins/dump_plugin + /common/craft-parts/explanation/dump_plugin /common/craft-parts/reference/plugins/nil_plugin python_plugin poetry_plugin diff --git a/docs/reference/profile.md b/docs/reference/profile.md new file mode 100644 index 000000000..1bf6b8e2c --- /dev/null +++ b/docs/reference/profile.md @@ -0,0 +1,8 @@ +(profile)= +# Profile + +In the context of building a charm, in Rockcraft and Charmcraft, a **profile** is a name you can pass as an argument during rock / charm initialisation that will create all the usual rock/charm project files in a form customised for a specific purpose -- i.e., for a Kubernetes charm, for a Kubernetes charm for a Flask application etc. -- in order to speed up development. + +The customisation often takes the shape of a specific {ref}`extension ` in the charm's `charmcraft.yaml` file. + +> See more: {ref}`How to set up a charm project ` \ No newline at end of file diff --git a/docs/reuse/links.txt b/docs/reuse/links.txt index c6cf96137..dd1e4374c 100644 --- a/docs/reuse/links.txt +++ b/docs/reuse/links.txt @@ -1,8 +1,11 @@ .. _Charmcraft: https://canonical-charmcraft.readthedocs-hosted.com +.. _Charmhub: https://charmhub.io .. _Chisel: https://github.com/canonical/chisel .. _`Chisel releases`: https://github.com/canonical/chisel-releases .. _`Craft-parts`: https://canonical-craft-parts.readthedocs-hosted.com +.. _Discourse: https://discourse.charmhub.io/ .. _Docker: https://docs.docker.com/ +.. _Juju: https://juju.is/docs .. _`feature request`: https://github.com/canonical/charmcraft/issues/new?assignees=&labels=Enhancement&projects=&template=task.yaml .. _`OCI archive format`: https://github.com/opencontainers/image-spec/blob/main/layer.md#distributable-format .. _OCI_image_spec: https://github.com/opencontainers/image-spec/blob/main/spec.md diff --git a/docs/tutorial/flask.rst b/docs/tutorial/flask.rst index 37bfcc6bc..d17feca05 100644 --- a/docs/tutorial/flask.rst +++ b/docs/tutorial/flask.rst @@ -167,7 +167,8 @@ deployed in the Kubernetes cluster: .. seealso:: - See more: `skopeo `_ + See more: `Ubuntu manpage | skopeo + `_ Create the charm ================ @@ -520,4 +521,3 @@ Next steps - `SDK Explanation docs `_ ------------------------- - diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 3965c1272..615f25a43 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -1,10 +1,20 @@ .. _tutorial: Tutorial -******** +======== -.. toctree:: - :maxdepth: 2 +Our tutorial comes in multiple flavours -- pick your flavour of choice! + +.. note:: - flask + Currently all our tutorial flavours demonstrate ``charmcraft`` in combination + with an 12-factor app extension -- a journey that does not require any + `ops `_ and which does not currently include publishing the charm. To get a sense of the workflow for other types of applications and including the publication step, see :ref:`manage-charms`. + +.. toctree:: + :maxdepth: 2 + write-your-first-kubernetes-charm-for-a-django-app + flask + write-your-first-kubernetes-charm-for-a-fastapi-app + write-your-first-kubernetes-charm-for-a-go-app diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.md b/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.md new file mode 100644 index 000000000..2ec4bce77 --- /dev/null +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.md @@ -0,0 +1,567 @@ +(write-your-first-kubernetes-charm-for-a-django-app)= +# Write your first Kubernetes charm for a Django app + + +**What you’ll need:** + +- A working station, e.g., a laptop, with amd64 architecture which has sufficient resources to launch a virtual machine with 4 CPUs, 4 GB RAM, and a 50 GB disk + - Note that a workstation with arm64 architecture can complete the majority of this tutorial. +- Familiarity with Linux. +- About an hour of free time. + +**What you’ll do:** + +Create a Django application. Use that to create a rock with `rockcraft`. Use that to create a charm with `charmcraft`. Use that to test-deploy, configure, etc., your Django application on a local Kubernetes cloud, `microk8s`, with `juju`. All of that multiple times, mimicking a real development process. + +```{note} + +**rock** + +An Ubuntu LTS-based OCI compatible container image designed to meet security, stability, and reliability requirements for cloud-native software. + +**charm** + +A package consisting of YAML files + Python code that will automate every aspect of an application's lifecycle so it can be easily orchestrated with Juju. + +**`juju`** + +An orchestration engine for charmed applications. + +``` + + +```{important} + +**Should you get stuck or notice issues:** Please get in touch on [Matrix](https://matrix.to/#/#12-factor-charms:ubuntu.com) or [Discourse](https://discourse.charmhub.io/). + +``` + +## Set things up + +Install Multipass. + +> See more: [Multipass | How to install Multipass](https://multipass.run/docs/install-multipass) + +Use Multipass to launch an Ubuntu VM with the name `charm-dev` from the 22.04 blueprint: + +```bash +multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 22.04 +``` + +Once the VM is up, open a shell into it: + +```bash +multipass shell charm-dev +``` + +In order to create the rock, you'll need to install Rockcraft: + +```bash +sudo snap install rockcraft --classic +``` + +`LXD` will be required for building the rock. Make sure it is installed and initialised: + +```bash +sudo snap install lxd +lxd init --auto +``` + +In order to create the charm, you'll need to install Charmcraft: + +```bash +sudo snap install charmcraft --channel latest/edge --classic +``` + +```{note} + +This tutorial requires version `3.2.0` or later of Charmcraft. Check the version of Charmcraft using `charmcraft --version` If you have an older version of Charmcraft installed, use `sudo snap refresh charmcraft --channel latest/edge` to get the latest edge version of Charmcraft. + +``` + +MicroK8s is required to deploy the Django application on Kubernetes. Install MicroK8s: + +```bash +sudo snap install microk8s --channel 1.31-strict/stable +sudo adduser $USER snap_microk8s +newgrp snap_microk8s +``` + +Wait for MicroK8s to be ready using `sudo microk8s status --wait-ready`. Several MicroK8s add-ons are required for deployment: + +```bash +sudo microk8s enable hostpath-storage +# Required to host the OCI image of the Django application +sudo microk8s enable registry +# Required to expose the Django application +sudo microk8s enable ingress +``` + +Juju is required to deploy the Django application. Install Juju and bootstrap a development controller: + +```bash +sudo snap install juju --channel 3.5/stable +mkdir -p ~/.local/share +juju bootstrap microk8s dev-controller +``` + +Finally, create a new directory for this tutorial and go inside it: + +```bash +mkdir django-hello-world +cd django-hello-world +``` + +## Create the Django application + + +Create a `requirements.txt` file, copy the following text into it and then save it: + +``` +Django +``` + +Install `python3-venv` and create a virtual environment: + +```bash +sudo apt-get update && sudo apt-get install python3-venv -y +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Create a new project using `django-admin`: + +```bash +django-admin startproject django_hello_world +``` + +## Run the Django application locally + +Change into the `django_hello_world` directory and run the Django application to verify that it works: + +```bash +cd django_hello_world +python3 manage.py runserver +``` + +Test the Django application by using `curl` to send a request to the root endpoint. You may need a new terminal for this; if you are using Multipass use `multipass shell charm-dev` to get another terminal: + +```bash +curl localhost:8000 +``` + +The Django application should respond with `The install worked successfully! Congratulations!`. + +```{note} + +The response from the Django application includes HTML and CSS which makes it difficult to read on a terminal. + +``` + +The Django application looks good, so you can stop it for now using `ctrl+C`. + +## Pack the Django application into a rock + +First, we'll need a `rockcraft.yaml` file. Rockcraft will automate its creation and tailoring for a Django application by using the `django-framework` profile: + +```bash +cd .. +rockcraft init --profile django-framework +``` + +The `rockcraft.yaml` file will automatically be created and set the name based on your working directory. Open it in a text editor and check that the `name` is `django-hello-world`. Ensure that `platforms` includes the architecture of your host. For example, if your host uses the ARM architecture, include `arm64` in `platforms`. + +```{note} + +For this tutorial, we'll use the name `django-hello-world` and assume you are on the `amd64` platform. Check the architecture of your system using `dpkg --print-architecture`. Choosing a different name or running on a different platform will influence the names of the files generated by Rockcraft. + +``` + +Django applications require a database. Django will use a sqlite database by default. This won't work on Kubernetes because the database would disappear every time the pod is restarted (e.g., to perform an upgrade) and this database would not be shared by all containers as the application is scaled. We'll use Juju later to easily deploy a database. + +We'll need to update the `settings.py` file to prepare for integrating the app with a database. Open `django_hello_world/django_hello_world/settings.py` and include `import json`, `import os` and `import secrets` along with the other imports at the top of the file. + +Near the top of the `settings.py` file change the following settings to be production ready: + +```python +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32)) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true' + +ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '{ref}`]')) +``` + +Go further down to the Database section and change the `DATABASES` variable to: + +```python +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRESQL_DB_NAME'), + 'USER': os.environ.get('POSTGRESQL_DB_USERNAME'), + 'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'), + 'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'), + 'PORT': os.environ.get('POSTGRESQL_DB_PORT'), + } +} +``` + +We'll need to update the `requirements.txt` file to include `psycopg2-binary` so that the Django app can connect to PostgreSQL. + +Pack the rock: + +```bash +ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +``` + +```{note} + +Depending on your network, this step can take a couple of minutes to finish. + +`ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS` is required whilst the Django extension is experimental. + +``` + +Once Rockcraft has finished packing the Django rock, you'll find a new file in your working directory with the `.rock` extension: + +```bash +ls *.rock -l +``` + +The rock needs to be copied to the MicroK8s registry so that it can be deployed in the Kubernetes cluster: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.1_amd64.rock \ + docker://localhost:32000/django-hello-world:0.1 +``` + +```{note} + +If you changed the `name` or `version` in `rockcraft.yaml` or are not on an `amd64` platform, the name of the `.rock` file will be different for you. + +``` + +## Create the charm + +Create a new directory for the charm and go inside it: + +```bash +mkdir charm +cd charm +``` + +We'll need a `charmcraft.yaml`, `requirements.txt` and source code for the charm. The source code contains the logic required to operate the Django application. Charmcraft will automate the creation of these files by using the `django-framework` profile: + +```bash +charmcraft init --profile django-framework --name django-hello-world +``` + +The files will automatically be created in your working directory. We will need to connect to the PostgreSQL database. Open the `charmcraft.yaml` file and add the following section to the end of the file: + +```yaml +requires: + postgresql: + interface: postgresql_client + optional: false + limit: 1 +``` + +The charm depends on several libraries. Download the libraries and pack the charm: + +```bash +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +``` + +```{note} + +Depending on your network, this step can take a couple of minutes to finish. + +``` + +Once Charmcraft has finished packing the charm, you'll find a new file in your working directory with the `.charm` extension: + +```bash +ls *.charm -l +``` + +```{note} + +If you changed the name in charmcraft.yaml or are not on the amd64 platform, the name of the `.charm` file will be different for you. + +``` + +## Deploy the Django application + +A Juju model is needed to deploy the application. Create a new model: + +```bash +juju add-model django-hello-world +``` + +```{note} + +If you are not on a host with the `amd64` architecture, you will need to include a constraint to the Juju model to specify your architecture.For example, for the `arm64` architecture, use `juju set-model-constraints -m django-hello-world arch=arm64`. Check the architecture of your system using `dpkg --print-architecture`. + +``` + +Now deploy the Django application using Juju: + +```bash +juju deploy ./django-hello-world_ubuntu-22.04-amd64.charm \ + django-hello-world \ + --resource django-app-image=localhost:32000/django-hello-world:0.1 +``` + +Deploy PostgreSQL, and integrate and PostgreSQL with the Django application: +```bash +juju deploy postgresql-k8s --trust +juju integrate django-hello-world postgresql-k8s +``` + +```{note} + +It will take a few minutes to deploy the Django application. You can monitor the progress using `juju status --watch 5s`. Once the status of the App has gone to `active`, you can stop watching using `Ctrl+C`. + +``` + +The Django application should now be running. You can see the status of the deployment using `juju status` which should be similar to the following output: + +``` +django-hello-world dev-controller microk8s/localhost 3.5.3 unsupported 16:47:01+10:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +django-hello-world active 1 django-hello-world 3 10.152.183.126 no +postgresql-k8s 14.11 active 1 postgresql-k8s 14/stable 281 10.152.183.197 no + +Unit Workload Agent Address Ports Message +django-hello-world/0* active idle 10.1.157.80 +postgresql-k8s/0* active idle 10.1.157.78 Primary +``` + +To be able to test the deployment, we need to include the IP address in the allowed hosts configuration. We'll also enable debug mode for now whilst we are testing. Both can be done using `juju config django-hello-world django-allowed-hosts=* django-debug=true`. + +```{note} + +Setting the Django allowed hosts to `*` and turning on debug mode should not be done in production where you should set the actual hostname of the application and disable debug mode. We will do this in the tutorial for now and later demonstrate how we can set these to production ready values. + +``` + +Test the deployment using `curl` to send a request to the root endpoint. The IP address is the Address listed in the Unit section of the `juju status` output (e.g., `10.1.157.80` in the sample output above): + +```bash +curl 10.1.157.80:8000 +``` + +The Django app should again respond with `The install worked successfully! Congratulations!`. + +## Add a root endpoint + +The generated Django application does not come with a root endpoint, which is why we had to initially enable debug mode for testing. Let's add a root endpoint that returns a `Hello, world!` greeting. We will need to go back out to the root directory for the tutorial and go into the `django_hello_world` directory using `cd ../django_hello_world`. Add a new Django app using: + +```bash +django-admin startapp greeting +``` + +Open the `greeting/views.py` file and replace the content with: + +```python +from django.http import HttpResponse + + +def index(request): + return HttpResponse("Hello, world!\n") +``` + +Create the `greeting/urls.py` file with the following contents: + +```python +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), +] +``` + +Open the `django_hello_world/urls.py` file and edit the value of `urlpatterns` to include `path('', include("greeting.urls")`, for example: + +```python +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("", include("greeting.urls")), + path("admin/", admin.site.urls), +] +``` + +Since we're changing the application we should update the version of it. Go back to the root directory of the tutorial using `cd ..` and change the `version` in `rockcraft.yaml` to `0.2`. Pack and upload the rock using similar commands as before: + +```bash +ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.2_amd64.rock \ + docker://localhost:32000/django-hello-world:0.2 +``` + +Now we can deploy the new version of the Django application using: + +```bash +cd charm +juju refresh django-hello-world \ + --path=./django-hello-world_ubuntu-22.04-amd64.charm \ + --resource django-app-image=localhost:32000/django-hello-world:0.2 +``` + +Now that we have a valid root endpoint we can disable debug mode: + +```bash +juju config django-hello-world django-debug=false +``` + +Use `juju status --watch 5s` again to wait until the app is active again. The IP address will have changed so we need to retrieve it again using `juju status`. Now we can call the root endpoint using `curl 10.1.157.80:8000` and the Django application should respond with `Hello, world!`. + +## Enable a configuration + +To demonstrate how to provide configuration to the Django application, we will make the greeting configurable. Go back out to the tutorial root directory using `cd ..`. Open the `django_hello_world/greeting/views.py` file and replace the content with: + +```python +import os + +from django.http import HttpResponse + + +def index(request): + return HttpResponse(f"{os.environ.get('DJANGO_GREETING', 'Hello, world!')}\n") +``` + +Increment the `version` in `rockcraft.yaml` to `0.3` and run the pack and upload commands for the rock: + +```bash +ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.3_amd64.rock \ + docker://localhost:32000/django-hello-world:0.3 +``` + +Change back into the charm directory using `cd charm`. The `django-framework` Charmcraft extension supports adding configurations in `charmcraft.yaml` which will be passed as environment variables to the Django application. Add the following to the end of the `charmcraft.yaml` file: + +```yaml +config: + options: + greeting: + description: | + The greeting to be returned by the Django application. + default: "Hello, world!" + type: string +``` + +```{note} + +Configuration options are automatically capitalised and `-` are replaced by `_`. A `DJANGO_` prefix will also be added as a namespace for app configurations. + +``` + +We can now pack and deploy the new version of the Django app: + +```bash +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +juju refresh django-hello-world \ + --path=./django-hello-world_ubuntu-22.04-amd64.charm \ + --resource django-app-image=localhost:32000/django-hello-world:0.3 +``` + +After we wait for a bit monitoring `juju status` the application should go back to `active` again. Sending a request to the root endpoint using `curl 10.1.157.81:8000` (after getting the IP address from `juju status`) should result in the Django application responding with `Hello, world!` again. We can change the greeting using `juju config django-hello-world greeting='Hi!'`. After we wait for a moment for the app to be restarted, `curl 10.1.157.81:8000` should now respond with `Hi!`. + +## Expose the app using ingress + +```{note} + +This step of the tutorial only works for hosts with the `amd64` architecture. For other architectures, skip this step. + +``` + +As a final step, let's expose the application using ingress. Deploy the `nginx-ingress-integrator` charm and integrate it with the Django app: + +```bash +juju deploy nginx-ingress-integrator +juju integrate nginx-ingress-integrator django-hello-world +``` + +```{note} + +RBAC is enabled in the `charm-dev` Multipass blueprint. Run `juju trust nginx-ingress-integrator --scope cluster` if you're using the `charm-dev` blueprint. + +``` + +The hostname of the app needs to be defined so that it is accessible via the ingress. We will also set the default route to be the root endpoint: + +```bash +juju config nginx-ingress-integrator \ + service-hostname=django-hello-world path-routes=/ +``` + +Monitor `juju status` until everything has a status of `active`. Use `curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1` to send a request via the ingress. It should still be returning the `Hi!` greeting. + +```{note} + +The `-H "Host: django-hello-world"` option to the `curl` command is a way of setting the hostname of the request without setting a DNS record. + +``` + +We can now also change the Django allowed hosts to `django-hello-world` which is a production ready value (for production, you will need to setup a DNS record): + +```bash +juju config django-hello-world django-allowed-hosts=django-hello-world +``` + +Running `curl 127.0.0.1 -H "Host: django-hello-world"` should still get the Django app to respond with `Hi!`. + +## Tear things down + +You've reached the end of this tutorial. You have created a Django application, deployed it locally, build an OCI image for it and deployed it using Juju. Then we integrated it with PostgreSQL to be production ready, demonstrated how to add a root endpoint and how to configure the application and finally we exposed our application using an ingress. + +If you'd like to reset your working environment, you can run the following in the root directory for the tutorial: + +```bash +cd .. +deactivate +rm -rf charm .venv django_hello_world +# delete all the files created during the tutorial +rm django-hello-world_0.1_amd64.rock \ + django-hello-world_0.2_amd64.rock \ + django-hello-world_0.3_amd64.rock \ + rockcraft.yaml requirements.txt + +# Remove the juju model +juju destroy-model django-hello-world --destroy-storage +``` + +If you created an instance using Multipass, you can also clean it up. Start by exiting it: + +```bash +exit +``` + +And then you can proceed with its deletion: + +```bash +multipass delete charm-dev +multipass purge +``` + +## Next steps + +By the end of this tutorial you will have built a charm and evolved it in a number of typical ways. But there is a lot more to explore: + +| If you are wondering... | visit... | +|-------------------------|----------------------| +| "How do I...?" | {ref}`how-to-guides` | +| "What is...?" | {ref}`reference` | +| "Why...?", "So what?" | {ref}`explanation` | diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.md b/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.md new file mode 100644 index 000000000..9fd71630f --- /dev/null +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.md @@ -0,0 +1,549 @@ +(write-your-first-kubernetes-charm-for-a-fastapi-app)= +# Write your first Kubernetes charm for a FastAPI app + +**What you’ll need:** + +- A workstation, e.g., a laptop, with amd64 architecture which has sufficient resources to launch a virtual machine with 4 CPUs, 4 GB RAM, and a 50 GB disk + - Note that a workstation with arm64 architecture can complete the majority of this tutorial. +- Familiarity with Linux +- About 90 minutes of free time. + +**What you’ll do:** + +Create a FastAPI application. Use that to create a rock with `rockcraft`. Use that to create a charm with `charmcraft`. Use that to test-deploy, configure, etc., your Django application on a local Kubernetes cloud, `microk8s`, with `juju`. All of that multiple times, mimicking a real development process. + +```{note} + +**rock** + +An Ubuntu LTS-based OCI compatible container image designed to meet security, stability, and reliability requirements for cloud-native software. + +**charm** + +A package consisting of YAML files + Python code that will automate every aspect of an application's lifecycle so it can be easily orchestrated with Juju. + +**`juju`** + +An orchestration engine for charmed applications. + +``` + + +```{important} + +**Should you get stuck or notice issues:** Please get in touch on [Matrix](https://matrix.to/#/#12-factor-charms:ubuntu.com) or [Discourse](https://discourse.charmhub.io/). + +``` + + +## Set things up + +Install Multipass. + +> See more: [Multipass | How to install Multipass](https://multipass.run/docs/install-multipass) + +Use Multipass to launch an Ubuntu VM with the name `charm-dev` from the 24.04 blueprint: + +```bash +multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04 +``` + +Once the VM is up, open a shell into it: + +```bash +multipass shell charm-dev +``` + +In order to create the rock, you'll need to install Rockcraft: + +```bash +sudo snap install rockcraft --channel latest/edge --classic +``` + +`LXD` will be required for building the rock. Make sure it is installed and initialised: + +```bash +sudo snap install lxd +lxd init --auto +``` + +In order to create the charm, you'll need to install Charmcraft: + +```bash +sudo snap install charmcraft --channel latest/edge --classic +``` + +MicroK8s is required to deploy the FastAPI application on Kubernetes. Install MicroK8s: + +```bash +sudo snap install microk8s --channel 1.31-strict/stable +sudo adduser $USER snap_microk8s +newgrp snap_microk8s +``` + +Wait for MicroK8s to be ready using `sudo microk8s status --wait-ready`. Several MicroK8s add-ons are required for deployment: + +```bash +sudo microk8s enable hostpath-storage +# Required to host the OCI image of the FastAPI application +sudo microk8s enable registry +# Required to expose the FastAPI application +sudo microk8s enable ingress +``` + +> See more: [ingress^](https://microk8s.io/docs/ingress) + +Juju is required to deploy the FastAPI application. Install Juju and bootstrap a development controller: + +```bash +sudo snap install juju --channel 3.5/stable +mkdir -p ~/.local/share +juju bootstrap microk8s dev-controller +``` + +Finally, create a new directory for this tutorial and go inside it: + +```bash +mkdir fastapi-hello-world +cd fastapi-hello-world +``` + +```{note} + +This tutorial requires version `3.0.0` or later of Charmcraft. Check the version of Charmcraft using `charmcraft --version` If you have an older version of Charmcraft installed, use `sudo snap refresh charmcraft --channel latest/edge` to get the latest edge version of Charmcraft. + +This tutorial requires version `1.5.4` or later of Rockcraft. Check the version of Rockcraft using `rockcraft --version` If you have an older version of Rockcraft installed, use `sudo snap refresh rockcraft --channel latest/edge` to get the latest edge version of Rockcraft. + +``` + +## Create the FastAPI application + +Start by creating the "Hello, world" FastAPI application that will be used for this tutorial. + +Create a `requirements.txt` file, copy the following text into it and then save it: + +``` +fastapi[standard] +``` + +In the same directory, copy and save the following into a text file called `app.py`: + +```python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def root(): + return {"message": "Hello World"} +``` + +## Run the FastAPI application locally + +Install `python3-venv` and create a virtual environment: + +```bash +sudo apt-get update && sudo apt-get install python3-venv -y +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Now that we have a virtual environment with all the dependencies, let's run the FastAPI application to verify that it works: + +```bash +fastapi dev app.py --port 8080 +``` + +Test the FastAPI application by using `curl` to send a request to the root endpoint. You may need a new terminal for this; if you are using Multipass use `multipass shell charm-dev` to get another terminal: + +```bash +curl localhost:8080 +``` + +The FastAPI application should respond with `{"message":"Hello World"}`. The FastAPI application looks good, so we can stop for now using ctrl + c. + +## Pack the FastAPI application into a rock + +First, we'll need a `rockcraft.yaml` file. Rockcraft will automate its creation and tailoring for a FastAPI application by using the `fastapi-framework` profile: + +```bash +rockcraft init --profile fastapi-framework +``` + +The `rockcraft.yaml` file will automatically be created and set the name based on your working directory. Open the file in a text editor and check that the `name` is `fastapi-hello-world`. Ensure that `platforms` includes the architecture of your host. For example, if your host uses the ARM architecture, include `arm64` in `platforms`. + +```{note} + +For this tutorial, we'll use the `name` "fastapi-hello-world" and assume you are on the `amd64` platform. Check the architecture of your system using `dpkg --print-architecture`. Choosing a different name or running on a different platform will influence the names of the files generated by Rockcraft. + +``` + +Pack the rock: + +```bash +ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +``` + +```{note} + +Depending on your system and network, this step can take a couple of minutes to finish. + +``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required whilst the FastAPI extension is experimental. + +``` + +Once Rockcraft has finished packing the FastAPI rock, you'll find a new file in your working directory with the `.rock` extension: + +```bash +ls *.rock -l +``` + +```{note} + +If you changed the `name` or `version` in `rockcraft.yaml` or are not on an `amd64` platform, the name of the `.rock` file will be different for you. + +``` + +The rock needs to be copied to the MicroK8s registry so that it can be deployed in the Kubernetes cluster: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.1_amd64.rock \ + docker://localhost:32000/fastapi-hello-world:0.1 +``` + +> See more: [skopeo^](https://manpages.ubuntu.com/manpages/jammy/man1/skopeo.1.html) + +## Create the charm + +Create a new directory for the charm and go inside it: + +```bash +mkdir charm +cd charm +``` + +We'll need a `charmcraft.yaml`, `requirements.txt` and source code for the charm. The source code contains the logic required to operate the FastAPI application. Charmcraft will automate the creation of these files by using the `fastapi-framework` profile: + +```bash +charmcraft init --profile fastapi-framework --name fastapi-hello-world +``` + +The files will automatically be created in your working directory. + +The charm depends on several libraries. Download the libraries and pack the charm: + +```bash +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +``` + +```{note} + +Depending on your system and network, this step can take a couple of minutes to finish. + +``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required whilst the FastAPI extension is experimental. + +``` + +Once Charmcraft has finished packing the charm, you'll find a new file in your working directory with the `.charm` extension: + +```bash +ls *.charm -l +``` + +```{note} + +If you changed the name in charmcraft.yaml or are not on the amd64 platform, the name of the `.charm` file will be different for you. + +``` + +## Deploy the FastAPI application + +A Juju model is needed to deploy the application. Let's create a new model: + +```bash +juju add-model fastapi-hello-world +``` + +Now the FastAPI application can be deployed using Juju: + +```bash +juju deploy ./fastapi-hello-world_amd64.charm fastapi-hello-world \ + --resource app-image=localhost:32000/fastapi-hello-world:0.1 +``` + +```{note} + +It will take a few minutes to deploy the FastAPI application. You can monitor the progress using `juju status --watch 5s`. Once the status of the App has gone to `active`, you can stop watching using ctrl + c. + +> See more: {ref}`Command 'juju status' ` + +``` + +The FastAPI application should now be running. We can monitor the status of the deployment using `juju status` which should be similar to the following output: + +``` +Model Controller Cloud/Region Version SLA Timestamp +fastapi-hello-world dev-controller microk8s/localhost 3.5.4 unsupported 13:45:18+10:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +fastapi-hello-world active 1 fastapi-hello-world 0 10.152.183.53 no + +Unit Workload Agent Address Ports Message +fastapi-hello-world/0* active idle 10.1.157.75 +``` + +The deployment is finished when the status shows `active`. Let's expose the application using ingress. Deploy the `nginx-ingress-integrator` charm and integrate it with the FastAPI app: + +```bash +juju deploy nginx-ingress-integrator +juju integrate nginx-ingress-integrator fastapi-hello-world +``` + +The hostname of the app needs to be defined so that it is accessible via the ingress. We will also set the default route to be the root endpoint: + +```bash +juju config nginx-ingress-integrator \ + service-hostname=fastapi-hello-world path-routes=/ +``` + +Monitor `juju status` until everything has a status of `active`. Use `curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1` to send a request via the ingress. It should return the `{"message":"Hello World"}` greeting. + +```{note} + +The `--resolve fastapi-hello-world:80:127.0.0.1` option to the `curl` command is a way of resolving the hostname of the request without setting a DNS record. + +``` + +## Configure the FastAPI application + +Now let's customise the greeting using a configuration option. We will expect this configuration option to be available in the environment variable `APP_GREETING`. Go back out to the root directory of the project using `cd ..` and copy the following code into `app.py`: + +```python +import os + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def root(): + return {"message": os.getenv("APP_GREETING", "Hello World")} +``` + +Open `rockcraft.yaml` and update the version to `0.2`. Run `ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack` again, then upload the new OCI image to the MicroK8s registry: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.2_amd64.rock \ + docker://localhost:32000/fastapi-hello-world:0.2 +``` + +Change back into the charm directory using `cd charm`. The `fastapi-framework` Charmcraft extension supports adding configurations to `charmcraft.yaml` which will be passed as environment variables to the FastAPI application. Add the following to the end of the `charmcraft.yaml` file: + +```yaml +config: + options: + greeting: + description: | + The greeting to be returned by the FastAPI application. + default: "Hello, world!" + type: string +``` + +```{note} + +Configuration options are automatically capitalised and `-` are replaced by `_`. A `APP_` prefix will also be added to ensure that environment variables are namespaced. + +``` + +Run `CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack` again. The deployment can now be refreshed to make use of the new code: + +```bash +juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_amd64.charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.2 +``` + +Wait for `juju status` to show that the App is `active` again. Verify that the new configuration has been added using `juju config fastapi-hello-world | grep -A 6 greeting:` which should show the configuration option. + +```{note} + +The `grep` command extracts a portion of the configuration to make it easier to check whether the configuration option has been added. + +``` + +Running `curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1` shows that the response is still `{"message":"Hello, world!"}` as expected. The greeting can be changed using Juju: + +```bash +juju config fastapi-hello-world greeting='Hi!' +``` + +`curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1` now returns the updated `{"message":"Hi!"}` greeting. + +```{note} + +It might take a short time for the configuration to take effect. + +``` + +## Integrate with a database + +Now let's keep track of how many visitors your application has received. This will require integration with a database to keep the visitor count. This will require a few changes: + +* We will need to create a database migration that creates the `visitors` table +* We will need to keep track how many times the root endpoint has been called in the database +* We will need to add a new endpoint to retrieve the number of visitors from the database + +The charm created by the `fastapi-framework` extension will execute the `migrate.py` script if it exists. This script should ensure that the database is initialised and ready to be used by the application. We will create a `migrate.py` file containing this logic. + +Go back out to the tutorial root directory using `cd ..`. Create the `migrate.py` file using a text editor and paste the following code into it: + +```python +import os + +import psycopg2 + + +DATABASE_URI = os.environ{ref}`"POSTGRESQL_DB_CONNECT_STRING"] + + +def migrate(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS visitors ( + timestamp TIMESTAMP NOT NULL, + user_agent TEXT NOT NULL + ); + """) + conn.commit() + + +if __name__ == "__main__": + migrate() +``` + +```{note} + +The charm will pass the Database connection string in the `POSTGRESQL_DB_CONNECT_STRING` environment variable once postgres has been integrated with the charm. + +``` + +Open the `rockcraft.yaml` file in a text editor and update the version to `0.3`. + +To be able to connect to postgresql from the FastAPI app the `psycopg2-binary` dependency needs to be added in `requirements.txt`. The app code also needs to be updated to keep track of the number of visitors and to include a new endpoint to retrieve the number of visitors to the app. Open `app.py` in a text editor and replace its contents with the following code: + +```python +import datetime +import os +from typing import Annotated + +from fastapi import FastAPI, Header +import psycopg2 + +app = FastAPI() +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + + +@app.get("/") +async def root(user_agent: Annotated[str | None, Header()] = None): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + timestamp = datetime.datetime.now() + + cur.execute( + "INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)", + (timestamp, user_agent) + ) + conn.commit() + + return {"message": os.getenv("APP_GREETING", "Hello World")} + + +@app.get("/visitors") +async def visitors(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM visitors") + total_visitors = cur.fetchone()[0] + + return {"count": total_visitors} +``` + +Run `ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack` and upload the newly created rock to the MicroK8s registry: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.3_amd64.rock \ + docker://localhost:32000/fastapi-hello-world:0.3 +``` + +The FastAPI app now requires a database which needs to be declared in the `charmcraft.yaml` file. Go back into the charm directory using `cd charm`. Open `charmcraft.yaml` in a text editor and add the following section to the end: + +```yaml +requires: + postgresql: + interface: postgresql_client + optional: false +``` + +Pack the charm using `CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack` and refresh the deployment using Juju: + +```bash +juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_amd64.charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.3 +``` + +Deploy `postgresql-k8s` using Juju and integrate it with `fastapi-hello-world`: + +```bash +juju deploy postgresql-k8s --trust +juju integrate fastapi-hello-world postgresql-k8s +``` + +Wait for `juju status` to show that the App is `active` again. `curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1` should still return the `{"message":"Hi!"}` greeting. To check the total visitors, use `curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1` which should return `{"count":1}` after the previous request to the root endpoint and should be incremented each time the root endpoint is requested. If we perform another request to `curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`, `curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1` will return `{"count":2}`. + +## Tear things down + +We've reached the end of this tutorial. We have created a FastAPI application, deployed it locally, integrated it with a database and exposed it via ingress! + +If you'd like to reset your working environment, you can run the following in the root directory for the tutorial: + +```bash +# exit and delete the virtual environment +deactivate +rm -rf charm .venv __pycache__ +# delete all the files created during the tutorial +rm fastapi-hello-world_0.1_amd64.rock fastapi-hello-world_0.2_amd64.rock \ + fastapi-hello-world_0.3_amd64.rock rockcraft.yaml app.py \ + requirements.txt migrate.py +# Remove the juju model +juju destroy-model fastapi-hello-world --destroy-storage +``` + +If you created an instance using Multipass, you can also clean it up. Start by exiting it: + +```bash +exit +``` + +And then you can proceed with its deletion: + +```bash +multipass delete charm-dev +multipass purge +``` + + +## Next steps + +By the end of this tutorial you will have built a charm and evolved it in a number of typical ways. But there is a lot more to explore: + +| If you are wondering... | visit... | +|-------------------------|----------------------| +| "How do I...?" | {ref}`how-to-guides` | +| "What is...?" | {ref}`reference` | +| "Why...?", "So what?" | {ref}`explanation` | diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.md b/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.md new file mode 100644 index 000000000..a353f2732 --- /dev/null +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.md @@ -0,0 +1,620 @@ +(write-your-first-kubernetes-charm-for-a-go-app)= +# Write your first Kubernetes charm for a Go app + + +**What you’ll need:** + +- A working station, e.g., a laptop, with amd64 architecture which has sufficient resources to launch a virtual machine with 4 CPUs, 4 GB RAM, and a 50 GB disk + - Note that a workstation with arm64 architecture can complete the majority of this tutorial. +- Familiarity with Linux. +- About 90 minutes of free time. + +**What you’ll do:** + +Create a Go application. Use that to create a rock with `rockcraft`. Use that to create a charm with `charmcraft`. Use that to test-deploy, configure, etc., your Django application on a local Kubernetes cloud, `microk8s`, with `juju`. All of that multiple times, mimicking a real development process. + + +```{note} + +**rock** + +An Ubuntu LTS-based OCI compatible container image designed to meet security, stability, and reliability requirements for cloud-native software. + +**charm** + +A package consisting of YAML files + Python code that will automate every aspect of an application's lifecycle so it can be easily orchestrated with Juju. + +**`juju`** + +An orchestration engine for charmed applications. + +``` + + +```{important} + +**Should you get stuck or notice issues:** Please get in touch on [Matrix](https://matrix.to/#/#12-factor-charms:ubuntu.com) or [Discourse](https://discourse.charmhub.io/). + +``` + + +## Set things up + +Install Multipass. + +> See more: [Multipass | How to install Multipass](https://multipass.run/docs/install-multipass) + +Use Multipass to launch an Ubuntu VM with the name `charm-dev` from the 22.04 blueprint: + +```bash +multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 22.04 +``` + +Once the VM is up, open a shell into it: + +```bash +multipass shell charm-dev +``` + +In order to create the rock, you'll need to install Rockcraft: + +```bash +sudo snap install rockcraft --channel latest/edge --classic +``` + +`LXD` will be required for building the rock. Make sure it is installed and initialised: + +```bash +sudo snap install lxd +lxd init --auto +``` + +In order to create the charm, you'll need to install Charmcraft: + +```bash +sudo snap install charmcraft --channel latest/edge --classic +``` + +MicroK8s is required to deploy the Go application on Kubernetes. Install MicroK8s: + +```bash +sudo snap install microk8s --channel 1.31-strict/stable +sudo adduser $USER snap_microk8s +newgrp snap_microk8s +``` + +Wait for MicroK8s to be ready using `sudo microk8s status --wait-ready`. Several MicroK8s add-ons are required for deployment: + +```bash +sudo microk8s enable hostpath-storage +# Required to host the OCI image of the Go application +sudo microk8s enable registry +# Required to expose the Go application +sudo microk8s enable ingress +``` + +> See more: [ingress^](https://microk8s.io/docs/ingress) + +Juju is required to deploy the Go application. Install Juju and bootstrap a development controller: + +```bash +sudo snap install juju --channel 3.5/stable +mkdir -p ~/.local/share +juju bootstrap microk8s dev-controller +``` + +Finally, create a new directory for this tutorial and go inside it: + +```bash +mkdir go-hello-world +cd go-hello-world +``` + +```{note} + +This tutorial requires version `3.2.0` or later of Charmcraft. Check the version of Charmcraft using `charmcraft --version` If you have an older version of Charmcraft installed, use `sudo snap refresh charmcraft --channel latest/edge` to get the latest edge version of Charmcraft. + +This tutorial requires version `1.5.4` or later of Rockcraft. Check the version of Rockcraft using `rockcraft --version` If you have an older version of Rockcraft installed, use `sudo snap refresh rockcraft --channel latest/edge` to get the latest edge version of Rockcraft. + +``` + +## Create the Go application + +Start by creating the "Hello, world" Go application that will be used for this tutorial. + +Install `go` and initialise the Go module: +```bash +sudo snap install go --classic +go mod init go-hello-world +``` + +Create a `main.go` file, copy the following text into it and then save it: + +```go +package main + +import ( + "fmt" + "log" + "net/http" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + fmt.Fprintln(w, "Hello, world!") +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) +} +``` + +## Run the Go application locally + +Build the Go application so it can be run: + +```bash +go build . +``` + +Now that we have a binary compiled, let's run the Go application to verify that it works: + +```bash +./go-hello-world +``` + +Test the Go application by using `curl` to send a request to the root endpoint. You may need a new terminal for this; if you are using Multipass use `multipass shell charm-dev` to get another terminal: + +```bash +curl localhost:8080 +``` + +The Go application should respond with `Hello, world!`. The Go application looks good, so we can stop for now using ctrl + c. + +## Pack the Go application into a rock + +First, we'll need a `rockcraft.yaml` file. Rockcraft will automate its creation and tailoring for a Go application by using the `go-framework` profile: + +```bash +rockcraft init --profile go-framework +``` + +The `rockcraft.yaml` file will automatically be created and set the +name based on your working directory. Open the file in a text editor +and check that the `name` is `go-hello-world`. Ensure that `platforms` +includes the architecture of your host. For example, if your host uses +the ARM architecture, include `arm64` in `platforms`. + +```{note} + +For this tutorial, we'll use the `name` "go-hello-world" and assume you are on the `amd64` platform. Check the architecture of your system using `dpkg --print-architecture`. Choosing a different name or running on a different platform will influence the names of the files generated by Rockcraft. + +``` + +Pack the rock: + +```bash +ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +``` + +```{note} + +Depending on your system and network, this step can take a couple of minutes to finish. + +``` + +Once Rockcraft has finished packing the Go rock, you'll find a new file in your working directory with the `.rock` extension: + +```bash +ls *.rock -l +``` + +```{note} + +If you changed the `name` or `version` in `rockcraft.yaml` or are not on an `amd64` platform, the name of the `.rock` file will be different for you. + +``` + +The rock needs to be copied to the Microk8s registry so that it can be deployed in the Kubernetes cluster: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.1_amd64.rock \ + docker://localhost:32000/go-hello-world:0.1 +``` + +> See more: [skopeo^](https://manpages.ubuntu.com/manpages/jammy/man1/skopeo.1.html) + +## Create the charm + +Create a new directory for the charm and go inside it: + +```bash +mkdir charm +cd charm +``` + +We'll need a `charmcraft.yaml`, `requirements.txt` and source code for the charm. The source code contains the logic required to operate the Go application. Charmcraft will automate the creation of these files by using the `go-framework` profile: + +```bash +charmcraft init --profile go-framework --name go-hello-world +``` + +The files will automatically be created in your working directory. + +The charm depends on several libraries. Download the libraries and pack the charm: + +```bash +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs +CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +``` + +```{note} + +Depending on your system and network, this step can take a couple of minutes to finish. + +``` + +Once Charmcraft has finished packing the charm, you'll find a new file in your working directory with the `.charm` extension: + +```bash +ls *.charm -l +``` + +```{note} + +If you changed the name in charmcraft.yaml or are not on the amd64 platform, the name of the `.charm` file will be different for you. + +``` + +## Deploy the Go application + +A Juju model is needed to deploy the application. Let's create a new model: + +```bash +juju add-model go-hello-world +``` + +```{note} + +If you are not on a host with the amd64 architecture, you will need to include a constraint to the Juju model to specify your architecture. For example, for the arm64 architecture, use `juju set-model-constraints -m go-hello-world arch=arm64`. Check the architecture of your system using `dpkg --print-architecture`. + +``` + +Now the Go application can be deployed using Juju: + +```bash +juju deploy ./go-hello-world_amd64.charm \ + go-hello-world \ + --resource app-image=localhost:32000/go-hello-world:0.1 +``` + +```{note} + +It will take a few minutes to deploy the Go application. You can monitor the progress using `juju status --watch 5s`. Once the status of the App has gone to `active`, you can stop watching using ctrl + c. + +> See more: {ref}`Command 'juju status' ` + +``` + +The Go application should now be running. We can monitor the status of the deployment using `juju status` which should be similar to the following output: + +``` +go-hello-world microk8s microk8s/localhost 3.5.4 unsupported 14:35:07+02:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +go-hello-world active 1 go-hello-world 0 10.152.183.229 no + +Unit Workload Agent Address Ports Message +go-hello-world/0* active idle 10.1.157.79 +``` + +The deployment is finished when the status shows `active`. Let's expose the application using ingress. Deploy the `nginx-ingress-integrator` charm and integrate it with the Go app: + +```bash +juju deploy nginx-ingress-integrator --trust +juju integrate nginx-ingress-integrator go-hello-world +``` + +The hostname of the app needs to be defined so that it is accessible via the ingress. We will also set the default route to be the root endpoint: + +```bash +juju config nginx-ingress-integrator \ + service-hostname=go-hello-world path-routes=/ +``` + +```{note} + +By default, the port for the Go application should be 8080. If you want to change the default port, it can be done +with the configuration option `app-port` that will be exposed as `APP_PORT` to the Go application. + +``` + +Monitor `juju status` until everything has a status of `active`. Use `curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1` to send a request via the ingress. The Go application should respond with `Hello, world!`. + +## Configure the Go application + +Now let's customise the greeting using a configuration option. We will expect this configuration option to be available in the Go app configuration under the keyword `GREETING`. Go back out to the root directory of the project using `cd ..` and copy the following code into `main.go`: + +```python +package main + +import ( + "fmt" + "log" + "os" + "net/http" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + greeting, found := os.LookupEnv("APP_GREETING") + if !found { + greeting = "Hello, world!" + } + fmt.Fprintln(w, greeting) +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) +} +``` + +Open `rockcraft.yaml` and update the version to `0.2`. Run `ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack` again, then upload the new OCI image to the MicroK8s registry: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.2_amd64.rock \ + docker://localhost:32000/go-hello-world:0.2 +``` + +Change back into the charm directory using `cd charm`. The `go-framework` Charmcraft extension supports adding configurations to `charmcraft.yaml` which will be passed as environment variables to the Go application. Add the following to the end of the `charmcraft.yaml` file: + +```yaml +config: + options: + greeting: + description: | + The greeting to be returned by the Go application. + default: "Hello, world!" + type: string +``` + +```{note} + +Configuration options are automatically capitalised and `-` are replaced by `_`. A `APP_` prefix will also be added. + +``` + +Run `CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack` again. The deployment can now be refreshed to make use of the new code: + +```bash +juju refresh go-hello-world \ + --path=./go-hello-world_amd64.charm \ + --resource app-image=localhost:32000/go-hello-world:0.2 +``` + +Wait for `juju status` to show that the App is `active` again. Verify that the new configuration has been added using `juju config go-hello-world | grep -A 6 greeting:` which should show the configuration option. + +```{note} + +The `grep` command extracts a portion of the configuration to make it easier to check whether the configuration option has been added. + +``` + +Using `curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1` shows that the response is still `Hello, world!` as expected. The greeting can be changed using Juju: + +```bash +juju config go-hello-world greeting='Hi!' +``` + +`curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1` now returns the updated `Hi!` greeting. + +```{note} + +It might take a short time for the configuration to take effect. + +``` + +## Integrate with a database + +Now let's keep track of how many visitors your application has received. This will require integration with a database to keep the visitor count. This will require a few changes: + +* We will need to create a database migration that creates the `visitors` table +* We will need to keep track how many times the root endpoint has been called in the database +* We will need to add a new endpoint to retrieve the number of visitors from the database + +The charm created by the `go-framework` extension will execute the `migrate.sh` script if it exists. This script should ensure that the database is initialised and ready to be used by the application. We will create a `migrate.sh` file containing this logic. + +Go back out to the tutorial root directory using `cd ..`. Create the `migrate.sh` file using a text editor and paste the following code into it: + +```bash +#!/bin/bash + +PGPASSWORD="${POSTGRESQL_DB_PASSWORD}" psql -h "${POSTGRESQL_DB_HOSTNAME}" -U "${POSTGRESQL_DB_USERNAME}" "${POSTGRESQL_DB_NAME}" -c "CREATE TABLE IF NOT EXISTS visitors (timestamp TIMESTAMP NOT NULL, user_agent TEXT NOT NULL);" +``` + +```{note} + +The charm will pass the Database connection string in the `POSTGRESQL_DB_CONNECT_STRING` environment variable once PostgreSQL has been integrated with the charm. + +``` + +Change the permissions of the file `migrate.sh` so it is executable: +```bash +chmod u+x migrate.sh +``` + +For the migrations to work we need the `postgresql-client` package +installed in the rock. As by default the `go-framework` uses the `base` +base, we would also need to install a shell interpreter. Let's do it +as a slice, so the rock does not include unnecessary files. Open the `rockcraft.yaml` file using a text editor, update the version to `0.3` and add the +following to the end: +```yaml +parts: + runtime-debs: + plugin: nil + stage-packages: + # Added manually for the migrations + - postgresql-client + runtime-slices: + plugin: nil + stage-packages: + # Added manually for the migrations + - bash_bins +``` + +To be able to connect to PostgreSQL from the Go app the library `pgx` will be used. +The app code needs to be updated to keep +track of the number of visitors and to include a new endpoint to +retrieve the number of visitors to the app. Open `main.go` in a text +editor and replace its contents with the following code: + +```go +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + postgresqlURL := os.Getenv("POSTGRESQL_DB_CONNECT_STRING") + db, err := sql.Open("pgx", postgresqlURL) + if err != nil { + log.Printf("An error occurred while connecting to postgresql: %v", err) + return + } + defer db.Close() + + ua := req.Header.Get("User-Agent") + timestamp := time.Now() + _, err = db.Exec("INSERT into visitors (timestamp, user_agent) VALUES ($1, $2)", timestamp, ua) + if err != nil { + log.Printf("An error occurred while executing query: %v", err) + return + } + + greeting, found := os.LookupEnv("APP_GREETING") + if !found { + greeting = "Hello, world!" + } + + fmt.Fprintln(w, greeting) +} + +func visitorsHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("visitors request") + postgresqlURL := os.Getenv("POSTGRESQL_DB_CONNECT_STRING") + db, err := sql.Open("pgx", postgresqlURL) + if err != nil { + return + } + defer db.Close() + + var numVisitors int + err = db.QueryRow("SELECT count(*) from visitors").Scan(&numVisitors) + if err != nil { + log.Printf("An error occurred while executing query: %v", err) + return + } + fmt.Fprintf(w, "Number of visitors %d\n", numVisitors) +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.HandleFunc("/visitors", visitorsHandler) + http.ListenAndServe(":8080", nil) +} +``` + +Check all the packages and their dependencies in the Go project with the following command: +```bash +go mod tidy +``` + +Run `ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack` and upload the newly created rock to the MicroK8s registry: + +```bash +rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.3_amd64.rock \ + docker://localhost:32000/go-hello-world:0.3 +``` + +Go back into the charm directory using `cd charm`. The Go app now requires a database which needs to be declared in the `charmcraft.yaml` file. Open `charmcraft.yaml` in a text editor and add the following section to the end: + +```yaml +requires: + postgresql: + interface: postgresql_client + optional: false +``` + +Pack the charm using `CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack` and refresh the deployment using Juju: + +```bash +juju refresh go-hello-world \ + --path=./go-hello-world_amd64.charm \ + --resource app-image=localhost:32000/go-hello-world:0.3 +``` + +Deploy `postgresql-k8s` using Juju and integrate it with `go-hello-world`: + +```bash +juju deploy postgresql-k8s --trust +juju integrate go-hello-world postgresql-k8s +``` + +Wait for `juju status` to show that the App is `active` again. `curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1` should still return the `Hi!` greeting. To check the total visitors, use `curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1` which should return `Number of visitors 1` after the previous request to the root endpoint and should be incremented each time the root endpoint is requested. If we perform another request to `curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`, `curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1` will return `Number of visitors 2`. + +## Tear things down + +We've reached the end of this tutorial. We have created a Go application, deployed it locally, integrated it with a database and exposed it via ingress! + +If you'd like to reset your working environment, you can run the following in the root directory for the tutorial: + +```bash +cd .. +rm -rf charm +# delete all the files created during the tutorial +rm go-hello-world_0.1_amd64.rock go-hello-world_0.2_amd64.rock \ + go-hello-world_0.3_amd64.rock rockcraft.yaml main.go \ + migrate.sh go-hello-world go.mod go.sum +# Remove the juju model +juju destroy-model go-hello-world --destroy-storage +``` + +If you created an instance using Multipass, you can also clean it up. Start by exiting it: + +```bash +exit +``` + +And then you can proceed with its deletion: + +```bash +multipass delete charm-dev +multipass purge +``` + +## Next steps + +By the end of this tutorial you will have built a charm and evolved it in a number of typical ways. But there is a lot more to explore: + +| If you are wondering... | visit... | +|-------------------------|----------------------| +| "How do I...?" | {ref}`how-to-guides` | +| "What is...?" | {ref}`reference` | +| "Why...?", "So what?" | {ref}`explanation` |