Skip to content

Reference

Workspace

Whatever you build with Polycrate happens in your workspace. The workspace contains configuration and lifecycle artifacts. It's a directory on your filesystem (the so called workspace directory; can be specified using --workspace/-w) that can be synced and collaborated on via git or other tooling.

The workspace can be assembled using the workspace configuration.

Polycrate container

Most of the Polycrate magic happens inside of a Docker container running on the system that invokes the polycrate command.

The container will be started whenever you run an action. It is based on a public image (cargo.ayedo.cloud/library/polycrate) provided by ayedo (Dockerfile) and contains most of the best-practice tooling of cloud-native development and operations.

The container gives you access to a state-of-the-art DevOps runtime. Polycrate exports a snapshot of the workspace in various formats (yaml, json, environment vars, hcl, ...) and makes it available to the tooling inside the container so you can start building right away.

Note

You can run actions locally instead of in a container using the --local flag.

The main purpose for this would be:

  • Use the Polycrate CLI inside its own container in CI
  • Run local actions like setting up developer environments

This is currently EXPERIMENTAL and not well tested.

Dockerfile

Polycrate looks for a Dockerfile in your workspace (defaults to Dockerfile.poly, can be configured using --dockerfile). If it finds one, it will build it and run the container based on this image instead of the default image.

This can be used to persist changes to the workspace, like installing additional tools or libraries.

By convention, the Dockerfile should be built upon the official Polycrate image:

FROM cargo.ayedo.cloud/library/polycrate:latest

RUN pip install hcloud==1.16.0

Using loglevel 2 you can see and debug the build process:

DEBU[0000] Found Dockerfile.poly in Workspace           
DEBU[0000] Building image 'polycrate-demo:latest', --build=true 
WARN[0000] Building custom image polycrate-demo:latest  
DEBU[0000] Assembling docker context                    
DEBU[0000] Building image                               
DEBU[0001] Step 1/2 : FROM cargo.ayedo.cloud/library/polycrate:latest
DEBU[0001]  ---> 67237198f4a5                           
DEBU[0001] Step 2/2 : RUN pip install hcloud==1.16.0    
DEBU[0001]  ---> Using cache                            
DEBU[0001]  ---> 92a78743b4f4                           
DEBU[0001] Successfully built 92a78743b4f4              
DEBU[0001] Successfully tagged polycrate-demo:latest

Workspace configuration

The workspace configuration (default: workspace.poly) holds the configuration for a workspace and must be located inside the workspace directory.

Note

You can specify a custom workspace configuration file by using --workspace-config YOUR/CUSTOM/workspace-config.yml. This can be especially helpful when using Polycrate in CI.

# workspace.poly
name: polycrate-demo
blocks:
  - name: custom-block
    config:
      foo: bar

Note

The workspace name is limited to certain characters: ^[a-zA-Z]+([-/_]?[a-zA-Z0-9_]+)+$.

This constraint applies to ALL name stanzas in Polycrate.

Events

By default, Polycrate keeps a log of events in the workspace inside the logs root (defaults to .logs) of the workspace. An event will be saved as yaml inside a sub-folder of the logs root with the file name being the transaction id: .logs/2023/3/5/long-uuid.yml.

The event handler can be changed to a webhook endpoint in the workspace configuration:

# workspace.poly
name: polycrate-demo
events:
  handler: webhook # defaults to 'workspace'
  endpoint: https://example.com/xyz

The resulting event will look like this:

{
    "action": "install",
    "block": "custom-block",
    "command": "polycrate run custom-block install --loglevel=3 --workspace=/home/user/.polycrate/workspaces/polycrate-demo/",
    "config": {
        "endpoint": "https://example.com/xyz",
        "handler": "webhook"
    },
    "date": "2023-03-05T23:37:33Z",
    "labels": {
        "monk.event.class": "polycrate",
        "monk.event.level": "Info"
    },
    "transaction": "2952ded2-382e-4c90-8f08-82e71060148a",
    "user_email": "you@example.com", # taken from git config, if configured
    "user_name": "Your Name", # taken from git config, if configured
    "version": "latest",
    "workspace": "polycrate-demo"
}

Blocks

A Polycrate workspace is a modular system built out of so called blocks. Blocks are dedicated pieces of code/functionality that can be configured using the config stanza in the block configuration (default: block.poly) or the workspace configuration (default: workspace.poly). Blocks expose actions that can be executed using polycrate run $BLOCK_NAME $ACTION_NAME.

Polycrate looks for blocks inside the blocks root (defaults to blocks). Nested directories (e.g. blocks/foo/bar/baz) are allowed.

Note

If a block's name contains one or multiple slashes (/) and is installed through the registry, it will be saved to a nested directory structure: the block ayedo/k8s/harbor will be saved to blocks/ayedo/k8s/harbor. This also applies to the block's artifact directory.

Inheritance

Blocks can be inherited from other blocks which merges configuration of the parent block (typically a block you installed or created inside the blocks directory) into its child and changes the workdir to the parent block's workdir whenever an action runs. Such blocks that are based on other blocks are called dynamic blocks.

Note

When merging a parent block's config into a child block, existing config in the child block will not be overriden. The most common scenario where this is relevant is when you define defaults inside a block.poly file and overwrite them in your workspace.poly file.

...
blocks:
  - name: harbor
    from: ayedo/k8s/harbor
...

Dynamic blocks

Blocks can be created dynamically by defining their configuration in the workspace configuration directly. These blocks do not use custom code but only rely on the available tooling inside the Polycrate container.

Dynamic blocks can also inherit their default configuration and workdir from blocks that already exist in the blocks root by using the from: stanza in the block definition.

Dependencies

Polycrate supports workspace-level block dependencies by using the dependencies stanza:

...
dependencies:
  - ayedo/hcloud/inventory:0.0.1
  - ayedo/hcloud/k8s-infra:0.0.2
  - ayedo/hcloud/k8s:0.0.9
  - ayedo/k8s/nginx:0.0.2
  - ayedo/k8s/portainer:0.0.7
  - ayedo/k8s/cert-manager:0.0.3
  - ayedo/k8s/external-dns:0.0.21
  - cargo.ayedo.cloud/ayedo/k8s/harbor:0.0.1
...

Polycrate checks the configured dependencies against the installed blocks in the workspace with every invocation of the polycrate command. If a dependency is missing, it will be downloaded.

Pull blocks

To dynamically add blocks to the workspace from the registry, you can run polycrate pull $BLOCK_NAME:$BLOCK_VERSION. If $BLOCK_VERSION is not defined, latest will be assumed.

Blocks can be uninstalled from the workspace by running polycrate block uninstall BLOCK1 or simply deleting the block's directory.

If you want to use a custom registry, reference it when pulling blocks or adding them to the workspace dependencies: polycrate block pull index.docker.io/my-block

Push blocks

To push a block to an OCI-compatible registry, run the following command: polycrate block push $BLOCK_NAME.

If the block name doesn't contain a valid registry url, a default registry will be used. A custom registry can be used as target by giving the block an appropriate name, like: my.registry.com/block/name. Polycrate will resolve the registry url from the block's name.

Note

When pushing a block, omit the tag (what's coming after the :) as Polycrate will automatically add the version defined in the block's block.poly to the image.

Block directory

The block directory is the directory that contains custom code and the block.poly file of a block underneath the blocks root.

Note

By convention, the name of the block directory should be the same you defined in the name stanza of that Block.

Block configuration

The block configuration (defaults to block.poly) holds the configuration for a single block and must be located in the block directory.

# block.poly
name: custom-block
config:
  baz:
    foo: bar
actions:
  install:
    script:
      - echo "Install"
  uninstall:
    script:
      - echo "Uninstall"

The config stanza of the block configuration is free form and not typed. You can use it to define the configuration structure of your block according to your needs.

Note

Block names are limited to certain characters: ^[a-zA-Z]+([-/_]?[a-zA-Z0-9_]+)+$.

This constraint applies to ALL name stanzas in Polycrate.

Actions

actions:
  - name: install
    playbook: install.yml
    prompt: 
      message: "Do you really want to run this action?"

A block can expose an arbitrary amount of actions. Actions are used to implement the actual functionality of a block. Examples would be install or uninstall, but also status or init.

To execute an Ansible playbook, specify its name in the playbook stanza of an action. Polycrate will create the respective ansible-playbook command and make the workspace snapshot available as extra-vars to the playbook so they can be used even in the host stanza of a playbook.

Actions accept a prompt stanza. If configured with a non-empty message attribute, Polycrate will prompt the user for confirmation before execution an action. If the user declines confirmation, the action will fail. Please note that you can prompt for confirmation in workflow steps, too. If you prompt on an action and on a step, the user will have to confirm 2 times.

Note

You can auto-confirm all prompts by using the --force flag.

Actions also support a script stanza which contains a list of commands that will be merged into a Bash script and executed inside the Polycrate container (or locally if you specifiy --local) when you run the action. The script stanza is mutually exclusive with the playbook stanza.

[...]
actions:
  - name: script-action
    script:
      - ls -la
      - du -hsc
      - env
      - echo "Hello World"
[...]

Note

Action names are limited to certain characters: ^[a-zA-Z]+([-/_]?[a-zA-Z0-9_]+)+$.

This constraint applies to ALL name stanzas in Polycrate.

Note

Polycrate does not persist data between runs apart from changes made to the workspace directory (mounted at /workspace inside the execution container).

Note

It's fine to write data to the workspace directory. However it's best-practice to use Artifacts to persist custom data.

Artifacts

Artifacts can be stored in the artifacts root inside your workspace (which is configurable using --artifacts-root and defaults to artifacts).

By default, Polycrate looks for Ansible Inventories and Kubeconfigs in the artifacts directory of a Block.

For each block in the workspace a directory will automatically be created underneath the artifacts root (e.g. artifacts/blocks/$BLOCK_NAME).

Ansible Inventory

Polycrate can consume yaml-formated Ansible inventory files inside the artifacts directory of a block. Polycrate looks for a file named inventory.yml by default - this can be overridden using the inventory.filename stanza in the block configuration.

An inventory file can be created automatically by a block or provided manually (useful for existing infrastructure).

The inventories can be consumed by the owning block itself or by other blocks using the inventory stanza in the block configuration:

# workspace.poly
blocks:
  - namename: block-a
    inventory:
      from: block-b
      filename: inventory.yml # defaults to inventory.yml

This will add an environment variable (ANSIBLE_INVENTORY=path/to/inventory/of/block-b) to the container that points Ansible to the right inventory to work with.

The inventory of block-b could look like this:

all:
  hosts:
    host-1:
      ansible_host: 1.2.3.4
      ansible_ssh_port: 22
      ansible_python_interpreter: "/usr/bin/python3"
      ansible_user: root
  children:
    master:
      hosts:
        host-1

SSH

Polycrate can provide a shortcut to ssh into one of the nodes managed by a workspace if a valid Ansible Inventory exists.

To interpret the inventory, Polycrate starts an intermediate container which submits the necessary config for a successful ssh connection to Polycrate on exit.

Running polycrate ssh --block block-b host-1 will initiate an SSH session with host-1 from the inventory file of block-b.

Note

If you have a block called inventory that has an artifact called inventory.yml, you can omit the --block inventory part of the command as Polycrate assumes these filenames and block names as defaults: polycrate ssh host-1

Kubeconfig

Polycrate is integrated with Kubernetes and can connect to a cluster using a kubeconfig file. By default, Polycrate looks for kubeconfig files named kubeconfig.yml inside the artifacts directory of a block. This can be overridden using the kubeconfig.filename stanza in the block configuration.

A kubeconfig file can be created automatically by a block or provided manually (useful for existing infrastructure).

The kubeconfig file can be consumed by the owning block itself or by other blocks using the kubeconfig stanza in the block configuration:

# block.poly
name: block-a
  kubeconfig:
    from: block-b
    filename: kubeconfig.yml

This will add an environment variable (KUBECONFIG=path/to/kubeconfig/of/block-b) to the container that points kubectl, etc to the right kubeconfig to work with.

Workspace snapshot

Whenever you run an action with Polycrate, a workspace snapshot will be captured. This snapshot contains the computed workspace configuration and will be exported in the following formats:

yaml

Polycrate exports the workspace snapshot to a yaml file that will be saved locally and mounted into the container. The path of the file will be exported to the POLYCRATE_WORKSPACE_SNAPSHOT_YAML environment variable.

You can use the --snapshot flag when invoking polycrate run. This will prevent any action from running and instead dumps the workspace snapshot. Also, polycrate workspace snapshot has the same effect, but doesn't contain data about the current action and block.

Workflows

Polycrate supports workflows, i.e. the ordered execution of block actions.

# workspace.poly
name: workflow-workspace
blocks:
  - name: block-1
    actions:
      - name: action-1
        prompt: 
          message: "Do you really want to run this action?"
        script:
          - echo "block 1 action 1"
  - name: block-2
    actions:
      - name: action-1
        script:
          - echo "block 2 action 1"
workflows:
  - name: workflow-1
    prompt: 
          message: "Do you really want to run this workflow?"
    allow_failure: true
    steps:
      - name: block-1-action-1
        block: block-1
        action: action-1
      - name: block-2-action-1
        block: block-2
        action: action-1
        prompt: 
          message: "Do you really want to run this step?"

You can use polycrate workflows run workflow-1 (or for short polycrate run workflow-1) to execute this workflow.

If the workflow, one if its steps or an action has the prompt stanza with a non-empty message, Polycrate will interrupt the workflow and prompts the user for confirmation. The current workflow/step/action will fail if the user declines. If allow_failure is set to true for a workflow, execution will continue even if individual steps fail.

Note

You can auto-confirm all prompts by using the --force flag.

Loglevel

Polycrate supports 3 loglevel:

  • Loglevel 1: The default. Will only print logs of type INFO or above
  • Loglevel 2: Debug-Level. You know ...
  • Loglevel 3: Trace level.

The loglevel will be mapped to the respective Ansible verbosity meaning --loglevel 3 will result in Ansible executing as if you used -vvv.

Registry

Polycrate blocks can be pushed and pulled to and from a OCI-compatible registry. Polycrate uses cargo.ayedo.cloud as the default registry to obtain blocks from or push them to.

You can define your own registry by using --registry-url and pointing it to your OCI-compatible registry or simply include the registry URL in the block's name, just like with Docker.

Polycrate does not implement authentication with the registry but instead makes use of your local Docker credential helper. To authenticate against a registry, run docker login $REGISTRY_URL before pushing or pulling blocks.

Ansible

Polycrate provides a special integration with Ansible. The workspace snapshot that is being exported to yaml format and mounted to the Polycrate container will be consumed by Ansible automagically. As a result, the snapshot is available directly as top-level variables in Ansible which can be used in playbooks and templates.

The following example shows:

  • the default configuration (block.poly) of a block called traefik
  • the user-provided configuration for the block in workspace.poly
  • the Ansible playbook using the exposed variables ( block.config...)
  • an Ansible template using the exposed variables (templates/docker-compose.yml.j2)
  • the resulting file /polycrate/docker-compose.yml that is templated to a remote host

Note

Note how in block.poly the configured image is traefik:2.6 but in workspace.poly it's traefik:2.7. In the resulting docker-compose.yml, the image is traefik:2.7 as defaults in block.poly will be overridden by user-provided configuration in workspace.poly.

The block variable contains the configuration of the current block invoked by polycrate run traefik install. Additionally, there's a variable workspace available, that contains the fully compiled workspace including additional blocks that are available in the workspace.

Polycrate makes use of a special Ansible Vars Plugin to read in the Yaml-Snapshot and expose it as top-level variables to the Ansible facts.

name: traefik
config:
  image: "traefik:v2.6"
  letsencrypt:
    email: ""
    resolver: letsencrypt
actions:
  - name: install
    script:
      - ansible-playbook install.yml
  - name: uninstall
    script:
      - ansible-playbook uninstall.yml
  - name: prune
    script:
      - ansible-playbook prune.yml
name: ansible-traefik-demo
blocks:
  - name: traefik
    inventory:
      from: inventory-block
    config:
      letsencrypt:
        email: info@example.com
      image: traefik:2.7
- name: "install"
  hosts: all
  gather_facts: yes
  tasks:
    - name: Create remote block directory
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      with_items:
        - "/polycrate/{{ block.name }}"

    - name: Copy compose file
      ansible.builtin.template:
        src: docker-compose.yml.j2
        dest: "/polycrate/{{ block.name }}/docker-compose.yml"

    - name: Deploy compose stack
      docker_compose:
        project_src: "/polycrate/{{ block.name }}"
        remove_orphans: true
        files:
          - docker-compose.yml
version: "3.9"

services:
  traefik:
    image: "{{ block.config.image }}"
    container_name: "traefik"
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.email={{ block.config.letsencrypt.email }}"
      - "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.tlschallenge=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "traefik-letsencrypt:/letsencrypt"
    networks:
      - traefik

networks:
  traefik:
    name: traefik

volumes:
  traefik-letsencrypt:
version: "3.9"

services:
  traefik:
    image: "traefik:2.7"
    container_name: "traefik"
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=info@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "traefik-letsencrypt:/letsencrypt"
    networks:
      - traefik

networks:
  traefik:
    name: traefik

volumes:
  traefik-letsencrypt:

Last update: March 5, 2023