diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..90928d3 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,10 @@ +--- +steps: + - name: 'Check the repository with ansible-lint' + image: 'python:bookworm' + when: + - event: 'manual' + - event: 'push' + commands: + - 'pip -q install ansible-lint' + - 'ansible-lint' diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..bfe72e7 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,26 @@ +--- +extends: 'default' + +ignore: + - '.ansible/' + +rules: + braces: + max-spaces-inside: 1 + comments: + min-spaces-from-content: 1 + comments-indentation: false + document-start: + present: true + line-length: + max: 120 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + quoted-strings: + required: true + quote-type: 'single' + truthy: + allowed-values: + - 'true' + - 'false' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fac1c60 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +* Initial Release \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6adf56a --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Ansible Collection - enbewe.caddy + +Collecion for various tasks, based on caddy (https://caddyserver.com/), and related things. + +## Playbooks +### enbewe.caddy.deploy +Deploys several roles to matching host groups: + * Role `enbewe.caddy.caddy` is deployed to host group `caddy` + * Role `enbewe.caddy.static` is deployed to host group `site` + * Role `enbewe.caddy.isso` is deployed to host group `isso` + +## Roles +### enbewe.caddy.caddy +Installs the caddy server in a podman container and configures the server to act +as a reverse proxy to all configured sites. + +#### Required variables +**caddy_networks** *(Type: list / elements=string)* +The list of podman networks the proxy should be part of. + +**caddy_sites** *(Type: dict) +The sites that caddy should serve. +Each site has to have a `name`, that is used as the host in caddy config. +Additionally, the site should have the key `proxy_to`, that points to the address of the proxied service, +or it should have the key `directives` that can be used to create a free-form config for the site. + +#### Optional variables +**caddy_use_local_certs** *(Default: false)* +Enforce usage of local certificates, instead of the default Letsencrypt certs. + +**caddy_podman_image_name** *(Default: 'docker.io/library/caddy')* +Image to use for the caddy container. + +**caddy_podman_image_tag** *(Default: 'latest')* +Tag to use for the caddy container image. + +**caddy_conf_path** *(Default: '/etc/caddy')* +Path where the configuration of caddy should be stored. + + +### enbewe.caddy.static +Installs caddy servers in podman containers and serves one or more static site through them. + +#### Required variables +**static_caddy_sites** *(Type: list / elements=dict)* +The list of sites to deploy. Each site has to have to following variables set: + +**static_caddy_sites.name** *(Type: string)* +The name of the site. Used in container names and directories. + +**static_caddy_sites.network** *(Type: string)* +The network the site is deployed in. + +**static_caddy_sites.volume** *(Type: string)* +The name of the volume to use for the data of the container. + +**static_caddy_sites.archive_url** *(Type: string)* +The url to download the site data from. + +**static_caddy_sites.archive_username** *(Type: string)* +The user to authorize with when downloading the archive. + +**static_caddy_sites.archive_password** *(Type: string)* +The password to use when authorizing. + +**static_caddy_sites.tempfile** *(Type: string)* +The path to store the downloaded archive to. + + +### enbewe.caddy.isso +Installs isso comments server in a dedicated podman container. + +#### Required variables +**isso_network** *(Type: string)* +The name of the network to use. + +**isso_cfg_host** *(Type: string)* +The website name in isso. + +**isso_mail_username** *(Type: string)* +The username to connect to the mail server with. + +**isso_mail_password** *(Type: string)* +The password to use when connecting to the mail server. + +**isso_mail_host** *(Type: string)* +The host name of the mail server to use for notifications. + +**isso_mail_port** *(Type: string)* +The port to connect to the mail server at. + +**isso_mail_security** *(Type: string)* +The type of security to use with the mail server. Can be 'none', 'starttls' or 'ssl'. + +**isso_mail_rcpt** *(Type: string)* +Mail address to send the notifications to. + +**isso_mail_from** *(Type: string)* +The mail sender to use when sending notifications. + +**isso_admin_enabled** *(Type: boolean)* +Whether the admin interface should be activated. + +**isso_admin_password** *(Type: string)* +The password to use for the admin interface. + +#### Optional variables +**isso_image_name** *(Default: 'ghcr.io/isso-comments/isso')* +Image to use for the isso container. + +**isso_image_tag** *(Default: 'release')* +Tag to use for the isso container image. + +**isso_storage_path** *(Default: '/srv/wwww/isso')* +Path where the data of caddy should be stored. diff --git a/collections/requirements.yml b/collections/requirements.yml new file mode 100644 index 0000000..3cb14fc --- /dev/null +++ b/collections/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: 'containers.podman' + version: '>=1.16.3' diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..22585d2 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,66 @@ +--- +### REQUIRED +# The namespace of the collection. This can be a company/brand/organization or product namespace under which all +# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with +# underscores or numbers and cannot contain consecutive underscores +namespace: 'enbewe' + +# The name of the collection. Has the same character restrictions as 'namespace' +name: 'caddy' + +# The version of the collection. Must be compatible with semantic versioning +version: '1.0.0' + +# The path to the Markdown (.md) readme file. This path is relative to the root of the collection +readme: 'README.md' + +# A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) +# @nicks:irc/im.site#channel' +authors: + - 'Nis Wechselberg ' + +### OPTIONAL but strongly recommended +# A short summary description of the collection +description: 'Caddy based tasks, reverse proxy as well as static site.' + +# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only +# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' +license: + - 'MIT' + +# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character +# requirements as 'namespace' and 'name' +tags: + - 'linux' + +# Collections that this collection requires to be installed for it to be usable. The key of the dict is the +# collection label 'namespace.name'. The value is a version range +# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version +# range specifiers can be set and are separated by ',' +dependencies: + containers.podman: '>=1.16.3' + +# The URL of the originating SCM repository +repository: 'https://git.enbewe.de/Coding/ansible-collection-caddy' + +# The URL to any online docs +# documentation: http://docs.example.com + +# The URL to the homepage of the collection/project +# homepage: http://example.com + +# The URL to the collection issue tracker +# issues: http://example.com/issue/tracker + +# A list of file glob-like patterns used to filter any files or directories that should not be included in the build +# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This +# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', +# and '.git' are always filtered. Mutually exclusive with 'manifest' +# build_ignore: [] + +# A dict controlling use of manifest directives used in building the collection artifact. The key 'directives' is a +# list of MANIFEST.in style +# L(directives,https://packaging.python.org/en/latest/guides/using-manifest-in/#manifest-in-commands). The key +# 'omit_default_directives' is a boolean that controls whether the default directives are used. Mutually exclusive +# with 'build_ignore' +# manifest: null diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 0000000..ddab9ac --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,52 @@ +--- +# Collections must specify a minimum required ansible version to upload +# to galaxy +requires_ansible: '>=2.18.0' + +# Content that Ansible needs to load from another location or that has +# been deprecated/removed +# plugin_routing: +# action: +# redirected_plugin_name: +# redirect: ns.col.new_location +# deprecated_plugin_name: +# deprecation: +# removal_version: "4.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# removed_plugin_name: +# tombstone: +# removal_version: "2.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# become: +# cache: +# callback: +# cliconf: +# connection: +# doc_fragments: +# filter: +# httpapi: +# inventory: +# lookup: +# module_utils: +# modules: +# netconf: +# shell: +# strategy: +# terminal: +# test: +# vars: + +# Python import statements that Ansible needs to load from another location +# import_redirection: +# ansible_collections.ns.col.plugins.module_utils.old_location: +# redirect: ansible_collections.ns.col.plugins.module_utils.new_location + +# Groups of actions/modules that take a common set of options +# action_groups: +# group_name: +# - module1 +# - module2 diff --git a/playbooks/deploy.yml b/playbooks/deploy.yml new file mode 100644 index 0000000..4bd02ee --- /dev/null +++ b/playbooks/deploy.yml @@ -0,0 +1,15 @@ +--- +- name: 'Deploy reverse proxy to host group' + hosts: 'caddy' + roles: + - 'enbewe.caddy.caddy' + +- name: 'Deploy static sites to host group' + hosts: 'site' + roles: + - 'enbewe.caddy.static' + +- name: 'Deploy isso comment system to host group' + hosts: 'isso' + roles: + - 'enbewe.caddy.isso' diff --git a/roles/caddy/defaults/main.yml b/roles/caddy/defaults/main.yml new file mode 100644 index 0000000..110de89 --- /dev/null +++ b/roles/caddy/defaults/main.yml @@ -0,0 +1,7 @@ +--- +caddy_podman_image_name: 'docker.io/library/caddy' +caddy_podman_image_tag: 'latest' + +caddy_conf_path: '/etc/caddy' + +caddy_use_local_certs: false diff --git a/roles/caddy/handlers/main.yml b/roles/caddy/handlers/main.yml new file mode 100644 index 0000000..0fcabb5 --- /dev/null +++ b/roles/caddy/handlers/main.yml @@ -0,0 +1,23 @@ +--- +- name: 'Reload caddy services' + become: true + ansible.builtin.service: + daemon-reload: true + +- name: 'Restart caddy image' + become: true + ansible.builtin.service: + name: 'caddy-image.service' + state: 'restarted' + +- name: 'Restart caddy volume' + become: true + ansible.builtin.service: + name: 'caddy-data-volume.service' + state: 'restarted' + +- name: 'Restart caddy container' + become: true + ansible.builtin.service: + name: 'caddy.service' + state: 'restarted' diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml new file mode 100644 index 0000000..1fad8bf --- /dev/null +++ b/roles/caddy/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: 'Ensure required software is installed' + become: true + ansible.builtin.apt: + name: 'podman' + state: 'present' + +- name: 'Define caddy image' + become: true + containers.podman.podman_image: + name: '{{ caddy_podman_image_name }}:{{ caddy_podman_image_tag }}' + state: 'quadlet' + notify: + - 'Reload caddy services' + - 'Restart caddy image' + +- name: 'Define caddy data volume' + become: true + containers.podman.podman_volume: + name: 'caddy-data' + state: 'quadlet' + notify: + - 'Reload caddy services' + - 'Restart caddy volume' + +- name: 'Create caddy conf directory' + become: true + ansible.builtin.file: + name: '{{ caddy_conf_path }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 'u=rwx,g=rx,o=rx' + +- name: 'Generate Caddyfile' + become: true + ansible.builtin.template: + src: 'Caddyfile.j2' + dest: '{{ caddy_conf_path }}/Caddyfile' + owner: 'root' + group: 'root' + mode: 'u=rw,g=r,o=r' + notify: + - 'Restart caddy container' + +- name: 'Create caddy container' + become: true + containers.podman.podman_container: + name: 'caddy' + image: 'caddy.image' + network: '{{ caddy_networks }}' + state: 'quadlet' + volume: + - '{{ caddy_conf_path }}:/etc/caddy' + - 'caddy-data.volume:/data' + publish: + - '80:80' + - '443:443' + quadlet_options: | + [Install] + WantedBy=default.target + notify: + - 'Reload caddy services' + - 'Restart caddy container' diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 new file mode 100644 index 0000000..fdc026b --- /dev/null +++ b/roles/caddy/templates/Caddyfile.j2 @@ -0,0 +1,17 @@ +{% if caddy_use_local_certs %} +{ + local_certs +} +{% endif %} + +{% for site in caddy_sites %} +{{ site.name }} { +{% if site.directives is defined %} + {{ site.directives | indent }} +{% endif %} +{% if site.proxy_to is defined %} + reverse_proxy {{ site.proxy_to }} +{% endif %} +} + +{% endfor %} diff --git a/roles/isso/defaults/main.yml b/roles/isso/defaults/main.yml new file mode 100644 index 0000000..aa5644d --- /dev/null +++ b/roles/isso/defaults/main.yml @@ -0,0 +1,5 @@ +--- +isso_image_name: 'ghcr.io/isso-comments/isso' +isso_image_tag: 'release' + +isso_storage_path: '/srv/www/isso' diff --git a/roles/isso/handlers/main.yml b/roles/isso/handlers/main.yml new file mode 100644 index 0000000..c639bf4 --- /dev/null +++ b/roles/isso/handlers/main.yml @@ -0,0 +1,17 @@ +--- +- name: 'Reload isso services' + become: true + ansible.builtin.service: + daemon-reload: true + +- name: 'Restart isso image' + become: true + ansible.builtin.service: + name: 'isso-image.service' + state: 'restarted' + +- name: 'Restart isso container' + become: true + ansible.builtin.service: + name: 'isso.service' + state: 'restarted' diff --git a/roles/isso/tasks/main.yml b/roles/isso/tasks/main.yml new file mode 100644 index 0000000..2c23d46 --- /dev/null +++ b/roles/isso/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: 'Ensure required software is installed' + become: true + ansible.builtin.apt: + name: 'podman' + state: 'present' + +- name: 'Create podman network' + become: true + containers.podman.podman_network: + name: '{{ isso_network }}' + ipv6: true + state: 'quadlet' + +- name: 'Define isso image' + become: true + containers.podman.podman_image: + name: '{{ isso_image_name }}:{{ isso_image_tag }}' + state: 'quadlet' + notify: + - 'Reload isso services' + - 'Restart isso image' + +- name: 'Prepare isso data directories' + become: true + ansible.builtin.file: + name: '{{ item }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 'u=rwx,g=rx,o=rx' + loop: + - '{{ isso_storage_path }}/data' + - '{{ isso_storage_path }}/cfg' + +- name: 'Write isso configuration' + become: true + ansible.builtin.template: + src: 'isso.cfg.j2' + dest: '{{ isso_storage_path }}/cfg/isso.cfg' + owner: 'root' + group: 'root' + mode: 'u=rw,g=r,o=r' + notify: + - 'Restart isso container' + +- name: 'Create caddy container' + become: true + containers.podman.podman_container: + name: 'isso' + image: 'isso.image' + network: '{{ isso_network }}.network' + state: 'quadlet' + volume: + - '{{ isso_storage_path }}/data:/db' + - '{{ isso_storage_path }}/cfg:/config' + quadlet_options: | + [Install] + WantedBy=default.target + notify: + - 'Reload isso services' + - 'Restart isso container' diff --git a/roles/isso/templates/isso.cfg.j2 b/roles/isso/templates/isso.cfg.j2 new file mode 100644 index 0000000..59aba5f --- /dev/null +++ b/roles/isso/templates/isso.cfg.j2 @@ -0,0 +1,88 @@ +[general] +; file location to the SQLite3 database +dbpath = /db/isso.db +;required to dispatch multiple websites, not used otherwise +name = isso +; Your website(s) +; If Isso is unable to connect to at least on site, you’ll get a warning during startup and comments are most likely non-functional. +host = + {{ isso_cfg_host }} +; time range that allows users to edit/remove their own comments +max-age = 15m +; Select notification backend(s) for new comments, separated by comma. Available backends: stdout, smtp +notify = smtp +; Log console messages to file instead of standard out. +log-file = + +[moderation] +; enable comment moderation queue +enabled = true +; remove unprocessed comments in moderation queue after given time +purge-after = 30d + +[server] +; interface to listen on. Isso supports TCP/IP and unix domain sockets: +; Does not apply for uWSGI. +listen = http://localhost:8080 +; reload application, when the source code has changed. +; Useful for development. Only works with the internal webserver. +reload = off +; show 10 most time consuming function in Isso after each request. +; Do not use in production. +profile = off + +[smtp] +; self-explanatory, optional +username = {{ isso_mail_username }} +; self-explanatory (yes, plain text, create a dedicated account for notifications), optional. +password = {{ isso_mail_password }} +; SMTP server +host = {{ isso_mail_host }} +; SMTP port +port = {{ isso_mail_port }} +; use a secure connection to the server, possible values: none, starttls or ssl +security = {{ isso_mail_security }} +; recipient address, e.g. your email address +to = {{ isso_mail_rcpt }} +; sender address, e.g. “Foo Bar” +from = {{ isso_mail_from }} +; specify a timeout in seconds for blocking operations like the connection attempt. +timeout = 10 + +[guard] +; enable guard, recommended in production. Not useful for debugging purposes. +enabled = true +; limit to N new comments per minute. +ratelimit = 2 +; how many comments directly to the thread +direct-reply = 3 +; allow commenters to reply to their own comments when they could still edit the comment. +; After the editing timeframe is gone, commenters can reply to their own comments anyways. +reply-to-self = true +; force commenters to enter a value into the author field. No validation is performed on the provided value. +require-author = true +; force commenters to enter a value into the email field. No validation is performed on the provided value. +require-email = true + +[markup] +; Misaka-specific Markdown extensions, all flags starting with EXT_ can be used there, separated by comma. +options = strikethrough, superscript, autolink +; Additional HTML tags to allow in the generated output, comma-separated. +; By default, only a, blockquote, br, code, del, em, h1, h2, h3, h4, h5, h6, hr, +; ins, li, ol, p, pre, strong, table, tbody, td, th, thead and ul are allowed. +allowed-elements = +; Additional HTML attributes (independent from elements) to allow in the generated output, +; comma-separated. By default, only align and href are allowed. +allowed-attributes = + +[hash] +; A salt is used to protect against rainbow tables. Isso does not make use of pepper (yet). +; The default value has been in use since the release of Isso and generates the +; same identicons for same addresses across installations. +salt = Eech7co8Ohloopo9Ol6baimi +; Hash algorithm to use – either from Python’s hashlib or PBKDF2 +algorithm = pbkdf2 + +[admin] +enabled = {{ isso_admin_enabled }} +password = {{ isso_admin_password }} diff --git a/roles/static/defaults/main.yml b/roles/static/defaults/main.yml new file mode 100644 index 0000000..7b0d90f --- /dev/null +++ b/roles/static/defaults/main.yml @@ -0,0 +1,3 @@ +--- +static_caddy_image_name: 'docker.io/library/caddy' +static_caddy_image_tag: 'latest' diff --git a/roles/static/handlers/main.yml b/roles/static/handlers/main.yml new file mode 100644 index 0000000..dfc2fa4 --- /dev/null +++ b/roles/static/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: 'Reload caddy services' + become: true + ansible.builtin.service: + daemon-reload: true + +- name: 'Restart caddy image' + become: true + ansible.builtin.service: + name: 'caddy-image.service' + state: 'restarted' diff --git a/roles/static/tasks/deploy_static_site.yml b/roles/static/tasks/deploy_static_site.yml new file mode 100644 index 0000000..d9a5274 --- /dev/null +++ b/roles/static/tasks/deploy_static_site.yml @@ -0,0 +1,93 @@ +--- +- name: 'Create site network' + become: true + containers.podman.podman_network: + name: '{{ item.network }}' + ipv6: true + state: 'quadlet' + +- name: 'Define caddy image' + become: true + containers.podman.podman_image: + name: '{{ static_caddy_image_name }}:{{ static_caddy_image_tag }}' + state: 'quadlet' + notify: + - 'Reload caddy services' + - 'Restart caddy image' + +- name: 'Define site data volume' + become: true + containers.podman.podman_volume: + name: '{{ item.volume }}' + state: 'quadlet' + notify: + - 'Reload caddy services' + register: 'static_caddy_volume_changed' + +- name: 'Clean the handlers' + ansible.builtin.meta: 'flush_handlers' + +- name: 'Restart caddy static volume' + become: true + when: 'static_caddy_volume_changed.changed' # noqa: no-handler + ansible.builtin.service: + name: '{{ item.volume }}-volume.service' + state: 'restarted' + +- name: 'Create target directory for site data' + become: true + ansible.builtin.file: + name: '/srv/www/{{ item.name }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 'u=rwx,g=rx,o=rx' + +- name: 'Download site data archive from url' + become: true + when: 'not ansible_check_mode' + ansible.builtin.get_url: + dest: '{{ item.tempfile }}' + url: '{{ item.archive_url }}' + url_username: '{{ item.archive_username }}' + url_password: '{{ item.archive_password }}' + force_basic_auth: true + owner: 'root' + group: 'root' + mode: 'u=rw,g=,o=' + register: 'static_caddy_download_data' + +- name: 'Unarchive site data' + when: 'static_caddy_download_data.changed' # noqa: no-handler + become: true + ansible.builtin.unarchive: + src: '{{ item.tempfile }}' + dest: '/srv/www/{{ item.name }}' + remote_src: true + +- name: 'Create site container' + become: true + containers.podman.podman_container: + name: 'caddy-static-{{ item.name }}' + image: 'caddy.image' + network: '{{ item.network }}.network' + state: 'quadlet' + volume: + - '/srv/www/{{ item.name }}/:/usr/share/caddy/' + - '{{ item.volume }}.volume:/data' + quadlet_options: | + [Install] + WantedBy=default.target + notify: + - 'Reload caddy services' + register: 'static_caddy_container_create' + +- name: 'Clean the handlers' + ansible.builtin.meta: 'flush_handlers' + +- name: 'Restart caddy container' + when: 'static_caddy_container_create.changed' # noqa: no-handler + become: true + ansible.builtin.service: + name: 'caddy-static-{{ item.name }}.service' + state: 'restarted' diff --git a/roles/static/tasks/main.yml b/roles/static/tasks/main.yml new file mode 100644 index 0000000..ce3cb8e --- /dev/null +++ b/roles/static/tasks/main.yml @@ -0,0 +1,13 @@ +--- +- name: 'Ensure required software is installed' + become: true + ansible.builtin.apt: + name: '{{ item }}' + state: 'present' + loop: + - 'podman' + - 'unzip' + +- name: 'Deploy static sites' + ansible.builtin.include_tasks: 'deploy_static_site.yml' + loop: '{{ static_caddy_sites }}'