txt: ## dist_zef-jnthn-Dev-ContainerizedService.md
  # Dev::ContainerizedService
  
  This module aims to ease the process of setting up services (such as Postgres)
  for the purpose of having a local development environment for Raku projects.
  For example, one might have a Raku web application that uses a database. In
  order to try out the application locally, a database instance needs to be set
  up. Ideally this should be effortless and also isolated.
  
  As the name suggests, this module achieves its aims using containers. It
  depends on nothing more than Raku and having a functioning `docker`
  installation.
  
  ## Usage
  
  ### Getting Started
  
  Let's assume we have a web application that uses a Postgres database and expects
  that the `DB_CONN_INFO` environment variable will be populated with a connection
  string.
  
  To make a development environment configuration using this module, we create a
  script `devenv.raku`:
  
  ```
  #!/usr/bin/env raku
  use Dev::ContainerizedService;
  
  service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
      env 'DB_CONN_INFO', $conninfo;
  }
  
  ```
  
  The `service` function specifies the service ID, a Docker image tag, and a block that
  should be called when the service is up and running. The `env` function, located in a
  service, specifies an environment variable to be set.
  
  We can then (assuming `chmod +x devenv.raku`) use the script as follows:
  
  ```
  ./devenv.raku run raku -Ilib service.raku
  
  ```
  
  This will:
  
  1. Pull the Postgres docker container if required
  2. Run the container, setting up a database user/password and binding it to a free
     port
  3. Run `raku -Ilib service.raku` with the `DB_CONN_INFO` environment variable set
  
  If using the `cro` development tool, one could do:
  
  ```
  ./devenv.raku run cro run
  
  ```
  
  ### Additional Actions
  
  The service block is run after the container is started (service implementations
  include readiness checks). As well as - or instead of - specifying environment
  variables to pass to the process, one can write any Raku code there. For
  example, one could run database migrations (in the case where it's desired to
  have them explicitly applied to production, rather than having them applied at
  application startup time).
  
  ### Retaining data
  
  By default, any created databases are not persisted once the `run` command is
  completed. To change this, alter the configuration file to specify a project name
  (the name of your application) and call `store`:
  
  ```
  #!/usr/bin/env raku
  use Dev::ContainerizedService;
  
  project 'my-app';
  store;
  
  service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
      env 'DB_CONN_INFO', $conninfo;
  }
  
  ```
  
  Now when using `./devenv.raku run ...`, for services that support it, Docker
  volume(s) will be created and the generated password(s) for services will be
  saved (in your home directory). These will be reused on subsequent runs.
  
  To clean up this storage, use:
  
  ```
  ./devenv.raku delete
  
  ```
  
  Which will remove any created volumes along with saved settings.
  
  ### Showing produced configuration
  
  When using storage, it is also possible to see the most recently passed service
  settings for each service by using:
  
  ```
  ./devenv.raku show
  
  ```
  
  The output looks like this:
  
  ```
  postgres
    conninfo: host=localhost port=29249 user=test password=xxlkC2MrOv4yJ3vP1V-pVI7 dbname=test
    dbname: test
    host: localhost
    password: xxlkC2MrOv4yJ3vP1V-pVI7
    port: 29249
    user: test
  
  ```
  
  When used while `run` is active, this is handy for obtaining connection string
  information in order to connect to the database using tools of your choice.
  
  ### Tools
  
  Some service specifications also come with a way to run related tools. For
  example, the `postgres` specification can run the `psql` command line client
  (using the version in the container, to be sure of server compatibility),
  injecting the correct credentials. Thus:
  
  ```
  ./devenv.raku tool postgres client
  
  ```
  
  Is sufficient to launch the client to look at the database. Note that this
  only works when the service is running (so one would run it in one terminal
  window, and then use the tool subcommand in another).
  
  ### Multiple stores
  
  Calling:
  
  ```
  store;
  
  ```
  
  Is equivalent to calling:
  
  ```
  store 'default';
  
  ```
  
  That is, it specifies the name of a default store. It is possible to have multiple
  independent stores, which are crated using the `--store` argument before the `run`
  subcommand:
  
  ```
  ./devenv.raku --store=bug42 run cro run
  
  ```
  
  To see the created stores, use:
  
  ```
  ./devenv.raku stores
  
  ```
  
  To show the produced service configuration for a particular store, use:
  
  ```
  ./devenv.raku --store=bug42 show
  
  ```
  
  To use a tool against a particular store, use:
  
  ```
  ./devenv.raku --store=bug42 tool postgres client
  
  ```
  
  To delete a particular store, rather than the default one, use:
  
  ```
  ./devenv.raku --store=bug42 delete
  
  ```
  
  ### Multiple instances of a given service
  
  One can have multiple instances of a given service. When doing this, it is wise
  to assign them names (otherwise names like `postgres-2` will be generated, and
  this will not be too informative in `show` output):
  
  ```
  service 'postgres', :tag<13.0>, :name<pg-products> -> (:$conninfo, *%) {
      env 'PRODUCT_DB_CONN_INFO', $conninfo;
  }
  
  service 'postgres', :tag<13.0>, :name<pg-billing> -> (:$conninfo, *%) {
      env 'BILLING_DB_CONN_INFO', $conninfo;
  }
  
  ```
  
  These names are used in the `tool` subcommand:
  
  ```
  ./devenv.raku -tool pg-billing client
  
  ```
  
  ### Custom images
  
  Sometimes you might not want to use the default docker images for a service; for
  exmaple, if doing Postgres with pgvector you might need an image with that
  extension installed instead. In that case, pass in `image-name`:
  
  ```
  service 'postgres', :image-name<pgvector/pgvector>, :tag<0.8.0-pg17>, -> (:$conninfo, :$host, :$port, :$user, :$password, :$dbname) {
      ...
  }
  
  ```
  
  ### Is this magic?
  
  Not really; the `Dev::ContainerizedService` module exports a `MAIN` sub, which is
  how it gets to provide the program entrypoint.
  
  ## Available Services
  
  ### Postgres
  
  Either obtain a connection string:
  
  ```
  service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
      env 'DB_CONN_INFO', $conninfo;
  }
  
  ```
  
  Or the individual parts of the database connection details:
  
  ```
  service 'postgres', :tag<13.0>, -> (:$host, :$port, :$user, :$password, :$dbname, *%) {
      env 'DB_HOST', $host;
      env 'DB_PORT', $port;
      env 'DB_USER', $user;
      env 'DB_PASS', $password;
      env 'DB_NAME', $dbname;
  }
  
  ```
  
  Postgres supports storage of the database between runs when `store` is used.
  
  The `client` tool is available, and runs the `psql` client:
  
  ```
  ./devenv.raku tool postgres client
  
  ```
  
  ### Redis
  
  Obtain the host and port of the started instance:
  
  ```
  service 'redis', :tag<7.0>, -> (:$host, :$port) {
      env 'REDIS_HOST', $host;
      env 'REDIS_PORT', $port;
  }
  
  ```
  
  Redis is currently always in-memory and will never be stored.
  
  ## The service I want isn't here!
  
  1. Fork this repository.
  2. Add a module `Dev::ContainerizedService::Spec::Foo`, and in it write a
     class of the same name that does `Dev::ContainerizedService::Spec`. See
     the role's documentation as well as other specs as an example.
  3. Add a mapping to the `constant %specs` in `Dev::ContainerizedService`.
  4. Write a test to make sure it works.
  5. Add an example to the `README.md`.
  6. Submit a pull request.
  