├── .ansible-lint ├── .flake8 ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .yamllint ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── bin └── generate ├── defaults ├── main.yml └── main.yml.base ├── example.yml ├── files └── nginx.default ├── handlers └── main.yml ├── library ├── dokku_acl_app.py ├── dokku_acl_service.py ├── dokku_app.py ├── dokku_builder.py ├── dokku_certs.py ├── dokku_checks.py ├── dokku_clone.py ├── dokku_config.py ├── dokku_docker_options.py ├── dokku_domains.py ├── dokku_git_sync.py ├── dokku_global_cert.py ├── dokku_http_auth.py ├── dokku_image.py ├── dokku_letsencrypt.py ├── dokku_network.py ├── dokku_network_property.py ├── dokku_ports.py ├── dokku_proxy.py ├── dokku_ps_scale.py ├── dokku_registry.py ├── dokku_resource_limit.py ├── dokku_resource_reserve.py ├── dokku_service_create.py ├── dokku_service_link.py └── dokku_storage.py ├── meta └── main.yml ├── module_utils ├── dokku_app.py ├── dokku_git.py └── dokku_utils.py ├── molecule └── default │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── requirements.txt └── tasks ├── dokku-daemon.yml ├── init.yml ├── install-pin.yml ├── install.yml ├── main.yml ├── nginx.yml ├── ssh-key.yml └── ssh-keys.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | # use `# noqa xxx` at the end of a line, to ignore a particular error 2 | # or add to the skip_list/warn_list, to ignore for the whole project 3 | skip_list: 4 | - name 5 | - fqcn-builtins 6 | - yaml[line-length] 7 | - risky-shell-pipe 8 | 9 | warn_list: 10 | - role-name 11 | 12 | # it appears that for errors related to missing roles/modules 13 | # (internal-error, syntax-check), the "# noqa" task/line-based approach of 14 | # skipping rules has no effect, which forces us to skip the entire file. 15 | exclude_paths: 16 | - molecule/default/converge.yml 17 | - molecule/default/verify.yml 18 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | galaxy-name: "dokku_bot.ansible_dokku" 9 | 10 | 11 | jobs: 12 | 13 | pre-commit: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.12" 22 | - uses: pre-commit/action@v3.0.1 23 | 24 | readme: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: "3.12" 34 | 35 | - name: Install requirements 36 | run: pip install -r requirements.txt 37 | 38 | - name: Update README 39 | run: | 40 | set -e; 41 | make generate; 42 | if [[ $(git diff) ]]; then 43 | echo "Please run `make generate`"; 44 | git status --short; 45 | git diff; 46 | exit 1; 47 | fi 48 | 49 | molecule: 50 | runs-on: ubuntu-latest 51 | 52 | strategy: 53 | matrix: 54 | distro: [ubuntu2004, ubuntu2204, ubuntu2404, debian11, debian12] 55 | fail-fast: false 56 | 57 | steps: 58 | 59 | - uses: actions/checkout@v4 60 | with: 61 | path: ${{ env.galaxy-name }} 62 | 63 | - name: Set up Python 3.12 64 | uses: actions/setup-python@v5 65 | with: 66 | python-version: "3.12" 67 | 68 | - name: Upgrade pip 69 | run: | 70 | pip install --upgrade pip wheel 71 | pip --version 72 | 73 | - name: Install requirements 74 | run: | 75 | pip install -r requirements.txt 76 | working-directory: ${{ env.galaxy-name }} 77 | 78 | - name: Create role requirements 79 | run: make ansible-role-requirements.yml 80 | working-directory: ${{ env.galaxy-name }} 81 | 82 | # See https://github.com/geerlingguy/raspberry-pi-dramble/issues/166 83 | - name: Force GitHub Actions' docker daemon to use vfs. 84 | run: | 85 | sudo apt install -y jq 86 | sudo systemctl stop docker 87 | echo '{ "exec-opts": ["native.cgroupdriver=cgroupfs"], "cgroup-parent": "/actions_job", "storage-driver":"vfs"}' | sudo tee /etc/docker/daemon.json 88 | sudo systemctl start docker 89 | 90 | - name: Run molecule 91 | run: molecule test 92 | working-directory: ${{ env.galaxy-name }} 93 | env: 94 | MOLECULE_DISTRO: ${{ matrix.distro }} 95 | 96 | release: 97 | name: Publish to ansible-galaxy 98 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 99 | needs: [pre-commit, molecule] 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v4 103 | - uses: robertdebock/galaxy-action@1.2.1 104 | with: 105 | galaxy_api_key: ${{ secrets.GALAXY_API_KEY }} 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ansible-role-requirements.yml 2 | .idea/ 3 | *swp 4 | .vscode 5 | .venv 6 | .python-version -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # For use with pre-commit. 2 | # See usage instructions at https://pre-commit.com 3 | repos: 4 | 5 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 6 | rev: v2.12.0 7 | hooks: 8 | - id: pretty-format-yaml 9 | args: [--autofix, --indent, "2", --preserve-quotes] 10 | 11 | - repo: https://github.com/adrienverge/yamllint 12 | rev: v1.35.0 13 | hooks: 14 | - id: yamllint 15 | 16 | - repo: https://github.com/psf/black 17 | rev: 24.2.0 18 | hooks: 19 | - id: black 20 | 21 | - repo: https://github.com/pycqa/flake8 22 | rev: 7.0.0 23 | hooks: 24 | - id: flake8 25 | 26 | - repo: https://github.com/ansible/ansible-lint 27 | rev: v24.9.2 28 | hooks: 29 | - id: ansible-lint 30 | files: \.(yaml|yml)$ 31 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | rules: 3 | document-start: disable 4 | braces: 5 | max-spaces-inside: 1 6 | level: error 7 | brackets: 8 | max-spaces-inside: 1 9 | level: error 10 | line-length: 11 | max: 160 12 | level: warning 13 | truthy: disable 14 | indentation: 15 | spaces: 2 16 | indent-sequences: false 17 | ignore: | 18 | default/roles 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | Contributions to the the Dokku open source project are highly welcome! 5 | For general hints see the project-wide [contributing guide](https://github.com/dokku/.github/blob/master/CONTRIBUTING.md). 6 | 7 | ## Codebase overview 8 | 9 | * The role's directory layout follows [standard Ansible practices](https://galaxy.ansible.com/docs/contributing/creating_role.html#roles). 10 | * Besides the yaml-based ansible instructions, the role includes several new Ansible *modules* in the `library/` folder (e.g. `dokku_app`). 11 | * The `README.md` of this repository is auto-generated: do *not* edit it directly. 12 | In order to update it, run `make generate`. 13 | 14 | ## Setting up a test environment 15 | 16 | This role is tested using [molecule](https://molecule.readthedocs.io/en/latest/). 17 | Setting up a test environment involves the following steps: 18 | 19 | * Install [docker](https://www.docker.com/) 20 | * Install [python](https://www.python.org/) 21 | * (optional) Create a python virtual environment 22 | * Run `pip install -r requirements.txt` 23 | * Run `pre-commit install` 24 | * Run `make generate` 25 | 26 | After this, you'll be able to test any changes made to the role using: 27 | 28 | ``` 29 | molecule test 30 | ``` 31 | This will ensure that: 32 | 33 | * the role adheres to coding standards (via `yamllint`, `ansible-lint`, `flake8` and `black` pre-commit hooks) 34 | * the role runs fine (with default parameters) 35 | * the role is idempotent (with default parameters) 36 | * any tests defined in `molecule/default/verify.yml` pass 37 | 38 | In addition to local testing, continuous integration tests on a selection of Ubuntu and Debian versions are run on any pull request. 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 Jose Diaz-Gonzalez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | rm -f README.md defaults/main.yml ../dokku.tar.gz 4 | 5 | .PHONY: release 6 | release: generate 7 | cd .. && tar -zcvf dokku-$(shell cat meta/main.yml | grep version | head -n 1 | cut -d':' -f2 | xargs).tar.gz dokku 8 | 9 | .PHONY: generate 10 | generate: clean README.md defaults/main.yml ansible-role-requirements.yml 11 | 12 | .PHONY: README.md 13 | README.md: 14 | bin/generate 15 | 16 | .PHONY: defaults/main.yml 17 | defaults/main.yml: 18 | bin/generate 19 | 20 | .PHONY: ansible-role-requirements.yml 21 | ansible-role-requirements.yml: 22 | bin/generate 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: Dokku 2 | 3 | [![Ansible Role](https://img.shields.io/ansible/role/d/dokku_bot/ansible_dokku)](https://galaxy.ansible.com/dokku_bot/ansible_dokku) [![Release](https://img.shields.io/github/release/dokku/ansible-dokku.svg)](https://github.com/dokku/ansible-dokku/releases) [![Build Status](https://github.com/dokku/ansible-dokku/workflows/CI/badge.svg)](https://github.com/dokku/ansible-dokku/actions) 4 | 5 | This Ansible role helps install Dokku on Debian/Ubuntu variants. Apart 6 | from installing Dokku, it also provides various modules that can be 7 | used to interface with dokku from your own Ansible playbooks. 8 | 9 | 10 | ## Table Of Contents 11 | 12 | - [Requirements](#requirements) 13 | - [Dependencies](#dependencies) 14 | - [Role Variables](#role-variables) 15 | - [Libraries](#libraries) 16 | - [Example Playbooks](#example-playbooks) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | 20 | ## Requirements 21 | 22 | Minimum Ansible Version: 2.2 23 | 24 | ### Platform Requirements 25 | 26 | Supported Platforms 27 | 28 | - Ubuntu: noble 29 | - Ubuntu: jammy 30 | - Ubuntu: focal 31 | - Debian: bookworm 32 | - Debian: bullseye 33 | 34 | ## Dependencies 35 | 36 | - geerlingguy.docker ansible role 37 | - nginxinc.nginx ansible role 38 | - Dokku (for library usage) 39 | 40 | ## Role Variables 41 | 42 | ### dokku_daemon_install 43 | 44 | - default: `True` 45 | - type: `boolean` 46 | - description: Whether to install the dokku-daemon 47 | 48 | ### dokku_daemon_version 49 | 50 | - default: `0.0.2` 51 | - type: `string` 52 | - description: The version of dokku-daemon to install 53 | 54 | ### dokku_hostname 55 | 56 | - default: `dokku.me` 57 | - type: `string` 58 | - description: Hostname, used as vhost domain and for showing app URL after deploy 59 | 60 | ### dokku_key_file 61 | 62 | - default: `/root/.ssh/id_rsa.pub` 63 | - type: `string` 64 | - description: Path on disk to an SSH key to add to the Dokku user (Will be ignored on `dpkg-reconfigure`) 65 | 66 | ### dokku_manage_nginx 67 | 68 | - default: `True` 69 | - type: `boolean` 70 | - description: Whether we should manage the 00-default nginx site 71 | 72 | ### dokku_packages_state 73 | 74 | - default: `present` 75 | - type: `string` 76 | - description: State of dokku packages. Accepts 'present' and 'latest' 77 | 78 | ### dokku_plugins 79 | 80 | - default: `{}` 81 | - type: `list` 82 | - description: A list of plugins to install. The host _must_ have network access to the install url, and git access if required. Plugins should be specified in the following format: 83 | 84 | ```yaml 85 | - name: postgres 86 | url: https://github.com/dokku/dokku-postgres.git 87 | 88 | - name: redis 89 | url: https://github.com/dokku/dokku-redis.git 90 | ``` 91 | 92 | ### dokku_skip_key_file 93 | 94 | - default: `false` 95 | - type: `string` 96 | - description: Do not check for the existence of the dokku/key_file. Setting this to "true", will require you to manually add an SSH key later on. 97 | 98 | ### dokku_users 99 | 100 | - default: `null` 101 | - type: `list` 102 | - description: A list of users who should have access to Dokku. This will _not_ grant them generic SSH access, but rather only access as the `dokku` user. Users should be specified in the following format: 103 | 104 | ```yaml 105 | - name: Jane Doe 106 | username: jane 107 | ssh_key: JANES_PUBLIC_SSH_KEY 108 | - name: Camilla 109 | username: camilla 110 | ssh_key: CAMILLAS_PUBLIC_SSH_KEY 111 | ``` 112 | 113 | ### dokku_version (deprecated) 114 | 115 | - default: `''` 116 | - type: `string` 117 | - description: The version of dokku to install. 118 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 119 | 120 | ### dokku_vhost_enable 121 | 122 | - default: `true` 123 | - type: `string` 124 | - description: Use vhost-based deployments (e.g., .dokku.me) 125 | 126 | ### dokku_web_config 127 | 128 | - default: `false` 129 | - type: `string` 130 | - description: Use web-based config for hostname and keyfile 131 | 132 | ### herokuish_version (deprecated) 133 | 134 | - default: `''` 135 | - type: `string` 136 | - description: The version of herokuish to install. 137 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 138 | 139 | ### plugn_version (deprecated) 140 | 141 | - default: `''` 142 | - type: `string` 143 | - description: The version of plugn to install. 144 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 145 | 146 | ### sshcommand_version (deprecated) 147 | 148 | - default: `''` 149 | - type: `string` 150 | - description: The version of sshcommand to install. 151 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 152 | 153 | ## Libraries 154 | 155 | ### dokku_acl_app 156 | 157 | Manage access control list for a given dokku application 158 | 159 | #### Requirements 160 | 161 | - the `dokku-acl` plugin 162 | 163 | #### Parameters 164 | 165 | |Parameter|Choices/Defaults|Comments| 166 | |---------|----------------|--------| 167 | |app
*required*||The name of the app| 168 | |state|*Choices:* |Whether the ACLs should be present or absent| 169 | |users
*required*||The list of users who can manage the app| 170 | 171 | #### Example 172 | 173 | ```yaml 174 | - name: let leopold manage hello-world 175 | dokku_acl_app: 176 | app: hello-world 177 | users: 178 | - leopold 179 | - name: remove leopold from hello-world 180 | dokku_acl_app: 181 | app: hello-world 182 | users: 183 | - leopold 184 | state: absent 185 | ``` 186 | 187 | ### dokku_acl_service 188 | 189 | Manage access control list for a given dokku service 190 | 191 | #### Requirements 192 | 193 | - the `dokku-acl` plugin 194 | 195 | #### Parameters 196 | 197 | |Parameter|Choices/Defaults|Comments| 198 | |---------|----------------|--------| 199 | |service
*required*||The name of the service| 200 | |state|*Choices:* |Whether the ACLs should be present or absent| 201 | |type
*required*||The type of the service| 202 | |users
*required*||The list of users who can manage the service| 203 | 204 | #### Example 205 | 206 | ```yaml 207 | - name: let leopold manage mypostgres postgres service 208 | dokku_acl_service: 209 | service: mypostgres 210 | type: postgres 211 | users: 212 | - leopold 213 | - name: remove leopold from mypostgres postgres service 214 | dokku_acl_service: 215 | service: hello-world 216 | type: postgres 217 | users: 218 | - leopold 219 | state: absent 220 | ``` 221 | 222 | ### dokku_app 223 | 224 | Create or destroy dokku apps 225 | 226 | #### Parameters 227 | 228 | |Parameter|Choices/Defaults|Comments| 229 | |---------|----------------|--------| 230 | |app
*required*||The name of the app| 231 | |state|*Choices:* |The state of the app| 232 | 233 | #### Example 234 | 235 | ```yaml 236 | - name: Create a dokku app 237 | dokku_app: 238 | app: hello-world 239 | 240 | - name: Delete that repo 241 | dokku_app: 242 | app: hello-world 243 | state: absent 244 | ``` 245 | 246 | ### dokku_builder 247 | 248 | Manage the builder configuration for a given dokku application 249 | 250 | #### Parameters 251 | 252 | |Parameter|Choices/Defaults|Comments| 253 | |---------|----------------|--------| 254 | |app
*required*||The name of the app. This is required only if global is set to False.| 255 | |global|*Default:* False|If the property being set is global| 256 | |property
*required*||The property to be changed (e.g., `build-dir`, `selected`)| 257 | |value||The value of the builder property (leave empty to unset)| 258 | 259 | #### Example 260 | 261 | ```yaml 262 | - name: Overriding the auto-selected builder 263 | dokku_builder: 264 | app: node-js-app 265 | property: selected 266 | value: dockerfile 267 | - name: Setting the builder to the default value 268 | dokku_builder: 269 | app: node-js-app 270 | property: selected 271 | - name: Changing the build build directory 272 | dokku_builder: 273 | app: monorepo 274 | property: build-dir 275 | value: backend 276 | - name: Overriding the auto-selected builder globally 277 | dokku_builder: 278 | global: true 279 | property: selected 280 | value: herokuish 281 | ``` 282 | 283 | ### dokku_certs 284 | 285 | Manages ssl configuration for an app. 286 | 287 | #### Parameters 288 | 289 | |Parameter|Choices/Defaults|Comments| 290 | |---------|----------------|--------| 291 | |app
*required*||The name of the app| 292 | |cert
*required*||Path to the ssl certificate| 293 | |key
*required*||Path to the ssl certificate key| 294 | |state|*Choices:* |The state of the ssl configuration| 295 | 296 | #### Example 297 | 298 | ```yaml 299 | - name: Adds an ssl certificate and key to an app 300 | dokku_certs: 301 | app: hello-world 302 | key: /etc/nginx/ssl/hello-world.key 303 | cert: /etc/nginx/ssl/hello-world.crt 304 | 305 | - name: Removes an ssl certificate and key from an app 306 | dokku_certs: 307 | app: hello-world 308 | state: absent 309 | ``` 310 | 311 | ### dokku_checks 312 | 313 | Manage the Zero Downtime checks for a dokku app 314 | 315 | #### Parameters 316 | 317 | |Parameter|Choices/Defaults|Comments| 318 | |---------|----------------|--------| 319 | |app
*required*||The name of the app| 320 | |state|*Choices:* |The state of the checks functionality| 321 | 322 | #### Example 323 | 324 | ```yaml 325 | - name: Disable the zero downtime deployment 326 | dokku_checks: 327 | app: hello-world 328 | state: absent 329 | 330 | - name: Re-enable the zero downtime deployment (enabled by default) 331 | dokku_checks: 332 | app: hello-world 333 | state: present 334 | ``` 335 | 336 | ### dokku_clone 337 | 338 | Clone a git repository and deploy app. 339 | 340 | #### Parameters 341 | 342 | |Parameter|Choices/Defaults|Comments| 343 | |---------|----------------|--------| 344 | |app
*required*||The name of the app| 345 | |build|*Default:* True|Whether to build the app after cloning.| 346 | |repository
*required*||Git repository url| 347 | |version||Git tree (tag or branch name). If not provided, default branch is used.| 348 | 349 | #### Example 350 | 351 | ```yaml 352 | - name: clone a git repository and build app 353 | dokku_clone: 354 | app: example-app 355 | repository: https://github.com/heroku/node-js-getting-started 356 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 357 | - name: clone specific tag from git repository and build app 358 | dokku_clone: 359 | app: example-app 360 | repository: https://github.com/heroku/node-js-getting-started 361 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 362 | - name: sync git repository without building app 363 | dokku_clone: 364 | app: example-app 365 | repository: https://github.com/heroku/node-js-getting-started 366 | build: false 367 | ``` 368 | 369 | ### dokku_config 370 | 371 | Manage environment variables for a given dokku application 372 | 373 | #### Parameters 374 | 375 | |Parameter|Choices/Defaults|Comments| 376 | |---------|----------------|--------| 377 | |app
*required*||The name of the app| 378 | |config
*required*|*Default:* {}|A map of environment variables where key => value| 379 | |restart|*Default:* True|Whether to restart the application or not. If the task is idempotent then setting restart to true will not perform a restart.| 380 | 381 | #### Example 382 | 383 | ```yaml 384 | - name: set KEY=VALUE 385 | dokku_config: 386 | app: hello-world 387 | config: 388 | KEY: VALUE_1 389 | KEY_2: VALUE_2 390 | 391 | - name: set KEY=VALUE without restart 392 | dokku_config: 393 | app: hello-world 394 | restart: false 395 | config: 396 | KEY: VALUE_1 397 | KEY_2: VALUE_2 398 | ``` 399 | 400 | ### dokku_docker_options 401 | 402 | Manage docker-options for a given dokku application 403 | 404 | #### Parameters 405 | 406 | |Parameter|Choices/Defaults|Comments| 407 | |---------|----------------|--------| 408 | |app
*required*||The name of the app| 409 | |option
*required*||A single docker option| 410 | |phase|*Choices:* |The phase in which to set the options| 411 | |state|*Choices:* |The state of the docker options| 412 | 413 | #### Example 414 | 415 | ```yaml 416 | - name: docker-options:add hello-world deploy 417 | dokku_docker_options: 418 | app: hello-world 419 | phase: deploy 420 | option: "-v /var/run/docker.sock:/var/run/docker.sock" 421 | 422 | - name: docker-options:remove hello-world deploy 423 | dokku_docker_options: 424 | app: hello-world 425 | phase: deploy 426 | option: "-v /var/run/docker.sock:/var/run/docker.sock" 427 | state: absent 428 | ``` 429 | 430 | ### dokku_domains 431 | 432 | Manages domains for a given application 433 | 434 | #### Parameters 435 | 436 | |Parameter|Choices/Defaults|Comments| 437 | |---------|----------------|--------| 438 | |app
*required*||The name of the app. This is required only if global is set to False.| 439 | |domains
*required*||A list of domains| 440 | |global|*Default:* False|Whether to change the global domains or app-specific domains.| 441 | |state|*Choices:* |The state of the application's domains| 442 | 443 | #### Example 444 | 445 | ```yaml 446 | # Adds domain, inclusive 447 | - name: domains:add hello-world dokku.me 448 | dokku_domains: 449 | app: hello-world 450 | domains: 451 | - dokku.me 452 | 453 | # Removes listed domains, but leaves others unchanged 454 | - name: domains:remove hello-world dokku.me 455 | dokku_domains: 456 | app: hello-world 457 | domains: 458 | - dokku.me 459 | state: absent 460 | 461 | # Clears all domains 462 | - name: domains:clear hello-world 463 | dokku_domains: 464 | app: hello-world 465 | state: clear 466 | 467 | # Enables the VHOST domain 468 | - name: domains:enable hello-world 469 | dokku_domains: 470 | app: hello-world 471 | state: enable 472 | 473 | # Disables the VHOST domain 474 | - name: domains:disable hello-world 475 | dokku_domains: 476 | app: hello-world 477 | state: disable 478 | 479 | # Sets the domain for the app, clearing all others 480 | - name: domains:set hello-world dokku.me 481 | dokku_domains: 482 | app: hello-world 483 | domains: 484 | - dokku.me 485 | state: set 486 | ``` 487 | 488 | ### dokku_git_sync 489 | 490 | Manages syncing git code from a remote repository for an app 491 | 492 | #### Requirements 493 | 494 | - the `dokku-git-sync` plugin (_commercial_) 495 | 496 | #### Parameters 497 | 498 | |Parameter|Choices/Defaults|Comments| 499 | |---------|----------------|--------| 500 | |app
*required*||The name of the app| 501 | |remote||The git remote url to use| 502 | |state|*Choices:* |The state of the git-sync integration| 503 | 504 | #### Example 505 | 506 | ```yaml 507 | - name: git-sync:enable hello-world 508 | dokku_git_sync: 509 | app: hello-world 510 | remote: git@github.com:hello-world/hello-world.git 511 | 512 | - name: git-sync:disable hello-world 513 | dokku_git_sync: 514 | app: hello-world 515 | state: absent 516 | ``` 517 | 518 | ### dokku_global_cert 519 | 520 | Manages global ssl configuration. 521 | 522 | #### Requirements 523 | 524 | - the `dokku-global-cert` plugin 525 | 526 | #### Parameters 527 | 528 | |Parameter|Choices/Defaults|Comments| 529 | |---------|----------------|--------| 530 | |cert
*required*||Path to the ssl certificate| 531 | |key
*required*||Path to the ssl certificate key| 532 | |state|*Choices:* |The state of the ssl configuration| 533 | 534 | #### Example 535 | 536 | ```yaml 537 | - name: Adds an ssl certificate and key 538 | dokku_global_cert: 539 | key: /etc/nginx/ssl/global-hello-world.key 540 | cert: /etc/nginx/ssl/global-hello-world.crt 541 | 542 | - name: Removes an ssl certificate and key 543 | dokku_global_cert: 544 | state: absent 545 | ``` 546 | 547 | ### dokku_http_auth 548 | 549 | Manage HTTP Basic Authentication for a dokku app 550 | 551 | #### Requirements 552 | 553 | - the `dokku-http-auth` plugin 554 | 555 | #### Parameters 556 | 557 | |Parameter|Choices/Defaults|Comments| 558 | |---------|----------------|--------| 559 | |app
*required*||The name of the app| 560 | |password||The HTTP Auth Password (required for 'present' state)| 561 | |state|*Choices:* |The state of the http-auth plugin| 562 | |username||The HTTP Auth Username (required for 'present' state)| 563 | 564 | #### Example 565 | 566 | ```yaml 567 | - name: Enable the http-auth plugin 568 | dokku_http_auth: 569 | app: hello-world 570 | state: present 571 | username: samsepi0l 572 | password: hunter2 573 | 574 | - name: Disable the http-auth plugin 575 | dokku_http_auth: 576 | app: hello-world 577 | state: absent 578 | ``` 579 | 580 | ### dokku_image 581 | 582 | Pull Docker image and deploy app 583 | 584 | #### Parameters 585 | 586 | |Parameter|Choices/Defaults|Comments| 587 | |---------|----------------|--------| 588 | |app
*required*||The name of the app| 589 | |build_dir||Specify custom build directory for a custom build context| 590 | |image
*required*||Docker image| 591 | |user_email||Git user.email for customizing the author's email| 592 | |user_name||Git user.name for customizing the author's name| 593 | 594 | #### Example 595 | 596 | ```yaml 597 | - name: Pull and deploy meilisearch 598 | dokku_image: 599 | app: meilisearch 600 | image: getmeili/meilisearch:v0.24.0rc1 601 | - name: Pull and deploy image with custom author 602 | dokku_image: 603 | app: hello-world 604 | user_name: Elliot Alderson 605 | user_email: elliotalderson@protonmail.ch 606 | image: hello-world:latest 607 | - name: Pull and deploy image with custom build dir 608 | dokku_image: 609 | app: hello-world 610 | build_dir: /path/to/build 611 | image: hello-world:latest 612 | ``` 613 | 614 | ### dokku_letsencrypt 615 | 616 | Enable or disable the letsencrypt plugin for a dokku app 617 | 618 | #### Requirements 619 | 620 | - the `dokku-letsencrypt` plugin 621 | 622 | #### Parameters 623 | 624 | |Parameter|Choices/Defaults|Comments| 625 | |---------|----------------|--------| 626 | |app
*required*||The name of the app| 627 | |state|*Choices:* |The state of the letsencrypt plugin| 628 | 629 | #### Example 630 | 631 | ```yaml 632 | - name: Enable the letsencrypt plugin 633 | dokku_letsencrypt: 634 | app: hello-world 635 | 636 | - name: Disable the letsencrypt plugin 637 | dokku_letsencrypt: 638 | app: hello-world 639 | state: absent 640 | ``` 641 | 642 | ### dokku_network 643 | 644 | Create or destroy container networks for dokku apps 645 | 646 | #### Parameters 647 | 648 | |Parameter|Choices/Defaults|Comments| 649 | |---------|----------------|--------| 650 | |name
*required*||The name of the network| 651 | |state|*Choices:* |The state of the network| 652 | 653 | #### Example 654 | 655 | ```yaml 656 | - name: Create a network 657 | dokku_network: 658 | name: example-network 659 | 660 | - name: Delete that network 661 | dokku_network: 662 | name: example-network 663 | state: absent 664 | ``` 665 | 666 | ### dokku_network_property 667 | 668 | Set or clear a network property for a given dokku application 669 | 670 | #### Parameters 671 | 672 | |Parameter|Choices/Defaults|Comments| 673 | |---------|----------------|--------| 674 | |app
*required*||The name of the app. This is required only if global is set to False.| 675 | |global|*Default:* False|Whether to change the global network property| 676 | |property
*required*||The network property to be be modified. This can be any property supported by dokku (e.g., `initial-network`, `attach-post-create`, `attach-post-deploy`, `bind-all-interfaces`, `static-web-listener`, `tld`).| 677 | |value||The value of the network property (leave empty to unset)| 678 | 679 | #### Example 680 | 681 | ```yaml 682 | - name: Associates a network after a container is created but before it is started 683 | dokku_network_property: 684 | app: hello-world 685 | property: attach-post-create 686 | value: example-network 687 | 688 | - name: Associates the network at container creation 689 | dokku_network_property: 690 | app: hello-world 691 | property: initial-network 692 | value: example-network 693 | 694 | - name: Setting a global network property 695 | dokku_network_property: 696 | global: true 697 | property: attach-post-create 698 | value: example-network 699 | 700 | - name: Clearing a network property 701 | dokku_network_property: 702 | app: hello-world 703 | property: attach-post-create 704 | ``` 705 | 706 | ### dokku_ports 707 | 708 | Manage ports for a given dokku application 709 | 710 | #### Parameters 711 | 712 | |Parameter|Choices/Defaults|Comments| 713 | |---------|----------------|--------| 714 | |app
*required*||The name of the app| 715 | |mappings
*required*||A list of port mappings| 716 | |state|*Choices:* |The state of the port mappings| 717 | 718 | #### Example 719 | 720 | ```yaml 721 | - name: ports:set hello-world http:80:80 722 | dokku_ports: 723 | app: hello-world 724 | mappings: 725 | - http:80:8080 726 | 727 | - name: ports:remove hello-world http:80:80 728 | dokku_ports: 729 | app: hello-world 730 | mappings: 731 | - http:80:8080 732 | state: absent 733 | 734 | - name: ports:clear hello-world 735 | dokku_ports: 736 | app: hello-world 737 | state: clear 738 | ``` 739 | 740 | ### dokku_proxy 741 | 742 | Enable or disable the proxy for a dokku app 743 | 744 | #### Parameters 745 | 746 | |Parameter|Choices/Defaults|Comments| 747 | |---------|----------------|--------| 748 | |app
*required*||The name of the app| 749 | |state|*Choices:* |The state of the proxy| 750 | 751 | #### Example 752 | 753 | ```yaml 754 | - name: Enable the default proxy 755 | dokku_proxy: 756 | app: hello-world 757 | 758 | - name: Disable the default proxy 759 | dokku_proxy: 760 | app: hello-world 761 | state: absent 762 | ``` 763 | 764 | ### dokku_ps_scale 765 | 766 | Manage process scaling for a given dokku application 767 | 768 | #### Parameters 769 | 770 | |Parameter|Choices/Defaults|Comments| 771 | |---------|----------------|--------| 772 | |app
*required*||The name of the app| 773 | |scale
*required*|*Default:* {}|A map of scale values where proctype => qty| 774 | |skip_deploy|*Default:* False|Whether to skip the corresponding deploy or not. If the task is idempotent then leaving skip_deploy as false will not trigger a deploy.| 775 | 776 | #### Example 777 | 778 | ```yaml 779 | - name: scale web and worker processes 780 | dokku_ps_scale: 781 | app: hello-world 782 | scale: 783 | web: 4 784 | worker: 4 785 | 786 | - name: scale web and worker processes without deploy 787 | dokku_ps_scale: 788 | app: hello-world 789 | skip_deploy: true 790 | scale: 791 | web: 4 792 | worker: 4 793 | ``` 794 | 795 | ### dokku_registry 796 | 797 | Manage the registry configuration for a given dokku application 798 | 799 | #### Requirements 800 | 801 | - the `dokku-registry` plugin 802 | 803 | #### Parameters 804 | 805 | |Parameter|Choices/Defaults|Comments| 806 | |---------|----------------|--------| 807 | |app
*required*||The name of the app| 808 | |image||Alternative to app name for image repository name| 809 | |password||The registry password (required for 'present' state)| 810 | |server||The registry server hostname (required for 'present' state)| 811 | |state|*Choices:* |The state of the registry integration| 812 | |username||The registry username (required for 'present' state)| 813 | 814 | #### Example 815 | 816 | ```yaml 817 | - name: registry:enable hello-world 818 | dokku_registry: 819 | app: hello-world 820 | password: password 821 | server: localhost:8080 822 | username: user 823 | 824 | - name: registry:enable hello-world with args 825 | dokku_registry: 826 | app: hello-world 827 | image: other-image 828 | password: password 829 | server: localhost:8080 830 | username: user 831 | 832 | - name: registry:disable hello-world 833 | dokku_registry: 834 | app: hello-world 835 | state: absent 836 | ``` 837 | 838 | ### dokku_resource_limit 839 | 840 | Manage resource limits for a given dokku application 841 | 842 | #### Parameters 843 | 844 | |Parameter|Choices/Defaults|Comments| 845 | |---------|----------------|--------| 846 | |app
*required*||The name of the app| 847 | |clear_before|*Choices:* |Clear all resource limits before applying| 848 | |process_type||The process type selector| 849 | |resources||The Resource type and quantity (required when state=present)| 850 | |state|*Choices:* |The state of the resource limits| 851 | 852 | #### Example 853 | 854 | ```yaml 855 | - name: Limit CPU and memory of a dokku app 856 | dokku_resource_limit: 857 | app: hello-world 858 | resources: 859 | cpu: 100 860 | memory: 100 861 | 862 | - name: name: Limit resources per process type of a dokku app 863 | dokku_resource_limit: 864 | app: hello-world 865 | process_type: web 866 | resources: 867 | cpu: 100 868 | memory: 100 869 | 870 | - name: Clear limits before applying new limits 871 | dokku_resource_limit: 872 | app: hello-world 873 | state: present 874 | clear_before: True 875 | resources: 876 | cpu: 100 877 | memory: 100 878 | 879 | - name: Remove all resource limits 880 | dokku_resource_limit: 881 | app: hello-world 882 | state: absent 883 | ``` 884 | 885 | ### dokku_resource_reserve 886 | 887 | Manage resource reservations for a given dokku application 888 | 889 | #### Parameters 890 | 891 | |Parameter|Choices/Defaults|Comments| 892 | |---------|----------------|--------| 893 | |app
*required*||The name of the app| 894 | |clear_before|*Choices:* |Clear all reserves before apply| 895 | |process_type||The process type selector| 896 | |resources||The Resource type and quantity (required when state=present)| 897 | |state|*Choices:* |The state of the resource reservations| 898 | 899 | #### Example 900 | 901 | ```yaml 902 | - name: Reserve CPU and memory for a dokku app 903 | dokku_resource_reserve: 904 | app: hello-world 905 | resources: 906 | cpu: 100 907 | memory: 100 908 | 909 | - name: Create a reservation per process type of a dokku app 910 | dokku_resource_reserve: 911 | app: hello-world 912 | process_type: web 913 | resources: 914 | cpu: 100 915 | memory: 100 916 | 917 | - name: Clear all reservations before applying 918 | dokku_resource_reserve: 919 | app: hello-world 920 | state: present 921 | clear_before: True 922 | resources: 923 | cpu: 100 924 | memory: 100 925 | 926 | - name: Remove all resource reservations 927 | dokku_resource_reserve: 928 | app: hello-world 929 | state: absent 930 | ``` 931 | 932 | ### dokku_service_create 933 | 934 | Creates a given service 935 | 936 | #### Parameters 937 | 938 | |Parameter|Choices/Defaults|Comments| 939 | |---------|----------------|--------| 940 | |name
*required*||The name of the service| 941 | |service
*required*||The type of service to create| 942 | 943 | #### Example 944 | 945 | ```yaml 946 | - name: redis:create default 947 | dokku_service_create: 948 | name: default 949 | service: redis 950 | 951 | - name: postgres:create default 952 | dokku_service_create: 953 | name: default 954 | service: postgres 955 | 956 | - name: postgres:create default with custom image 957 | environment: 958 | POSTGRES_IMAGE: postgis/postgis 959 | POSTGRES_IMAGE_VERSION: 13-master 960 | dokku_service_create: 961 | name: default 962 | service: postgres 963 | ``` 964 | 965 | ### dokku_service_link 966 | 967 | Links and unlinks a given service to an application 968 | 969 | #### Parameters 970 | 971 | |Parameter|Choices/Defaults|Comments| 972 | |---------|----------------|--------| 973 | |app
*required*||The name of the app| 974 | |name
*required*||The name of the service| 975 | |service
*required*||The type of service to link| 976 | |state|*Choices:* |The state of the service link| 977 | 978 | #### Example 979 | 980 | ```yaml 981 | - name: redis:link default hello-world 982 | dokku_service_link: 983 | app: hello-world 984 | name: default 985 | service: redis 986 | 987 | - name: postgres:link default hello-world 988 | dokku_service_link: 989 | app: hello-world 990 | name: default 991 | service: postgres 992 | 993 | - name: redis:unlink default hello-world 994 | dokku_service_link: 995 | app: hello-world 996 | name: default 997 | service: redis 998 | state: absent 999 | ``` 1000 | 1001 | ### dokku_storage 1002 | 1003 | Manage storage for dokku applications 1004 | 1005 | #### Parameters 1006 | 1007 | |Parameter|Choices/Defaults|Comments| 1008 | |---------|----------------|--------| 1009 | |app||The name of the app| 1010 | |create_host_dir|*Default:* False|Whether to create the host directory or not| 1011 | |group|*Default:* 32767|A group or gid that should own the created folder| 1012 | |mounts|*Default:* []|A list of mounts to create, colon (:) delimited, in the format: `host_dir:container_dir`| 1013 | |state|*Choices:* |The state of the service link| 1014 | |user|*Default:* 32767|A user or uid that should own the created folder| 1015 | 1016 | #### Example 1017 | 1018 | ```yaml 1019 | - name: mount a path 1020 | dokku_storage: 1021 | app: hello-world 1022 | mounts: 1023 | - /var/lib/dokku/data/storage/hello-world:/data 1024 | 1025 | - name: mount a path and create the host_dir directory 1026 | dokku_storage: 1027 | app: hello-world 1028 | mounts: 1029 | - /var/lib/dokku/data/storage/hello-world:/data 1030 | create_host_dir: true 1031 | 1032 | - name: unmount a path 1033 | dokku_storage: 1034 | app: hello-world 1035 | mounts: 1036 | - /var/lib/dokku/data/storage/hello-world:/data 1037 | state: absent 1038 | 1039 | - name: unmount a path and destroy the host_dir directory (and contents) 1040 | dokku_storage: 1041 | app: hello-world 1042 | mounts: 1043 | - /var/lib/dokku/data/storage/hello-world:/data 1044 | destroy_host_dir: true 1045 | state: absent 1046 | ``` 1047 | 1048 | ## Example Playbooks 1049 | 1050 | ### Installing Dokku 1051 | 1052 | ```yaml 1053 | --- 1054 | - hosts: all 1055 | roles: 1056 | - dokku_bot.ansible_dokku 1057 | ``` 1058 | 1059 | ### Installing Plugins 1060 | 1061 | ```yaml 1062 | --- 1063 | - hosts: all 1064 | roles: 1065 | - dokku_bot.ansible_dokku 1066 | vars: 1067 | dokku_plugins: 1068 | - name: clone 1069 | url: https://github.com/crisward/dokku-clone.git 1070 | - name: postgres 1071 | url: https://github.com/dokku/dokku-postgres.git 1072 | ``` 1073 | 1074 | ### Deploying a simple word inflector 1075 | 1076 | ```yaml 1077 | --- 1078 | - hosts: all 1079 | roles: 1080 | - dokku_bot.ansible_dokku 1081 | tasks: 1082 | - name: dokku apps:create inflector 1083 | dokku_app: 1084 | app: inflector 1085 | 1086 | - name: dokku clone inflector 1087 | dokku_clone: 1088 | app: inflector 1089 | repository: https://github.com/cakephp/inflector.cakephp.org 1090 | ``` 1091 | 1092 | ### Setting up a Small VPS with a Dokku App 1093 | 1094 | ```yaml 1095 | --- 1096 | - hosts: all 1097 | roles: 1098 | - dokku_bot.ansible_dokku 1099 | - geerlingguy.swap 1100 | vars: 1101 | # If you are running dokku on a small VPS, you'll most likely 1102 | # need some swap to ensure you don't run out of RAM during deploys 1103 | swap_file_size_mb: '2048' 1104 | dokku_version: 0.19.13 1105 | dokku_users: 1106 | - name: yourname 1107 | username: yourname 1108 | ssh_key: "{{lookup('file', '~/.ssh/id_rsa.pub')}}" 1109 | dokku_plugins: 1110 | - name: clone 1111 | url: https://github.com/crisward/dokku-clone.git 1112 | - name: letsencrypt 1113 | url: https://github.com/dokku/dokku-letsencrypt.git 1114 | tasks: 1115 | - name: create app 1116 | dokku_app: 1117 | # change this name in your template! 1118 | app: &appname appname 1119 | - name: environment configuration 1120 | dokku_config: 1121 | app: *appname 1122 | config: 1123 | # specify port so `domains` can setup the port mapping properly 1124 | PORT: "5000" 1125 | - name: git clone 1126 | # note you'll need to add a deployment key to the GH repo if it's private! 1127 | dokku_clone: 1128 | app: *appname 1129 | repository: git@github.com:heroku/python-getting-started.git 1130 | - name: add domain 1131 | dokku_domains: 1132 | app: *appname 1133 | domains: 1134 | - example.com 1135 | - name: add letsencrypt 1136 | dokku_letsencrypt: 1137 | app: *appname 1138 | ``` 1139 | 1140 | ## Contributing 1141 | 1142 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 1143 | 1144 | ## License 1145 | 1146 | MIT License 1147 | 1148 | See LICENSE.md for further details. 1149 | -------------------------------------------------------------------------------- /bin/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import ast 3 | import os 4 | import yaml 5 | 6 | 7 | def represent_none(self, _): 8 | return self.represent_scalar("tag:yaml.org,2002:null", "") 9 | 10 | 11 | yaml.add_representer(type(None), represent_none) 12 | 13 | 14 | def section_header(text, level=1): 15 | return "{0} {1}".format("#" * level, text) 16 | 17 | 18 | def get_libraries(role_path): 19 | libraries_path = os.path.join(role_path, "library") 20 | files = [] 21 | for dirpath, dirnames, filenames in os.walk(libraries_path): 22 | files.extend(filenames) 23 | break 24 | 25 | data = {} 26 | for f in files: 27 | data[f] = {} 28 | M = ast.parse("".join(open(os.path.join(libraries_path, f)))) 29 | for child in M.body: 30 | if isinstance(child, ast.Assign): 31 | for t in child.targets: 32 | theid = None 33 | try: 34 | theid = t.id 35 | except AttributeError: 36 | print("Failed to assign id for %s on %s, skipping" % (t, f)) 37 | continue 38 | if "DOCUMENTATION" == theid: 39 | data[f]["docs"] = yaml.load( 40 | child.value.s[1:].strip(), Loader=yaml.SafeLoader 41 | ) 42 | elif "EXAMPLES" == theid: 43 | data[f]["examples"] = child.value.s[1:].strip() 44 | 45 | return data 46 | 47 | 48 | def add_table_of_contents(text): 49 | text.append(section_header("Table Of Contents", 2)) 50 | text.append( 51 | "\n".join( 52 | [ 53 | "- [Requirements](#requirements)", 54 | "- [Dependencies](#dependencies)", 55 | "- [Role Variables](#role-variables)", 56 | "- [Libraries](#libraries)", 57 | "- [Example Playbooks](#example-playbooks)", 58 | "- [Contributing](#contributing)", 59 | "- [License](#license)", 60 | ] 61 | ) 62 | ) 63 | return text 64 | 65 | 66 | def add_libraries(text, role_path): 67 | libraries = get_libraries(role_path) 68 | 69 | def chosen(choice, default): 70 | if choice == default: 71 | return "**{0}** (default)".format(choice) 72 | return "{0}".format(choice) 73 | 74 | library_keys = sorted(libraries.keys()) 75 | text.append(section_header("Libraries", 2)) 76 | for library in library_keys: 77 | data = libraries[library] 78 | options = [ 79 | "|Parameter|Choices/Defaults|Comments|", 80 | "|---------|----------------|--------|", 81 | ] 82 | 83 | option_keys = sorted(data["docs"].get("options", {}).keys()) 84 | for parameter in option_keys: 85 | config = data["docs"]["options"][parameter] 86 | choices = config.get("choices", []) 87 | default = config.get("default", None) 88 | 89 | choice_default = [] 90 | if len(choices) > 0: 91 | choices = [ 92 | "
  • {0}
  • ".format(chosen(choice, default)) for choice in choices 93 | ] 94 | choice_default.append( 95 | "*Choices:* ".format("".join(choices)) 96 | ) 97 | elif default is not None: 98 | choice_default.append("*Default:* {0}".format(default)) 99 | 100 | title = parameter 101 | if config.get("required", False): 102 | title = "{0}
    *required*".format(parameter) 103 | 104 | options.append( 105 | "|{0}|{1}|{2}|".format( 106 | title, 107 | "\n".join(choice_default), 108 | "
    ".join(config.get("description")).strip(), 109 | ) 110 | ) 111 | 112 | text.append(section_header(data["docs"]["module"], 3)) 113 | text.append(data["docs"]["short_description"]) 114 | 115 | requirements = data["docs"].get("requirements", []) 116 | if len(requirements) > 0: 117 | text.append(section_header("Requirements", 4)) 118 | requirements = ["- {0}".format(r) for r in requirements] 119 | text.append("{0}".format("\n".join(requirements))) 120 | 121 | if len(options) > 0: 122 | text.append(section_header("Parameters", 4)) 123 | text.append("\n".join(options)) 124 | 125 | text.append(section_header("Example", 4)) 126 | text.append("```yaml\n{0}\n```".format(data["examples"].strip())) 127 | 128 | return text 129 | 130 | 131 | def add_requirements(text, meta, defaults): 132 | text.append(section_header("Requirements", 2)) 133 | text.append( 134 | "Minimum Ansible Version: {0}".format( 135 | meta["galaxy_info"]["min_ansible_version"] 136 | ) 137 | ) 138 | 139 | if len(meta["galaxy_info"]["platforms"]) > 0: 140 | text.append(section_header("Platform Requirements", 3)) 141 | text.append("Supported Platforms") 142 | platforms = [] 143 | for platform in meta["galaxy_info"]["platforms"]: 144 | for v in platform["versions"]: 145 | platforms.append("- {0}: {1}".format(platform["name"], v)) 146 | text.append("\n".join(platforms)) 147 | 148 | text.append(section_header("Dependencies", 2)) 149 | dependencies = [] 150 | if len(meta["dependencies"]) > 0: 151 | dependencies = [ 152 | "- {0} ansible role".format(dependency["role"]) 153 | for dependency in meta["dependencies"] 154 | ] 155 | dependencies.append("- Dokku (for library usage)") 156 | text.append("\n".join(dependencies)) 157 | 158 | return text 159 | 160 | 161 | def add_variables(text, role_path): 162 | defaults = {} 163 | defaults_file = os.path.join(role_path, "defaults", "main.yml.base") 164 | with open(defaults_file) as f: 165 | defaults = yaml.load(f.read(), Loader=yaml.SafeLoader) 166 | 167 | if len(defaults) > 0: 168 | text.append(section_header("Role Variables", 2)) 169 | variables = sorted(defaults.keys()) 170 | for variable in variables: 171 | config = defaults[variable] 172 | header_text = "{0}".format(variable) 173 | if config.get("deprecated", False): 174 | header_text += " (deprecated)" 175 | text.append(section_header(header_text, 3)) 176 | 177 | # Note: there doesn't seem to be a straighforward way of mapping a python value to its yaml representation 178 | # without getting a whole yaml document back. See https://stackoverflow.com/questions/28376530 179 | default_str = config["default"] 180 | if default_str == "": 181 | default_str = "''" 182 | elif default_str is None: 183 | default_str = "null" 184 | 185 | text.append( 186 | "- default: `{0}`\n- type: `{1}`\n- description: {2}".format( 187 | default_str, config["type"], config["description"].strip() 188 | ) 189 | ) 190 | 191 | return text 192 | 193 | 194 | def add_examples(text, role_path): 195 | examples = {} 196 | examples_file = os.path.join(role_path, "example.yml") 197 | with open(examples_file) as f: 198 | examples = yaml.load(f.read(), Loader=yaml.SafeLoader)["examples"] 199 | 200 | text.append(section_header("Example Playbooks", 2)) 201 | for e in examples: 202 | text.append(section_header(e["name"], 3)) 203 | text.append("```yaml\n{0}\n```".format(e["example"].strip())) 204 | 205 | return text 206 | 207 | 208 | def generate_readme(): 209 | role_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 210 | 211 | meta = {} 212 | meta_file = os.path.join(role_path, "meta", "main.yml") 213 | with open(meta_file) as f: 214 | meta = yaml.load(f.read(), Loader=yaml.SafeLoader) 215 | 216 | defaults = {} 217 | defaults_file = os.path.join(role_path, "defaults", "main.yml") 218 | with open(defaults_file) as f: 219 | defaults = yaml.load(f.read(), Loader=yaml.SafeLoader) 220 | 221 | text = [] 222 | text.append(section_header("Ansible Role: Dokku")) 223 | text.append( 224 | "[![Ansible Role](https://img.shields.io/ansible/role/d/dokku_bot/ansible_dokku)](https://galaxy.ansible.com/dokku_bot/ansible_dokku) " 225 | + "[![Release](https://img.shields.io/github/release/dokku/ansible-dokku.svg)](https://github.com/dokku/ansible-dokku/releases) " 226 | + "[![Build Status](https://github.com/dokku/ansible-dokku/workflows/CI/badge.svg)](https://github.com/dokku/ansible-dokku/actions)" 227 | ) 228 | 229 | text.append(meta["galaxy_info"]["description"]) 230 | 231 | text = add_table_of_contents(text) 232 | text = add_requirements(text, meta, defaults) 233 | text = add_variables(text, role_path) 234 | text = add_libraries(text, role_path) 235 | text = add_examples(text, role_path) 236 | 237 | text.append(section_header("Contributing", 2)) 238 | text.append("See [CONTRIBUTING.md](./CONTRIBUTING.md).") 239 | 240 | text.append(section_header("License", 2)) 241 | text.append(meta["galaxy_info"]["license"]) 242 | if os.path.isfile(os.path.join(role_path, "LICENSE.md")): 243 | text.append("See LICENSE.md for further details.") 244 | 245 | readme_file = os.path.join(role_path, "README.md") 246 | with open(readme_file, "w") as f: 247 | f.write("\n\n".join(text) + "\n") 248 | 249 | 250 | def generate_defaults(): 251 | role_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 252 | defaults_base_file = os.path.join(role_path, "defaults", "main.yml.base") 253 | 254 | defaults_base = {} 255 | with open(defaults_base_file) as f: 256 | defaults_base = yaml.load(f.read(), Loader=yaml.SafeLoader) 257 | 258 | defaults = {} 259 | if len(defaults_base) > 0: 260 | for variable, config in defaults_base.items(): 261 | if config["default"] is None and not config["templated"]: 262 | continue 263 | defaults[variable] = config["default"] 264 | 265 | defaults_file = os.path.join(role_path, "defaults", "main.yml") 266 | with open(defaults_file, "w") as f: 267 | f.write(yaml.dump(defaults, explicit_start=False)) 268 | 269 | 270 | def generate_requirements(): 271 | role_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 272 | 273 | meta = {} 274 | meta_file = os.path.join(role_path, "meta", "main.yml") 275 | with open(meta_file) as f: 276 | meta = yaml.load(f.read(), Loader=yaml.SafeLoader) 277 | 278 | with open(os.path.join(role_path, "ansible-role-requirements.yml"), "w") as f: 279 | yaml.dump(meta["dependencies"], f, explicit_start=True) 280 | 281 | 282 | def main(): 283 | generate_defaults() 284 | generate_readme() 285 | generate_requirements() 286 | 287 | 288 | if __name__ == "__main__": 289 | main() 290 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | dokku_daemon_install: true 2 | dokku_daemon_version: 0.0.2 3 | dokku_hostname: dokku.me 4 | dokku_key_file: /root/.ssh/id_rsa.pub 5 | dokku_manage_nginx: true 6 | dokku_packages_state: present 7 | dokku_plugins: {} 8 | dokku_skip_key_file: 'false' 9 | dokku_version: '' 10 | dokku_vhost_enable: 'true' 11 | dokku_web_config: 'false' 12 | herokuish_version: '' 13 | plugn_version: '' 14 | sshcommand_version: '' 15 | -------------------------------------------------------------------------------- /defaults/main.yml.base: -------------------------------------------------------------------------------- 1 | dokku_packages_state: 2 | default: present 3 | description: State of dokku packages. Accepts 'present' and 'latest' 4 | type: string 5 | 6 | dokku_version: 7 | default: "" 8 | description: | 9 | The version of dokku to install. 10 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 11 | deprecated: true 12 | templated: true 13 | type: string 14 | 15 | dokku_manage_nginx: 16 | default: true 17 | description: Whether we should manage the 00-default nginx site 18 | type: boolean 19 | 20 | dokku_daemon_install: 21 | default: true 22 | description: Whether to install the dokku-daemon 23 | type: boolean 24 | dokku_daemon_version: 25 | default: 0.0.2 26 | description: The version of dokku-daemon to install 27 | type: string 28 | 29 | herokuish_version: 30 | default: "" 31 | description: | 32 | The version of herokuish to install. 33 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 34 | deprecated: true 35 | templated: true 36 | type: string 37 | 38 | sshcommand_version: 39 | default: "" 40 | description: | 41 | The version of sshcommand to install. 42 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 43 | deprecated: true 44 | templated: true 45 | type: string 46 | 47 | plugn_version: 48 | default: "" 49 | description: | 50 | The version of plugn to install. 51 | Scheduled for deletion after 07/2021. Use `dokku_packages_state` instead. 52 | deprecated: true 53 | templated: true 54 | type: string 55 | 56 | dokku_users: 57 | default: null 58 | description: | 59 | A list of users who should have access to Dokku. This will _not_ grant them generic SSH access, but rather only access as the `dokku` user. Users should be specified in the following format: 60 | 61 | ```yaml 62 | - name: Jane Doe 63 | username: jane 64 | ssh_key: JANES_PUBLIC_SSH_KEY 65 | - name: Camilla 66 | username: camilla 67 | ssh_key: CAMILLAS_PUBLIC_SSH_KEY 68 | ``` 69 | templated: false 70 | type: list 71 | 72 | # debconf 73 | dokku_key_file: 74 | default: /root/.ssh/id_rsa.pub 75 | description: Path on disk to an SSH key to add to the Dokku user (Will be ignored on `dpkg-reconfigure`) 76 | type: string 77 | dokku_skip_key_file: 78 | default: 'false' 79 | description: Do not check for the existence of the dokku/key_file. Setting this to "true", will require you to manually add an SSH key later on. 80 | type: string 81 | dokku_hostname: 82 | default: dokku.me 83 | description: Hostname, used as vhost domain and for showing app URL after deploy 84 | type: string 85 | dokku_vhost_enable: 86 | default: 'true' 87 | description: Use vhost-based deployments (e.g., .dokku.me) 88 | type: string 89 | dokku_web_config: 90 | default: 'false' 91 | description: Use web-based config for hostname and keyfile 92 | type: string 93 | 94 | dokku_plugins: 95 | default: {} 96 | description: | 97 | A list of plugins to install. The host _must_ have network access to the install url, and git access if required. Plugins should be specified in the following format: 98 | 99 | ```yaml 100 | - name: postgres 101 | url: https://github.com/dokku/dokku-postgres.git 102 | 103 | - name: redis 104 | url: https://github.com/dokku/dokku-redis.git 105 | ``` 106 | type: list 107 | -------------------------------------------------------------------------------- /example.yml: -------------------------------------------------------------------------------- 1 | examples: 2 | - name: Installing Dokku 3 | example: | 4 | --- 5 | - hosts: all 6 | roles: 7 | - dokku_bot.ansible_dokku 8 | 9 | - name: Installing Plugins 10 | example: | 11 | --- 12 | - hosts: all 13 | roles: 14 | - dokku_bot.ansible_dokku 15 | vars: 16 | dokku_plugins: 17 | - name: clone 18 | url: https://github.com/crisward/dokku-clone.git 19 | - name: postgres 20 | url: https://github.com/dokku/dokku-postgres.git 21 | 22 | - name: Deploying a simple word inflector 23 | example: | 24 | --- 25 | - hosts: all 26 | roles: 27 | - dokku_bot.ansible_dokku 28 | tasks: 29 | - name: dokku apps:create inflector 30 | dokku_app: 31 | app: inflector 32 | 33 | - name: dokku clone inflector 34 | dokku_clone: 35 | app: inflector 36 | repository: https://github.com/cakephp/inflector.cakephp.org 37 | - name: Setting up a Small VPS with a Dokku App 38 | example: | 39 | --- 40 | - hosts: all 41 | roles: 42 | - dokku_bot.ansible_dokku 43 | - geerlingguy.swap 44 | vars: 45 | # If you are running dokku on a small VPS, you'll most likely 46 | # need some swap to ensure you don't run out of RAM during deploys 47 | swap_file_size_mb: '2048' 48 | dokku_version: 0.19.13 49 | dokku_users: 50 | - name: yourname 51 | username: yourname 52 | ssh_key: "{{lookup('file', '~/.ssh/id_rsa.pub')}}" 53 | dokku_plugins: 54 | - name: clone 55 | url: https://github.com/crisward/dokku-clone.git 56 | - name: letsencrypt 57 | url: https://github.com/dokku/dokku-letsencrypt.git 58 | tasks: 59 | - name: create app 60 | dokku_app: 61 | # change this name in your template! 62 | app: &appname appname 63 | - name: environment configuration 64 | dokku_config: 65 | app: *appname 66 | config: 67 | # specify port so `domains` can setup the port mapping properly 68 | PORT: "5000" 69 | - name: git clone 70 | # note you'll need to add a deployment key to the GH repo if it's private! 71 | dokku_clone: 72 | app: *appname 73 | repository: git@github.com:heroku/python-getting-started.git 74 | - name: add domain 75 | dokku_domains: 76 | app: *appname 77 | domains: 78 | - example.com 79 | - name: add letsencrypt 80 | dokku_letsencrypt: 81 | app: *appname 82 | -------------------------------------------------------------------------------- /files/nginx.default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | 5 | server_name _; 6 | return 444; 7 | log_not_found off; 8 | } 9 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: start dokku-daemon 2 | service: 3 | name: dokku-daemon 4 | state: started 5 | 6 | - name: start nginx 7 | service: 8 | name: nginx 9 | state: started 10 | 11 | - name: reload nginx 12 | service: 13 | name: nginx 14 | state: reloaded 15 | -------------------------------------------------------------------------------- /library/dokku_acl_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | 6 | DOCUMENTATION = """ 7 | --- 8 | module: dokku_acl_app 9 | short_description: Manage access control list for a given dokku application 10 | options: 11 | app: 12 | description: 13 | - The name of the app 14 | required: True 15 | default: null 16 | aliases: [] 17 | users: 18 | description: 19 | - The list of users who can manage the app 20 | required: True 21 | aliases: [] 22 | state: 23 | description: 24 | - Whether the ACLs should be present or absent 25 | required: False 26 | default: present 27 | choices: ["present", "absent" ] 28 | aliases: [] 29 | author: Leopold Talirz 30 | requirements: 31 | - the `dokku-acl` plugin 32 | """ 33 | 34 | EXAMPLES = """ 35 | - name: let leopold manage hello-world 36 | dokku_acl_app: 37 | app: hello-world 38 | users: 39 | - leopold 40 | - name: remove leopold from hello-world 41 | dokku_acl_app: 42 | app: hello-world 43 | users: 44 | - leopold 45 | state: absent 46 | """ 47 | 48 | 49 | def dokku_acl_app_set(data): 50 | is_error = True 51 | has_changed = False 52 | meta = {"present": False} 53 | 54 | has_changed = False 55 | 56 | # get users for app 57 | command = "dokku acl:list {0}".format(data["app"]) 58 | output, error = subprocess_check_output(command, redirect_stderr=True) 59 | 60 | if error is not None: 61 | meta["error"] = error 62 | return (is_error, has_changed, meta) 63 | 64 | users = set(output) 65 | 66 | if data["state"] == "absent": 67 | for user in data["users"]: 68 | if user not in users: 69 | continue 70 | 71 | command = "dokku --quiet acl:remove {0} {1}".format(data["app"], user) 72 | output, error = subprocess_check_output(command) 73 | has_changed = True 74 | if error is not None: 75 | meta["error"] = error 76 | return (is_error, has_changed, meta) 77 | else: 78 | for user in data["users"]: 79 | if user in users: 80 | continue 81 | 82 | command = "dokku --quiet acl:add {0} {1}".format(data["app"], user) 83 | output, error = subprocess_check_output(command) 84 | has_changed = True 85 | if error is not None: 86 | meta["error"] = error 87 | return (is_error, has_changed, meta) 88 | 89 | is_error = False 90 | return (is_error, has_changed, meta) 91 | 92 | 93 | def main(): 94 | fields = { 95 | "app": {"required": True, "type": "str"}, 96 | "users": {"required": True, "type": "list"}, 97 | "state": { 98 | "required": False, 99 | "default": "present", 100 | "choices": ["absent", "present"], 101 | "type": "str", 102 | }, 103 | } 104 | 105 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 106 | is_error, has_changed, result = dokku_acl_app_set(module.params) 107 | 108 | if is_error: 109 | module.fail_json(msg=result["error"], meta=result) 110 | module.exit_json(changed=has_changed, meta=result) 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /library/dokku_acl_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | 6 | DOCUMENTATION = """ 7 | --- 8 | module: dokku_acl_service 9 | short_description: Manage access control list for a given dokku service 10 | options: 11 | service: 12 | description: 13 | - The name of the service 14 | required: True 15 | default: null 16 | aliases: [] 17 | type: 18 | description: 19 | - The type of the service 20 | required: True 21 | default: null 22 | aliases: [] 23 | users: 24 | description: 25 | - The list of users who can manage the service 26 | required: True 27 | aliases: [] 28 | state: 29 | description: 30 | - Whether the ACLs should be present or absent 31 | required: False 32 | default: present 33 | choices: ["present", "absent" ] 34 | aliases: [] 35 | author: Leopold Talirz 36 | requirements: 37 | - the `dokku-acl` plugin 38 | """ 39 | 40 | EXAMPLES = """ 41 | - name: let leopold manage mypostgres postgres service 42 | dokku_acl_service: 43 | service: mypostgres 44 | type: postgres 45 | users: 46 | - leopold 47 | - name: remove leopold from mypostgres postgres service 48 | dokku_acl_service: 49 | service: hello-world 50 | type: postgres 51 | users: 52 | - leopold 53 | state: absent 54 | """ 55 | 56 | 57 | def dokku_acl_service_set(data): 58 | is_error = True 59 | has_changed = False 60 | meta = {"present": False} 61 | 62 | has_changed = False 63 | 64 | # get users for service 65 | command = "dokku --quiet acl:list-service {0} {1}".format( 66 | data["type"], data["service"] 67 | ) 68 | output, error = subprocess_check_output(command, redirect_stderr=True) 69 | 70 | if error is not None: 71 | meta["error"] = error 72 | return (is_error, has_changed, meta) 73 | 74 | users = set(output) 75 | 76 | if data["state"] == "absent": 77 | for user in data["users"]: 78 | if user not in users: 79 | continue 80 | 81 | command = "dokku --quiet acl:remove-service {0} {1} {2}".format( 82 | data["type"], data["service"], user 83 | ) 84 | output, error = subprocess_check_output(command) 85 | has_changed = True 86 | if error is not None: 87 | meta["error"] = error 88 | return (is_error, has_changed, meta) 89 | else: 90 | for user in data["users"]: 91 | if user in users: 92 | continue 93 | 94 | command = "dokku --quiet acl:add-service {0} {1} {2}".format( 95 | data["type"], data["service"], user 96 | ) 97 | output, error = subprocess_check_output(command, redirect_stderr=True) 98 | has_changed = True 99 | if error is not None: 100 | meta["error"] = error 101 | return (is_error, has_changed, meta) 102 | 103 | is_error = False 104 | return (is_error, has_changed, meta) 105 | 106 | 107 | def main(): 108 | fields = { 109 | "service": {"required": True, "type": "str"}, 110 | "type": {"required": True, "type": "str"}, 111 | "users": {"required": True, "type": "list"}, 112 | "state": { 113 | "required": False, 114 | "default": "present", 115 | "choices": ["absent", "present"], 116 | "type": "str", 117 | }, 118 | } 119 | 120 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 121 | is_error, has_changed, result = dokku_acl_service_set(module.params) 122 | 123 | if is_error: 124 | module.fail_json(msg=result["error"], meta=result) 125 | module.exit_json(changed=has_changed, meta=result) 126 | 127 | 128 | if __name__ == "__main__": 129 | main() 130 | -------------------------------------------------------------------------------- /library/dokku_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_app import ( 5 | dokku_app_ensure_present, 6 | dokku_app_ensure_absent, 7 | ) 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_app 12 | short_description: Create or destroy dokku apps 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | state: 21 | description: 22 | - The state of the app 23 | required: False 24 | default: present 25 | choices: [ "present", "absent" ] 26 | aliases: [] 27 | author: Jose Diaz-Gonzalez 28 | requirements: [ ] 29 | """ 30 | 31 | EXAMPLES = """ 32 | - name: Create a dokku app 33 | dokku_app: 34 | app: hello-world 35 | 36 | - name: Delete that repo 37 | dokku_app: 38 | app: hello-world 39 | state: absent 40 | """ 41 | 42 | 43 | def main(): 44 | fields = { 45 | "app": {"required": True, "type": "str"}, 46 | "state": { 47 | "required": False, 48 | "default": "present", 49 | "choices": ["present", "absent"], 50 | "type": "str", 51 | }, 52 | } 53 | choice_map = { 54 | "present": dokku_app_ensure_present, 55 | "absent": dokku_app_ensure_absent, 56 | } 57 | 58 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 59 | is_error, has_changed, result = choice_map.get(module.params["state"])( 60 | module.params 61 | ) 62 | 63 | if is_error: 64 | module.fail_json(msg=result["error"], meta=result) 65 | module.exit_json(changed=has_changed, meta=result) 66 | 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /library/dokku_builder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import subprocess 4 | 5 | from ansible.module_utils.basic import AnsibleModule 6 | 7 | DOCUMENTATION = """ 8 | --- 9 | module: dokku_builder 10 | short_description: Manage the builder configuration for a given dokku application 11 | options: 12 | app: 13 | description: 14 | - The name of the app. This is required only if global is set to False. 15 | required: True 16 | default: null 17 | aliases: [] 18 | property: 19 | description: 20 | - The property to be changed (e.g., `build-dir`, `selected`) 21 | required: True 22 | aliases: [] 23 | value: 24 | description: 25 | - The value of the builder property (leave empty to unset) 26 | required: False 27 | aliases: [] 28 | global: 29 | description: 30 | - If the property being set is global 31 | required: False 32 | default: False 33 | aliases: [] 34 | author: Simo Aleksandrov 35 | """ 36 | 37 | EXAMPLES = """ 38 | - name: Overriding the auto-selected builder 39 | dokku_builder: 40 | app: node-js-app 41 | property: selected 42 | value: dockerfile 43 | - name: Setting the builder to the default value 44 | dokku_builder: 45 | app: node-js-app 46 | property: selected 47 | - name: Changing the build build directory 48 | dokku_builder: 49 | app: monorepo 50 | property: build-dir 51 | value: backend 52 | - name: Overriding the auto-selected builder globally 53 | dokku_builder: 54 | global: true 55 | property: selected 56 | value: herokuish 57 | """ 58 | 59 | 60 | def dokku_builder(data): 61 | is_error = True 62 | has_changed = False 63 | meta = {"present": False} 64 | 65 | if data["global"] and data["app"]: 66 | is_error = True 67 | meta["error"] = 'When "global" is set to true, "app" must not be provided.' 68 | return (is_error, has_changed, meta) 69 | 70 | # Check if "value" is set and evaluates to a non-empty string, otherwise use an empty string 71 | value = data["value"] if "value" in data else None 72 | if not value: 73 | value = "" 74 | 75 | command = "dokku builder:set {0} {1} {2}".format( 76 | "--global" if data["global"] else data["app"], 77 | data["property"], 78 | value, 79 | ) 80 | 81 | try: 82 | subprocess.check_call(command, shell=True) 83 | is_error = False 84 | has_changed = True 85 | meta["present"] = True 86 | except subprocess.CalledProcessError as e: 87 | meta["error"] = str(e) 88 | 89 | return (is_error, has_changed, meta) 90 | 91 | 92 | def main(): 93 | fields = { 94 | "app": {"required": False, "type": "str"}, 95 | "property": {"required": True, "type": "str"}, 96 | "value": {"required": False, "type": "str", "no_log": True}, 97 | "global": {"required": False, "type": "bool"}, 98 | } 99 | 100 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 101 | is_error, has_changed, result = dokku_builder(module.params) 102 | 103 | if is_error: 104 | module.fail_json(msg=result["error"], meta=result) 105 | module.exit_json(changed=has_changed, meta=result) 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /library/dokku_certs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | import re 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_certs 11 | short_description: Manages ssl configuration for an app. 12 | description: 13 | - Manages ssl configuration for an app. 14 | - Will not update certificates 15 | - Only checks for presence of the crt file, not the key 16 | options: 17 | app: 18 | description: 19 | - The name of the app 20 | required: True 21 | default: null 22 | aliases: [] 23 | key: 24 | description: 25 | - Path to the ssl certificate key 26 | required: True 27 | default: null 28 | aliases: [] 29 | cert: 30 | description: 31 | - Path to the ssl certificate 32 | required: True 33 | default: null 34 | aliases: [] 35 | state: 36 | description: 37 | - The state of the ssl configuration 38 | required: False 39 | default: present 40 | choices: [ "present", "absent" ] 41 | aliases: [] 42 | author: Jose Diaz-Gonzalez 43 | requirements: [ ] 44 | """ 45 | 46 | EXAMPLES = """ 47 | - name: Adds an ssl certificate and key to an app 48 | dokku_certs: 49 | app: hello-world 50 | key: /etc/nginx/ssl/hello-world.key 51 | cert: /etc/nginx/ssl/hello-world.crt 52 | 53 | - name: Removes an ssl certificate and key from an app 54 | dokku_certs: 55 | app: hello-world 56 | state: absent 57 | """ 58 | 59 | 60 | def to_bool(v): 61 | return v.lower() == "true" 62 | 63 | 64 | def dokku_certs_report(data): 65 | command = "dokku --quiet certs:report {0}".format(data["app"]) 66 | output, error = subprocess_check_output(command) 67 | if error is not None: 68 | return output, error 69 | output = [re.sub(r"\s\s+", "", line) for line in output] 70 | report = {} 71 | 72 | allowed_keys = [ 73 | "dir", 74 | "enabled", 75 | "hostnames", 76 | "expires at", 77 | "issuer", 78 | "starts at", 79 | "subject", 80 | "verified", 81 | ] 82 | RE_PREFIX = re.compile("^ssl-") 83 | for line in output: 84 | if ":" not in line: 85 | continue 86 | key, value = line.split(":", 1) 87 | key = RE_PREFIX.sub(r"", key.replace(" ", "-").lower()) 88 | if key not in allowed_keys: 89 | continue 90 | 91 | if key == "enabled": 92 | value = to_bool(value) 93 | report[key] = value 94 | 95 | return report, error 96 | 97 | 98 | def dokku_certs_absent(data=None): 99 | has_changed = False 100 | is_error = True 101 | meta = {"present": True} 102 | 103 | report, error = dokku_certs_report(data) 104 | if error: 105 | meta["error"] = error 106 | return (is_error, has_changed, meta) 107 | 108 | if not report["enabled"]: 109 | is_error = False 110 | meta["present"] = False 111 | return (is_error, has_changed, meta) 112 | 113 | command = "dokku --quiet certs:remove {0}".format(data["app"]) 114 | try: 115 | subprocess.check_call(command, shell=True) 116 | is_error = False 117 | has_changed = True 118 | meta["present"] = False 119 | except subprocess.CalledProcessError as e: 120 | meta["error"] = str(e) 121 | 122 | return (is_error, has_changed, meta) 123 | 124 | 125 | def dokku_certs_present(data): 126 | is_error = True 127 | has_changed = False 128 | meta = {"present": False} 129 | 130 | report, error = dokku_certs_report(data) 131 | if error: 132 | meta["error"] = error 133 | return (is_error, has_changed, meta) 134 | 135 | if report["enabled"]: 136 | is_error = False 137 | meta["present"] = False 138 | return (is_error, has_changed, meta) 139 | 140 | command = "dokku certs:add {0} {1} {2}".format( 141 | data["app"], data["cert"], data["key"] 142 | ) 143 | try: 144 | subprocess.check_call(command, shell=True) 145 | is_error = False 146 | has_changed = True 147 | meta["present"] = True 148 | except subprocess.CalledProcessError as e: 149 | meta["error"] = str(e) 150 | 151 | return (is_error, has_changed, meta) 152 | 153 | 154 | def main(): 155 | fields = { 156 | "app": {"required": True, "type": "str"}, 157 | "key": {"required": False, "type": "str"}, 158 | "cert": {"required": False, "type": "str"}, 159 | "state": { 160 | "required": False, 161 | "default": "present", 162 | "choices": ["present", "absent"], 163 | "type": "str", 164 | }, 165 | } 166 | choice_map = { 167 | "present": dokku_certs_present, 168 | "absent": dokku_certs_absent, 169 | } 170 | 171 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 172 | is_error, has_changed, result = choice_map.get(module.params["state"])( 173 | module.params 174 | ) 175 | 176 | if is_error: 177 | module.fail_json(msg=result["error"], meta=result) 178 | module.exit_json(changed=has_changed, meta=result) 179 | 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /library/dokku_checks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import subprocess 4 | 5 | from ansible.module_utils.basic import AnsibleModule 6 | from ansible.module_utils.dokku_utils import subprocess_check_output 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_checks 11 | short_description: Manage the Zero Downtime checks for a dokku app 12 | options: 13 | app: 14 | description: 15 | - The name of the app 16 | required: True 17 | default: null 18 | aliases: [] 19 | state: 20 | description: 21 | - The state of the checks functionality 22 | required: False 23 | default: present 24 | choices: [ "present", "absent" ] 25 | aliases: [] 26 | author: Simo Aleksandrov 27 | """ 28 | 29 | EXAMPLES = """ 30 | - name: Disable the zero downtime deployment 31 | dokku_checks: 32 | app: hello-world 33 | state: absent 34 | 35 | - name: Re-enable the zero downtime deployment (enabled by default) 36 | dokku_checks: 37 | app: hello-world 38 | state: present 39 | """ 40 | 41 | 42 | def dokku_checks_enabled(data): 43 | command = "dokku --quiet checks:report {0}" 44 | response, error = subprocess_check_output(command.format(data["app"])) 45 | 46 | if error: 47 | return None, error 48 | 49 | report = response[0].split(":")[1] 50 | return report.strip() != "_all_", error 51 | 52 | 53 | def dokku_checks_present(data): 54 | is_error = True 55 | has_changed = False 56 | meta = {"present": False} 57 | 58 | enabled, error = dokku_checks_enabled(data) 59 | if error: 60 | meta["error"] = error 61 | return (is_error, has_changed, meta) 62 | 63 | if enabled: 64 | is_error = False 65 | meta["present"] = True 66 | return (is_error, has_changed, meta) 67 | 68 | command = "dokku --quiet checks:enable {0}".format(data["app"]) 69 | try: 70 | subprocess.check_call(command, shell=True) 71 | is_error = False 72 | has_changed = True 73 | meta["present"] = True 74 | except subprocess.CalledProcessError as e: 75 | meta["error"] = str(e) 76 | 77 | return (is_error, has_changed, meta) 78 | 79 | 80 | def dokku_checks_absent(data=None): 81 | is_error = True 82 | has_changed = False 83 | meta = {"present": True} 84 | 85 | enabled, error = dokku_checks_enabled(data) 86 | if error: 87 | meta["error"] = error 88 | return (is_error, has_changed, meta) 89 | 90 | if enabled is False: 91 | is_error = False 92 | meta["present"] = False 93 | return (is_error, has_changed, meta) 94 | 95 | command = "dokku --quiet checks:disable {0}".format(data["app"]) 96 | try: 97 | subprocess.check_call(command, shell=True) 98 | is_error = False 99 | has_changed = True 100 | meta["present"] = False 101 | except subprocess.CalledProcessError as e: 102 | meta["error"] = str(e) 103 | 104 | return (is_error, has_changed, meta) 105 | 106 | 107 | def main(): 108 | fields = { 109 | "app": {"required": True, "type": "str"}, 110 | "state": { 111 | "required": False, 112 | "default": "present", 113 | "choices": ["present", "absent"], 114 | "type": "str", 115 | }, 116 | } 117 | choice_map = { 118 | "present": dokku_checks_present, 119 | "absent": dokku_checks_absent, 120 | } 121 | 122 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 123 | is_error, has_changed, result = choice_map.get(module.params["state"])( 124 | module.params 125 | ) 126 | 127 | if is_error: 128 | module.fail_json(msg=result["error"], meta=result) 129 | module.exit_json(changed=has_changed, meta=result) 130 | 131 | 132 | if __name__ == "__main__": 133 | main() 134 | -------------------------------------------------------------------------------- /library/dokku_clone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import subprocess 4 | 5 | from ansible.module_utils.basic import AnsibleModule 6 | from ansible.module_utils.dokku_app import dokku_app_ensure_present 7 | from ansible.module_utils.dokku_git import dokku_git_sha 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_clone 12 | short_description: Clone a git repository and deploy app. 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | repository: 21 | description: 22 | - Git repository url 23 | required: True 24 | default: null 25 | aliases: [] 26 | version: 27 | description: 28 | - Git tree (tag or branch name). If not provided, default branch is used. 29 | required: False 30 | default: null 31 | aliases: [] 32 | build: 33 | description: 34 | - Whether to build the app after cloning. 35 | required: False 36 | default: true 37 | aliases: [] 38 | author: Jose Diaz-Gonzalez 39 | """ 40 | 41 | EXAMPLES = """ 42 | 43 | - name: clone a git repository and build app 44 | dokku_clone: 45 | app: example-app 46 | repository: https://github.com/heroku/node-js-getting-started 47 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 48 | - name: clone specific tag from git repository and build app 49 | dokku_clone: 50 | app: example-app 51 | repository: https://github.com/heroku/node-js-getting-started 52 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 53 | - name: sync git repository without building app 54 | dokku_clone: 55 | app: example-app 56 | repository: https://github.com/heroku/node-js-getting-started 57 | build: false 58 | """ 59 | 60 | 61 | def dokku_clone(data): 62 | 63 | # create app (if not exists) 64 | is_error, has_changed, meta = dokku_app_ensure_present(data) 65 | meta["present"] = False # meaning: requested *version* of app is present 66 | if is_error: 67 | return (is_error, has_changed, meta) 68 | 69 | sha_old = dokku_git_sha(data["app"]) 70 | 71 | # sync with remote repository 72 | command_git_sync = "dokku git:sync {app} {repository}".format( 73 | app=data["app"], repository=data["repository"] 74 | ) 75 | if data["version"]: 76 | command_git_sync += " {version}".format(version=data["version"]) 77 | if data["build"]: 78 | command_git_sync += " --build" 79 | try: 80 | subprocess.check_output(command_git_sync, stderr=subprocess.STDOUT, shell=True) 81 | except subprocess.CalledProcessError as e: 82 | is_error = True 83 | if "is not a dokku command" in str(e.output): 84 | meta["error"] = ( 85 | "Please upgrade to dokku>=0.23.0 in order to use the 'git:sync' command." 86 | ) 87 | else: 88 | meta["error"] = str(e.output) 89 | return (is_error, has_changed, meta) 90 | finally: 91 | meta["present"] = True # meaning: requested *version* of app is present 92 | 93 | if data["build"] or dokku_git_sha(data["app"]) != sha_old: 94 | has_changed = True 95 | 96 | return (is_error, has_changed, meta) 97 | 98 | 99 | def main(): 100 | fields = { 101 | "app": {"required": True, "type": "str"}, 102 | "repository": {"required": True, "type": "str"}, 103 | "version": {"required": False, "type": "str"}, 104 | "build": {"default": True, "required": False, "type": "bool"}, 105 | } 106 | 107 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 108 | is_error, has_changed, result = dokku_clone(module.params) 109 | 110 | if is_error: 111 | module.fail_json(msg=result["error"], meta=result) 112 | module.exit_json(changed=has_changed, meta=result) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /library/dokku_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import json 6 | import pipes 7 | import subprocess 8 | 9 | try: 10 | basestring 11 | except NameError: 12 | basestring = str 13 | 14 | 15 | DOCUMENTATION = """ 16 | --- 17 | module: dokku_config 18 | short_description: Manage environment variables for a given dokku application 19 | options: 20 | app: 21 | description: 22 | - The name of the app 23 | required: True 24 | default: null 25 | aliases: [] 26 | config: 27 | description: 28 | - A map of environment variables where key => value 29 | required: True 30 | default: {} 31 | aliases: [] 32 | restart: 33 | description: 34 | - Whether to restart the application or not. If the task is idempotent 35 | then setting restart to true will not perform a restart. 36 | required: false 37 | default: true 38 | author: Jose Diaz-Gonzalez 39 | requirements: [ ] 40 | """ 41 | 42 | EXAMPLES = """ 43 | - name: set KEY=VALUE 44 | dokku_config: 45 | app: hello-world 46 | config: 47 | KEY: VALUE_1 48 | KEY_2: VALUE_2 49 | 50 | - name: set KEY=VALUE without restart 51 | dokku_config: 52 | app: hello-world 53 | restart: false 54 | config: 55 | KEY: VALUE_1 56 | KEY_2: VALUE_2 57 | """ 58 | 59 | 60 | def dokku_config(app): 61 | command = "dokku config:export --format json {0}".format(app) 62 | output, error = subprocess_check_output(command, split=None) 63 | 64 | if error is None: 65 | try: 66 | output = json.loads(output) 67 | except ValueError as e: 68 | error = str(e) 69 | 70 | return output, error 71 | 72 | 73 | def dokku_config_set(data): 74 | is_error = True 75 | has_changed = False 76 | meta = {"present": False} 77 | 78 | values = [] 79 | invalid_values = [] 80 | existing, error = dokku_config(data["app"]) 81 | for key, value in data["config"].items(): 82 | if not isinstance(value, basestring): 83 | invalid_values.append(key) 84 | continue 85 | 86 | if value == existing.get(key, None): 87 | continue 88 | values.append("{0}={1}".format(key, pipes.quote(value))) 89 | 90 | if invalid_values: 91 | template = "All values must be strings, found invalid types for {0}" 92 | meta["error"] = template.format(", ".join(invalid_values)) 93 | return (is_error, has_changed, meta) 94 | 95 | if len(values) == 0: 96 | is_error = False 97 | has_changed = False 98 | return (is_error, has_changed, meta) 99 | 100 | command = "dokku config:set {0}{1} {2}".format( 101 | "--no-restart " if data["restart"] is False else "", 102 | data["app"], 103 | " ".join(values), 104 | ) 105 | 106 | try: 107 | subprocess.check_call(command, shell=True) 108 | is_error = False 109 | has_changed = True 110 | except subprocess.CalledProcessError as e: 111 | meta["error"] = str(e) 112 | 113 | return (is_error, has_changed, meta) 114 | 115 | 116 | def main(): 117 | fields = { 118 | "app": {"required": True, "type": "str"}, 119 | "config": {"required": True, "type": "dict", "no_log": True}, 120 | "restart": {"required": False, "type": "bool"}, 121 | } 122 | 123 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 124 | is_error, has_changed, result = dokku_config_set(module.params) 125 | 126 | if is_error: 127 | module.fail_json(msg=result["error"], meta=result) 128 | module.exit_json(changed=has_changed, meta=result) 129 | 130 | 131 | if __name__ == "__main__": 132 | main() 133 | -------------------------------------------------------------------------------- /library/dokku_docker_options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import pipes 6 | import subprocess 7 | import re 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_docker_options 12 | short_description: Manage docker-options for a given dokku application 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | option: 21 | description: 22 | - A single docker option 23 | required: True 24 | default: null 25 | aliases: [] 26 | phase: 27 | description: 28 | - The phase in which to set the options 29 | required: False 30 | default: null 31 | choices: [ "build", "deploy", "run" ] 32 | aliases: [] 33 | state: 34 | description: 35 | - The state of the docker options 36 | required: False 37 | default: present 38 | choices: [ "present", "absent" ] 39 | aliases: [] 40 | author: Jose Diaz-Gonzalez 41 | requirements: [ ] 42 | """ 43 | 44 | EXAMPLES = """ 45 | - name: docker-options:add hello-world deploy 46 | dokku_docker_options: 47 | app: hello-world 48 | phase: deploy 49 | option: "-v /var/run/docker.sock:/var/run/docker.sock" 50 | 51 | - name: docker-options:remove hello-world deploy 52 | dokku_docker_options: 53 | app: hello-world 54 | phase: deploy 55 | option: "-v /var/run/docker.sock:/var/run/docker.sock" 56 | state: absent 57 | """ 58 | 59 | 60 | def dokku_docker_options(data): 61 | options = {"build": "", "deploy": "", "run": ""} 62 | command = "dokku --quiet docker-options:report {0}".format(data["app"]) 63 | output, error = subprocess_check_output(command) 64 | if error is None: 65 | for line in output: 66 | match = re.match( 67 | "Docker options (?Pbuild|deploy|run):(?P.+)", 68 | line.strip(), 69 | ) 70 | if match: 71 | options[match.group("type")] = match.group("options").strip() 72 | return options, error 73 | 74 | 75 | def dokku_docker_options_absent(data): 76 | is_error = True 77 | has_changed = False 78 | meta = {"present": True} 79 | 80 | existing, error = dokku_docker_options(data) 81 | if error: 82 | meta["error"] = error 83 | return (is_error, has_changed, meta) 84 | 85 | if data["option"] not in existing[data["phase"]]: 86 | is_error = False 87 | meta["present"] = False 88 | return (is_error, has_changed, meta) 89 | 90 | command = "dokku --quiet docker-options:remove {0} {1} {2}".format( 91 | data["app"], data["phase"], pipes.quote(data["option"]) 92 | ) 93 | try: 94 | subprocess.check_call(command, shell=True) 95 | is_error = False 96 | has_changed = True 97 | meta["present"] = False 98 | except subprocess.CalledProcessError as e: 99 | meta["error"] = str(e) 100 | 101 | return (is_error, has_changed, meta) 102 | 103 | 104 | def dokku_docker_options_present(data): 105 | is_error = True 106 | has_changed = False 107 | meta = {"present": False} 108 | 109 | existing, error = dokku_docker_options(data) 110 | if error: 111 | meta["error"] = error 112 | return (is_error, has_changed, meta) 113 | 114 | if data["option"] in existing[data["phase"]]: 115 | is_error = False 116 | meta["present"] = True 117 | return (is_error, has_changed, meta) 118 | 119 | command = "dokku --quiet docker-options:add {0} {1} {2}".format( 120 | data["app"], data["phase"], pipes.quote(data["option"]) 121 | ) 122 | try: 123 | subprocess.check_call(command, shell=True) 124 | is_error = False 125 | has_changed = True 126 | meta["present"] = True 127 | except subprocess.CalledProcessError as e: 128 | meta["error"] = str(e) 129 | 130 | return (is_error, has_changed, meta) 131 | 132 | 133 | def main(): 134 | fields = { 135 | "app": {"required": True, "type": "str"}, 136 | "state": { 137 | "required": False, 138 | "default": "present", 139 | "choices": ["present", "absent"], 140 | "type": "str", 141 | }, 142 | "phase": { 143 | "required": True, 144 | "choices": ["build", "deploy", "run"], 145 | "type": "str", 146 | }, 147 | "option": {"required": True, "type": "str"}, 148 | } 149 | choice_map = { 150 | "present": dokku_docker_options_present, 151 | "absent": dokku_docker_options_absent, 152 | } 153 | 154 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 155 | is_error, has_changed, result = choice_map.get(module.params["state"])( 156 | module.params 157 | ) 158 | 159 | if is_error: 160 | module.fail_json(msg=result["error"], meta=result) 161 | module.exit_json(changed=has_changed, meta=result) 162 | 163 | 164 | if __name__ == "__main__": 165 | main() 166 | -------------------------------------------------------------------------------- /library/dokku_domains.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import pipes 6 | import subprocess 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_domains 11 | short_description: Manages domains for a given application 12 | options: 13 | global: 14 | description: 15 | - Whether to change the global domains or app-specific domains. 16 | default: False 17 | aliases: [] 18 | app: 19 | description: 20 | - The name of the app. This is required only if global is set to False. 21 | required: True 22 | default: null 23 | aliases: [] 24 | domains: 25 | description: 26 | - A list of domains 27 | required: True 28 | default: null 29 | aliases: [] 30 | state: 31 | description: 32 | - The state of the application's domains 33 | required: False 34 | default: present 35 | choices: [ "enable", "disable", "clear", "present", "absent", "set" ] 36 | aliases: [] 37 | author: Jose Diaz-Gonzalez 38 | requirements: [ ] 39 | """ 40 | 41 | EXAMPLES = """ 42 | # Adds domain, inclusive 43 | - name: domains:add hello-world dokku.me 44 | dokku_domains: 45 | app: hello-world 46 | domains: 47 | - dokku.me 48 | 49 | # Removes listed domains, but leaves others unchanged 50 | - name: domains:remove hello-world dokku.me 51 | dokku_domains: 52 | app: hello-world 53 | domains: 54 | - dokku.me 55 | state: absent 56 | 57 | # Clears all domains 58 | - name: domains:clear hello-world 59 | dokku_domains: 60 | app: hello-world 61 | state: clear 62 | 63 | # Enables the VHOST domain 64 | - name: domains:enable hello-world 65 | dokku_domains: 66 | app: hello-world 67 | state: enable 68 | 69 | # Disables the VHOST domain 70 | - name: domains:disable hello-world 71 | dokku_domains: 72 | app: hello-world 73 | state: disable 74 | 75 | # Sets the domain for the app, clearing all others 76 | - name: domains:set hello-world dokku.me 77 | dokku_domains: 78 | app: hello-world 79 | domains: 80 | - dokku.me 81 | state: set 82 | """ 83 | 84 | 85 | def dokku_global_domains(): 86 | command = "dokku --quiet domains --global --domains-global-vhosts" 87 | return subprocess_check_output(command) 88 | 89 | 90 | def dokku_domains(data): 91 | if data["global"]: 92 | command = "dokku --quiet domains:report --global --domains-global-vhosts" 93 | else: 94 | command = "dokku --quiet domains:report {0} --domains-app-vhosts".format( 95 | data["app"] 96 | ) 97 | return subprocess_check_output(command, split=" ") 98 | 99 | 100 | def dokku_domains_disable(data): 101 | is_error = True 102 | has_changed = False 103 | meta = {"present": True} 104 | 105 | if data["global"]: 106 | is_error = True 107 | meta["error"] = '"disable" state cannot be used with global domains.' 108 | return (is_error, has_changed, meta) 109 | 110 | domains = dokku_domains(data) 111 | if "No domain names set for plugins" in domains: 112 | is_error = False 113 | meta["present"] = False 114 | return (is_error, has_changed, meta) 115 | 116 | command = "dokku --quiet domains:disable {0}".format(data["app"]) 117 | try: 118 | subprocess.check_call(command, shell=True) 119 | is_error = False 120 | has_changed = True 121 | meta["present"] = False 122 | except subprocess.CalledProcessError as e: 123 | meta["error"] = str(e) 124 | 125 | return (is_error, has_changed, meta) 126 | 127 | 128 | def dokku_domains_enable(data): 129 | is_error = True 130 | has_changed = False 131 | meta = {"present": False} 132 | 133 | if data["global"]: 134 | is_error = True 135 | meta["error"] = '"enable" state cannot be used with global domains.' 136 | return (is_error, has_changed, meta) 137 | 138 | domains = dokku_domains(data) 139 | if "No domain names set for plugins" not in domains: 140 | is_error = False 141 | meta["present"] = True 142 | return (is_error, has_changed, meta) 143 | 144 | command = "dokku --quiet domains:enable {0}".format(data["app"]) 145 | try: 146 | subprocess.check_call(command, shell=True) 147 | is_error = False 148 | has_changed = True 149 | meta["present"] = True 150 | except subprocess.CalledProcessError as e: 151 | meta["error"] = str(e) 152 | 153 | return (is_error, has_changed, meta) 154 | 155 | 156 | def dokku_domains_absent(data): 157 | is_error = True 158 | has_changed = False 159 | meta = {"present": True} 160 | 161 | existing, error = dokku_domains(data) 162 | if error: 163 | meta["error"] = error 164 | return (is_error, has_changed, meta) 165 | 166 | to_remove = [d for d in data["domains"] if d in existing] 167 | to_remove = [pipes.quote(d) for d in to_remove] 168 | 169 | if len(to_remove) == 0: 170 | is_error = False 171 | meta["present"] = False 172 | return (is_error, has_changed, meta) 173 | 174 | if data["global"]: 175 | command = "dokku --quiet domains:remove-global {0}".format(" ".join(to_remove)) 176 | else: 177 | command = "dokku --quiet domains:remove {0} {1}".format( 178 | data["app"], " ".join(to_remove) 179 | ) 180 | try: 181 | subprocess.check_call(command, shell=True) 182 | is_error = False 183 | has_changed = True 184 | meta["present"] = False 185 | except subprocess.CalledProcessError as e: 186 | meta["error"] = str(e) 187 | 188 | return (is_error, has_changed, meta) 189 | 190 | 191 | def dokku_domains_present(data): 192 | is_error = True 193 | has_changed = False 194 | meta = {"present": False} 195 | 196 | existing, error = dokku_domains(data) 197 | if error: 198 | meta["error"] = error 199 | return (is_error, has_changed, meta) 200 | 201 | to_add = [d for d in data["domains"] if d not in existing] 202 | to_add = [pipes.quote(d) for d in to_add] 203 | 204 | if len(to_add) == 0: 205 | is_error = False 206 | meta["present"] = True 207 | return (is_error, has_changed, meta) 208 | 209 | if data["global"]: 210 | command = "dokku --quiet domains:add-global {0}".format(" ".join(to_add)) 211 | else: 212 | command = "dokku --quiet domains:add {0} {1}".format( 213 | data["app"], " ".join(to_add) 214 | ) 215 | try: 216 | subprocess.check_call(command, shell=True) 217 | is_error = False 218 | has_changed = True 219 | meta["present"] = True 220 | except subprocess.CalledProcessError as e: 221 | meta["error"] = str(e) 222 | 223 | return (is_error, has_changed, meta) 224 | 225 | 226 | def dokku_domains_clear(data): 227 | is_error = True 228 | has_changed = False 229 | meta = {"present": False} 230 | 231 | if data["global"]: 232 | command = "dokku --quiet domains:clear-global" 233 | else: 234 | command = "dokku --quiet domains:clear {0}".format(data["app"]) 235 | try: 236 | subprocess.check_call(command, shell=True) 237 | is_error = False 238 | has_changed = True 239 | meta["present"] = True 240 | except subprocess.CalledProcessError as e: 241 | meta["error"] = str(e) 242 | 243 | return (is_error, has_changed, meta) 244 | 245 | 246 | def dokku_domains_set(data): 247 | is_error = True 248 | has_changed = False 249 | meta = {"present": False} 250 | 251 | existing, error = dokku_domains(data) 252 | if error: 253 | meta["error"] = error 254 | return (is_error, has_changed, meta) 255 | 256 | to_set = [pipes.quote(d) for d in data["domains"]] 257 | 258 | if data["global"]: 259 | command = "dokku --quiet domains:set-global {0}".format(" ".join(to_set)) 260 | else: 261 | command = "dokku --quiet domains:set {0} {1}".format( 262 | data["app"], " ".join(to_set) 263 | ) 264 | try: 265 | subprocess.check_call(command, shell=True) 266 | is_error = False 267 | has_changed = True 268 | meta["present"] = True 269 | except subprocess.CalledProcessError as e: 270 | meta["error"] = str(e) 271 | 272 | return (is_error, has_changed, meta) 273 | 274 | 275 | def main(): 276 | fields = { 277 | "global": {"required": False, "default": False, "type": "bool"}, 278 | "app": {"required": False, "type": "str"}, 279 | "domains": {"required": True, "type": "list"}, 280 | "state": { 281 | "required": False, 282 | "default": "present", 283 | "choices": ["absent", "clear", "enable", "disable", "present", "set"], 284 | "type": "str", 285 | }, 286 | } 287 | choice_map = { 288 | "absent": dokku_domains_absent, 289 | "clear": dokku_domains_clear, 290 | "disable": dokku_domains_disable, 291 | "enable": dokku_domains_enable, 292 | "present": dokku_domains_present, 293 | "set": dokku_domains_set, 294 | } 295 | 296 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 297 | is_error, has_changed, result = choice_map.get(module.params["state"])( 298 | module.params 299 | ) 300 | 301 | if is_error: 302 | module.fail_json(msg=result["error"], meta=result) 303 | module.exit_json(changed=has_changed, meta=result) 304 | 305 | 306 | if __name__ == "__main__": 307 | main() 308 | -------------------------------------------------------------------------------- /library/dokku_git_sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import pipes 6 | import re 7 | import subprocess 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_git_sync 12 | short_description: Manages syncing git code from a remote repository for an app 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | remote: 21 | description: 22 | - The git remote url to use 23 | required: False 24 | default: null 25 | aliases: [] 26 | state: 27 | description: 28 | - The state of the git-sync integration 29 | required: False 30 | default: present 31 | choices: [ "present", "absent" ] 32 | aliases: [] 33 | author: Jose Diaz-Gonzalez 34 | requirements: 35 | - the `dokku-git-sync` plugin (_commercial_) 36 | """ 37 | 38 | EXAMPLES = """ 39 | - name: git-sync:enable hello-world 40 | dokku_git_sync: 41 | app: hello-world 42 | remote: git@github.com:hello-world/hello-world.git 43 | 44 | - name: git-sync:disable hello-world 45 | dokku_git_sync: 46 | app: hello-world 47 | state: absent 48 | """ 49 | 50 | 51 | def to_bool(v): 52 | return v.lower() == "true" 53 | 54 | 55 | def to_str(v): 56 | return "true" if v else "false" 57 | 58 | 59 | def dokku_module_set(command_prefix, data, key, value=None): 60 | has_changed = False 61 | error = None 62 | 63 | if value: 64 | command = "dokku --quiet {0}:set {1} {2} {3}".format( 65 | command_prefix, data["app"], key, pipes.quote(value) 66 | ) 67 | else: 68 | command = "dokku --quiet {0}:set {1} {2}".format( 69 | command_prefix, data["app"], key 70 | ) 71 | 72 | try: 73 | subprocess.check_call(command, shell=True) 74 | has_changed = True 75 | except subprocess.CalledProcessError as e: 76 | error = str(e) 77 | 78 | return (has_changed, error) 79 | 80 | 81 | def dokku_module_set_blank(command_prefix, data, setable_fields): 82 | error = None 83 | errors = [] 84 | changed_keys = [] 85 | has_changed = False 86 | 87 | for key in setable_fields: 88 | changed, error = dokku_module_set(command_prefix, data, key) 89 | if changed: 90 | has_changed = True 91 | changed_keys.append(key) 92 | if error: 93 | errors.append(error) 94 | 95 | if len(errors) > 0: 96 | error = ",".join(errors) 97 | 98 | return (has_changed, changed_keys, error) 99 | 100 | 101 | def dokku_module_set_values(command_prefix, data, report, setable_fields): 102 | error = None 103 | errors = [] 104 | changed_keys = [] 105 | has_changed = False 106 | 107 | if "enabled" in data: 108 | data["enabled"] = to_str(data["enabled"]) 109 | if "enabled" in report: 110 | report["enabled"] = to_str(report["enabled"]) 111 | 112 | for key, value in report.items(): 113 | if key not in setable_fields: 114 | continue 115 | if data.get(key, None) is None: 116 | continue 117 | if data[key] == value: 118 | continue 119 | 120 | changed, error = dokku_module_set(command_prefix, data, key, data[key]) 121 | if error: 122 | errors.append(error) 123 | if changed: 124 | has_changed = True 125 | changed_keys.append(key) 126 | 127 | if len(errors) > 0: 128 | error = ",".join(errors) 129 | 130 | return (has_changed, changed_keys, error) 131 | 132 | 133 | def dokku_module_require_fields(data, required_fields): 134 | error = None 135 | missing_keys = [] 136 | 137 | if len(required_fields) > 0: 138 | for key in required_fields: 139 | if data.get(key, None) is None: 140 | missing_keys.append(key) 141 | 142 | if len(missing_keys) > 0: 143 | error = "missing required arguments: {0}".format(", ".join(missing_keys)) 144 | 145 | return error 146 | 147 | 148 | def dokku_module_report(command_prefix, data, re_compiled, allowed_report_keys): 149 | command = "dokku --quiet {0}:report {1}".format(command_prefix, data["app"]) 150 | output, error = subprocess_check_output(command) 151 | if error is not None: 152 | return output, error 153 | 154 | output = [re.sub(r"\s\s+", "", line) for line in output] 155 | report = {} 156 | 157 | for line in output: 158 | if ":" not in line: 159 | continue 160 | key, value = line.split(":", 1) 161 | key = re_compiled.sub(r"", key.replace(" ", "-").lower()) 162 | if key not in allowed_report_keys: 163 | continue 164 | 165 | value = value.strip() 166 | if key == "enabled": 167 | value = to_bool(value) 168 | report[key] = value 169 | 170 | return report, error 171 | 172 | 173 | def dokku_module_absent( 174 | command_prefix, 175 | data, 176 | re_compiled, 177 | allowed_report_keys, 178 | required_present_fields, 179 | setable_fields, 180 | ): 181 | has_changed = False 182 | is_error = True 183 | meta = {"present": True, "changed": []} 184 | 185 | report, error = dokku_module_report( 186 | command_prefix, data, re_compiled, allowed_report_keys 187 | ) 188 | if error: 189 | meta["error"] = error 190 | return (is_error, has_changed, meta) 191 | 192 | if not report["enabled"]: 193 | is_error = False 194 | meta["present"] = False 195 | return (is_error, has_changed, meta) 196 | 197 | data["enabled"] = "false" 198 | has_changed, changed_keys, error = dokku_module_set_blank( 199 | command_prefix, data, setable_fields 200 | ) 201 | if error: 202 | meta["error"] = error 203 | else: 204 | is_error = False 205 | meta["present"] = False 206 | 207 | if len(changed_keys) > 0: 208 | meta["changed"] = changed_keys 209 | 210 | return (is_error, has_changed, meta) 211 | 212 | 213 | def dokku_module_present( 214 | command_prefix, 215 | data, 216 | re_compiled, 217 | allowed_report_keys, 218 | required_present_fields, 219 | setable_fields, 220 | ): 221 | is_error = True 222 | has_changed = False 223 | meta = {"present": False, "changed": []} 224 | 225 | data["enabled"] = "true" 226 | error = dokku_module_require_fields(data, required_present_fields) 227 | if error: 228 | meta["error"] = error 229 | return (is_error, has_changed, meta) 230 | 231 | report, error = dokku_module_report( 232 | command_prefix, data, re_compiled, allowed_report_keys 233 | ) 234 | if error: 235 | meta["error"] = error 236 | return (is_error, has_changed, meta) 237 | 238 | has_changed, changed_keys, error = dokku_module_set_values( 239 | command_prefix, data, report, setable_fields 240 | ) 241 | if error: 242 | meta["error"] = error 243 | else: 244 | is_error = False 245 | meta["present"] = True 246 | 247 | if len(changed_keys) > 0: 248 | meta["changed"] = changed_keys 249 | 250 | return (is_error, has_changed, meta) 251 | 252 | 253 | def main(): 254 | fields = { 255 | "app": {"required": True, "type": "str"}, 256 | "remote": {"required": False, "type": "str", "default": None}, 257 | "state": { 258 | "required": False, 259 | "default": "present", 260 | "choices": ["absent", "present"], 261 | "type": "str", 262 | }, 263 | } 264 | choice_map = { 265 | "absent": dokku_module_absent, 266 | "present": dokku_module_present, 267 | } 268 | 269 | allowed_report_keys = ["remote"] 270 | command_prefix = "git-sync" 271 | required_present_fields = ["remote"] 272 | setable_fields = ["remote"] 273 | RE_PREFIX = re.compile("^git-sync-") 274 | 275 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 276 | is_error, has_changed, result = choice_map.get(module.params["state"])( 277 | command_prefix=command_prefix, 278 | data=module.params, 279 | re_compiled=RE_PREFIX, 280 | allowed_report_keys=allowed_report_keys, 281 | required_present_fields=required_present_fields, 282 | setable_fields=setable_fields, 283 | ) 284 | 285 | if is_error: 286 | module.fail_json(msg=result["error"], meta=result) 287 | module.exit_json(changed=has_changed, meta=result) 288 | 289 | 290 | if __name__ == "__main__": 291 | main() 292 | -------------------------------------------------------------------------------- /library/dokku_global_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import re 6 | import subprocess 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_global_cert 11 | short_description: Manages global ssl configuration. 12 | description: 13 | - Manages ssl configuration for the server. 14 | - Will not update certificates 15 | - Only checks for presence of the crt file, not the key 16 | options: 17 | key: 18 | description: 19 | - Path to the ssl certificate key 20 | required: True 21 | default: null 22 | aliases: [] 23 | cert: 24 | description: 25 | - Path to the ssl certificate 26 | required: True 27 | default: null 28 | aliases: [] 29 | state: 30 | description: 31 | - The state of the ssl configuration 32 | required: False 33 | default: present 34 | choices: [ "present", "absent" ] 35 | aliases: [] 36 | author: Jose Diaz-Gonzalez 37 | requirements: 38 | - the `dokku-global-cert` plugin 39 | """ 40 | 41 | EXAMPLES = """ 42 | - name: Adds an ssl certificate and key 43 | dokku_global_cert: 44 | key: /etc/nginx/ssl/global-hello-world.key 45 | cert: /etc/nginx/ssl/global-hello-world.crt 46 | 47 | - name: Removes an ssl certificate and key 48 | dokku_global_cert: 49 | state: absent 50 | """ 51 | 52 | 53 | def to_bool(v): 54 | return v.lower() == "true" 55 | 56 | 57 | def dokku_global_cert(data): 58 | command = "dokku --quiet global-cert:report" 59 | output, error = subprocess_check_output(command) 60 | if error is not None: 61 | return output, error 62 | output = [re.sub(r"\s\s+", "", line) for line in output] 63 | report = {} 64 | 65 | allowed_keys = [ 66 | "dir", 67 | "enabled", 68 | "hostnames", 69 | "expires at", 70 | "issuer", 71 | "starts at", 72 | "subject", 73 | "verified", 74 | ] 75 | RE_PREFIX = re.compile("^global-cert-") 76 | for line in output: 77 | if ":" not in line: 78 | continue 79 | key, value = line.split(":", 1) 80 | key = RE_PREFIX.sub(r"", key.replace(" ", "-").lower()) 81 | if key not in allowed_keys: 82 | continue 83 | 84 | if key == "enabled": 85 | value = to_bool(value) 86 | report[key] = value 87 | 88 | return report, error 89 | 90 | 91 | def dokku_global_cert_absent(data=None): 92 | has_changed = False 93 | is_error = True 94 | meta = {"present": True} 95 | 96 | report, error = dokku_global_cert(data) 97 | if error: 98 | meta["error"] = error 99 | return (is_error, has_changed, meta) 100 | 101 | if not report["enabled"]: 102 | is_error = False 103 | meta["present"] = False 104 | return (is_error, has_changed, meta) 105 | 106 | command = "dokku --quiet global-cert:remove" 107 | try: 108 | subprocess.check_call(command, shell=True) 109 | is_error = False 110 | has_changed = True 111 | meta["present"] = False 112 | except subprocess.CalledProcessError as e: 113 | meta["error"] = str(e) 114 | 115 | return (is_error, has_changed, meta) 116 | 117 | 118 | def dokku_global_cert_present(data): 119 | is_error = True 120 | has_changed = False 121 | meta = {"present": False} 122 | 123 | report, error = dokku_global_cert(data) 124 | if error: 125 | meta["error"] = error 126 | return (is_error, has_changed, meta) 127 | 128 | if report["enabled"]: 129 | is_error = False 130 | meta["present"] = False 131 | return (is_error, has_changed, meta) 132 | 133 | command = "dokku --quiet global-cert:set {0} {1}".format(data["cert"], data["key"]) 134 | try: 135 | subprocess.check_call(command, shell=True) 136 | is_error = False 137 | has_changed = True 138 | meta["present"] = True 139 | except subprocess.CalledProcessError as e: 140 | meta["error"] = str(e) 141 | 142 | return (is_error, has_changed, meta) 143 | 144 | 145 | def main(): 146 | fields = { 147 | "key": {"required": False, "type": "str"}, 148 | "cert": {"required": False, "type": "str"}, 149 | "state": { 150 | "required": False, 151 | "default": "present", 152 | "choices": ["present", "absent"], 153 | "type": "str", 154 | }, 155 | } 156 | choice_map = { 157 | "present": dokku_global_cert_present, 158 | "absent": dokku_global_cert_absent, 159 | } 160 | 161 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 162 | is_error, has_changed, result = choice_map.get(module.params["state"])( 163 | module.params 164 | ) 165 | 166 | if is_error: 167 | module.fail_json(msg=result["error"], meta=result) 168 | module.exit_json(changed=has_changed, meta=result) 169 | 170 | 171 | if __name__ == "__main__": 172 | main() 173 | -------------------------------------------------------------------------------- /library/dokku_http_auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import subprocess 4 | 5 | from ansible.module_utils.basic import AnsibleModule 6 | from ansible.module_utils.dokku_utils import subprocess_check_output 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_http_auth 11 | short_description: Manage HTTP Basic Authentication for a dokku app 12 | options: 13 | app: 14 | description: 15 | - The name of the app 16 | required: True 17 | default: null 18 | aliases: [] 19 | state: 20 | description: 21 | - The state of the http-auth plugin 22 | required: False 23 | default: present 24 | choices: [ "present", "absent" ] 25 | aliases: [] 26 | username: 27 | description: 28 | - The HTTP Auth Username (required for 'present' state) 29 | required: False 30 | aliases: [] 31 | password: 32 | description: 33 | - The HTTP Auth Password (required for 'present' state) 34 | required: False 35 | aliases: [] 36 | author: Simo Aleksandrov 37 | requirements: 38 | - the `dokku-http-auth` plugin 39 | """ 40 | 41 | EXAMPLES = """ 42 | - name: Enable the http-auth plugin 43 | dokku_http_auth: 44 | app: hello-world 45 | state: present 46 | username: samsepi0l 47 | password: hunter2 48 | 49 | - name: Disable the http-auth plugin 50 | dokku_http_auth: 51 | app: hello-world 52 | state: absent 53 | """ 54 | 55 | 56 | def dokku_http_auth_enabled(data): 57 | command = "dokku --quiet http-auth:report {0}" 58 | response, error = subprocess_check_output(command.format(data["app"])) 59 | 60 | if error: 61 | return None, error 62 | 63 | report = response[0].split(":")[1] 64 | return report.strip() == "true", error 65 | 66 | 67 | def dokku_http_auth_present(data): 68 | is_error = True 69 | has_changed = False 70 | meta = {"present": False} 71 | 72 | enabled, error = dokku_http_auth_enabled(data) 73 | if error: 74 | meta["error"] = error 75 | return (is_error, has_changed, meta) 76 | 77 | if enabled: 78 | is_error = False 79 | meta["present"] = True 80 | return (is_error, has_changed, meta) 81 | 82 | command = "dokku --quiet http-auth:on {0} {1} {2}".format( 83 | data["app"], data["username"], data["password"] 84 | ) 85 | try: 86 | subprocess.check_call(command, shell=True) 87 | is_error = False 88 | has_changed = True 89 | meta["present"] = True 90 | except subprocess.CalledProcessError as e: 91 | meta["error"] = str(e) 92 | 93 | return (is_error, has_changed, meta) 94 | 95 | 96 | def dokku_http_auth_absent(data=None): 97 | is_error = True 98 | has_changed = False 99 | meta = {"present": True} 100 | 101 | enabled, error = dokku_http_auth_enabled(data) 102 | if error: 103 | meta["error"] = error 104 | return (is_error, has_changed, meta) 105 | 106 | if enabled is False: 107 | is_error = False 108 | meta["present"] = False 109 | return (is_error, has_changed, meta) 110 | 111 | command = "dokku --quiet http-auth:off {0}".format(data["app"]) 112 | try: 113 | subprocess.check_call(command, shell=True) 114 | is_error = False 115 | has_changed = True 116 | meta["present"] = False 117 | except subprocess.CalledProcessError as e: 118 | meta["error"] = str(e) 119 | 120 | return (is_error, has_changed, meta) 121 | 122 | 123 | def main(): 124 | fields = { 125 | "app": {"required": True, "type": "str"}, 126 | "state": { 127 | "required": False, 128 | "default": "present", 129 | "choices": ["present", "absent"], 130 | "type": "str", 131 | }, 132 | "username": {"required": False, "type": "str"}, 133 | "password": {"required": False, "type": "str", "no_log": True}, 134 | } 135 | choice_map = { 136 | "present": dokku_http_auth_present, 137 | "absent": dokku_http_auth_absent, 138 | } 139 | 140 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 141 | is_error, has_changed, result = choice_map.get(module.params["state"])( 142 | module.params 143 | ) 144 | 145 | if is_error: 146 | module.fail_json(msg=result["error"], meta=result) 147 | module.exit_json(changed=has_changed, meta=result) 148 | 149 | 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /library/dokku_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import subprocess 4 | 5 | from ansible.module_utils.basic import AnsibleModule 6 | from ansible.module_utils.dokku_app import dokku_app_ensure_present 7 | from ansible.module_utils.dokku_git import dokku_git_sha 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_image 12 | short_description: Pull Docker image and deploy app 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | image: 21 | description: 22 | - Docker image 23 | required: True 24 | default: null 25 | aliases: [] 26 | user_name: 27 | description: 28 | - Git user.name for customizing the author's name 29 | required: False 30 | default: null 31 | aliases: [] 32 | user_email: 33 | description: 34 | - Git user.email for customizing the author's email 35 | required: False 36 | default: null 37 | aliases: [] 38 | build_dir: 39 | description: 40 | - Specify custom build directory for a custom build context 41 | required: False 42 | default: null 43 | aliases: [] 44 | author: Simo Aleksandrov 45 | """ 46 | 47 | EXAMPLES = """ 48 | - name: Pull and deploy meilisearch 49 | dokku_image: 50 | app: meilisearch 51 | image: getmeili/meilisearch:v0.24.0rc1 52 | - name: Pull and deploy image with custom author 53 | dokku_image: 54 | app: hello-world 55 | user_name: Elliot Alderson 56 | user_email: elliotalderson@protonmail.ch 57 | image: hello-world:latest 58 | - name: Pull and deploy image with custom build dir 59 | dokku_image: 60 | app: hello-world 61 | build_dir: /path/to/build 62 | image: hello-world:latest 63 | """ 64 | 65 | 66 | def dokku_image(data): 67 | # create app (if not exists) 68 | is_error, has_changed, meta = dokku_app_ensure_present(data) 69 | meta["present"] = False # meaning: requested *version* of app is present 70 | if is_error: 71 | return (is_error, has_changed, meta) 72 | 73 | sha_old = dokku_git_sha(data["app"]) 74 | 75 | # get image 76 | command_git_from_image = "dokku git:from-image {app} {image}".format( 77 | app=data["app"], image=data["image"] 78 | ) 79 | if data["user_name"]: 80 | command_git_from_image += " {user_name}".format(user_name=data["user_name"]) 81 | if data["user_email"]: 82 | command_git_from_image += " {user_email}".format(user_email=data["user_email"]) 83 | if data["build_dir"]: 84 | command_git_from_image += ' --build-dir "{build_dir}"'.format( 85 | build_dir=data["build_dir"] 86 | ) 87 | try: 88 | subprocess.check_output( 89 | command_git_from_image, stderr=subprocess.STDOUT, shell=True 90 | ) 91 | except subprocess.CalledProcessError as e: 92 | is_error = True 93 | if "is not a dokku command" in str(e.output): 94 | meta["error"] = ( 95 | "Please upgrade to dokku>=0.24.0 in order to use the 'git:from-image' command." 96 | ) 97 | elif "No changes detected, skipping git commit" in str(e.output): 98 | is_error = False 99 | has_changed = False 100 | else: 101 | meta["error"] = str(e.output) 102 | return (is_error, has_changed, meta) 103 | finally: 104 | meta["present"] = True # meaning: requested *version* of app is present 105 | 106 | if dokku_git_sha(data["app"]) != sha_old: 107 | has_changed = True 108 | 109 | return (is_error, has_changed, meta) 110 | 111 | 112 | def main(): 113 | fields = { 114 | "app": {"required": True, "type": "str"}, 115 | "image": {"required": True, "type": "str"}, 116 | "user_name": {"required": False, "type": "str"}, 117 | "user_email": {"required": False, "type": "str"}, 118 | "build_dir": {"required": False, "type": "str"}, 119 | } 120 | 121 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 122 | is_error, has_changed, result = dokku_image(module.params) 123 | 124 | if is_error: 125 | module.fail_json(msg=result["error"], meta=result) 126 | module.exit_json(changed=has_changed, meta=result) 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /library/dokku_letsencrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | 7 | DOCUMENTATION = """ 8 | --- 9 | module: dokku_letsencrypt 10 | short_description: Enable or disable the letsencrypt plugin for a dokku app 11 | options: 12 | app: 13 | description: 14 | - The name of the app 15 | required: True 16 | default: null 17 | aliases: [] 18 | state: 19 | description: 20 | - The state of the letsencrypt plugin 21 | required: False 22 | default: present 23 | choices: [ "present", "absent" ] 24 | aliases: [] 25 | author: Gavin Ballard 26 | requirements: 27 | - the `dokku-letsencrypt` plugin 28 | """ 29 | 30 | EXAMPLES = """ 31 | - name: Enable the letsencrypt plugin 32 | dokku_letsencrypt: 33 | app: hello-world 34 | 35 | - name: Disable the letsencrypt plugin 36 | dokku_letsencrypt: 37 | app: hello-world 38 | state: absent 39 | """ 40 | 41 | 42 | def dokku_letsencrypt_enabled(data): 43 | command = "dokku --quiet letsencrypt:list | awk '{{print $1}}'" 44 | response, error = subprocess_check_output(command.format(data["app"])) 45 | 46 | if error: 47 | return None, error 48 | 49 | return data["app"] in response, error 50 | 51 | 52 | def dokku_letsencrypt_present(data): 53 | is_error = True 54 | has_changed = False 55 | meta = {"present": False} 56 | 57 | enabled, error = dokku_letsencrypt_enabled(data) 58 | if enabled: 59 | is_error = False 60 | meta["present"] = True 61 | return (is_error, has_changed, meta) 62 | 63 | command = "dokku --quiet letsencrypt:enable {0}".format(data["app"]) 64 | try: 65 | subprocess.check_call(command, shell=True) 66 | is_error = False 67 | has_changed = True 68 | meta["present"] = True 69 | except subprocess.CalledProcessError as e: 70 | meta["error"] = str(e) 71 | 72 | return (is_error, has_changed, meta) 73 | 74 | 75 | def dokku_letsencrypt_absent(data=None): 76 | is_error = True 77 | has_changed = False 78 | meta = {"present": True} 79 | 80 | enabled, error = dokku_letsencrypt_enabled(data) 81 | if enabled is False: 82 | is_error = False 83 | meta["present"] = False 84 | return (is_error, has_changed, meta) 85 | 86 | command = "dokku --quiet letsencrypt:disable {0}".format(data["app"]) 87 | try: 88 | subprocess.check_call(command, shell=True) 89 | is_error = False 90 | has_changed = True 91 | meta["present"] = False 92 | except subprocess.CalledProcessError as e: 93 | meta["error"] = str(e) 94 | 95 | return (is_error, has_changed, meta) 96 | 97 | 98 | def main(): 99 | fields = { 100 | "app": {"required": True, "type": "str"}, 101 | "state": { 102 | "required": False, 103 | "default": "present", 104 | "choices": ["present", "absent"], 105 | "type": "str", 106 | }, 107 | } 108 | choice_map = { 109 | "present": dokku_letsencrypt_present, 110 | "absent": dokku_letsencrypt_absent, 111 | } 112 | 113 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 114 | is_error, has_changed, result = choice_map.get(module.params["state"])( 115 | module.params 116 | ) 117 | 118 | if is_error: 119 | module.fail_json(msg=result["error"], meta=result) 120 | module.exit_json(changed=has_changed, meta=result) 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /library/dokku_network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | import subprocess 5 | 6 | DOCUMENTATION = """ 7 | --- 8 | module: dokku_network 9 | short_description: Create or destroy container networks for dokku apps 10 | options: 11 | name: 12 | description: 13 | - The name of the network 14 | required: True 15 | default: null 16 | aliases: [] 17 | state: 18 | description: 19 | - The state of the network 20 | required: False 21 | default: present 22 | choices: [ "present", "absent" ] 23 | aliases: [] 24 | author: Philipp Sessler 25 | requirements: [ ] 26 | """ 27 | 28 | EXAMPLES = """ 29 | - name: Create a network 30 | dokku_network: 31 | name: example-network 32 | 33 | - name: Delete that network 34 | dokku_network: 35 | name: example-network 36 | state: absent 37 | """ 38 | 39 | 40 | def dokku_network_exists(network): 41 | exists = False 42 | error = None 43 | command = "dokku --quiet network:exists {0}".format(network) 44 | try: 45 | subprocess.check_call(command, shell=True) 46 | exists = True 47 | except subprocess.CalledProcessError as e: 48 | error = str(e) 49 | return exists, error 50 | 51 | 52 | def dokku_network_present(data): 53 | is_error = True 54 | has_changed = False 55 | meta = {"present": False} 56 | 57 | exists, error = dokku_network_exists(data["name"]) 58 | if exists: 59 | is_error = False 60 | meta["present"] = True 61 | return (is_error, has_changed, meta) 62 | 63 | command = "dokku network:create {0}".format(data["name"]) 64 | try: 65 | subprocess.check_call(command, shell=True) 66 | is_error = False 67 | has_changed = True 68 | meta["present"] = True 69 | except subprocess.CalledProcessError as e: 70 | meta["error"] = str(e) 71 | 72 | return (is_error, has_changed, meta) 73 | 74 | 75 | def dokku_network_absent(data=None): 76 | is_error = True 77 | has_changed = False 78 | meta = {"present": True} 79 | 80 | exists, error = dokku_network_exists(data["name"]) 81 | if not exists: 82 | is_error = False 83 | meta["present"] = False 84 | return (is_error, has_changed, meta) 85 | 86 | command = "dokku --force network:destroy {0}".format(data["name"]) 87 | try: 88 | subprocess.check_call(command, shell=True) 89 | is_error = False 90 | has_changed = True 91 | meta["present"] = False 92 | except subprocess.CalledProcessError as e: 93 | meta["error"] = str(e) 94 | 95 | return (is_error, has_changed, meta) 96 | 97 | 98 | def main(): 99 | fields = { 100 | "name": {"required": True, "type": "str"}, 101 | "state": { 102 | "required": False, 103 | "default": "present", 104 | "choices": ["present", "absent"], 105 | "type": "str", 106 | }, 107 | } 108 | choice_map = { 109 | "present": dokku_network_present, 110 | "absent": dokku_network_absent, 111 | } 112 | 113 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 114 | is_error, has_changed, result = choice_map.get(module.params["state"])( 115 | module.params 116 | ) 117 | 118 | if is_error: 119 | module.fail_json(msg=result["error"], meta=result) 120 | module.exit_json(changed=has_changed, meta=result) 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /library/dokku_network_property.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | import subprocess 5 | 6 | DOCUMENTATION = """ 7 | --- 8 | module: dokku_network_property 9 | short_description: Set or clear a network property for a given dokku application 10 | options: 11 | global: 12 | description: 13 | - Whether to change the global network property 14 | default: False 15 | aliases: [] 16 | app: 17 | description: 18 | - The name of the app. This is required only if global is set to False. 19 | required: True 20 | default: null 21 | aliases: [] 22 | property: 23 | description: 24 | - > 25 | The network property to be be modified. This can be any property supported 26 | by dokku (e.g., `initial-network`, `attach-post-create`, `attach-post-deploy`, 27 | `bind-all-interfaces`, `static-web-listener`, `tld`). 28 | required: True 29 | default: null 30 | aliases: [] 31 | value: 32 | description: 33 | - The value of the network property (leave empty to unset) 34 | required: False 35 | default: null 36 | aliases: [] 37 | author: Philipp Sessler 38 | requirements: [ ] 39 | """ 40 | 41 | EXAMPLES = """ 42 | - name: Associates a network after a container is created but before it is started 43 | dokku_network_property: 44 | app: hello-world 45 | property: attach-post-create 46 | value: example-network 47 | 48 | - name: Associates the network at container creation 49 | dokku_network_property: 50 | app: hello-world 51 | property: initial-network 52 | value: example-network 53 | 54 | - name: Setting a global network property 55 | dokku_network_property: 56 | global: true 57 | property: attach-post-create 58 | value: example-network 59 | 60 | - name: Clearing a network property 61 | dokku_network_property: 62 | app: hello-world 63 | property: attach-post-create 64 | """ 65 | 66 | 67 | def dokku_network_property_set(data): 68 | is_error = True 69 | has_changed = False 70 | meta = {"present": False} 71 | 72 | if data["global"] and data["app"]: 73 | is_error = True 74 | meta["error"] = 'When "global" is set to true, "app" must not be provided.' 75 | return (is_error, has_changed, meta) 76 | 77 | # Check if "value" is set and evaluates to a non-empty string, otherwise use an empty string 78 | value = data["value"] if "value" in data else None 79 | if not value: 80 | value = "" 81 | 82 | command = "dokku network:set {0} {1} {2}".format( 83 | "--global" if data["global"] else data["app"], 84 | data["property"], 85 | value, 86 | ) 87 | 88 | try: 89 | subprocess.check_call(command, shell=True) 90 | is_error = False 91 | has_changed = True 92 | meta["present"] = True 93 | except subprocess.CalledProcessError as e: 94 | meta["error"] = str(e) 95 | 96 | return (is_error, has_changed, meta) 97 | 98 | 99 | def main(): 100 | fields = { 101 | "global": {"required": False, "default": False, "type": "bool"}, 102 | "app": {"required": False, "type": "str"}, 103 | "property": {"required": True, "type": "str"}, 104 | "value": {"required": False, "type": "str"}, 105 | } 106 | 107 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 108 | is_error, has_changed, result = dokku_network_property_set(module.params) 109 | 110 | if is_error: 111 | module.fail_json(msg=result["error"], meta=result) 112 | module.exit_json(changed=has_changed, meta=result) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /library/dokku_ports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output, get_dokku_version 5 | import pipes 6 | import re 7 | import subprocess 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_ports 12 | short_description: Manage ports for a given dokku application 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | mappings: 21 | description: 22 | - A list of port mappings 23 | required: True 24 | default: null 25 | aliases: [] 26 | state: 27 | description: 28 | - The state of the port mappings 29 | required: False 30 | default: present 31 | choices: [ "clear", "present", "absent" ] 32 | aliases: [] 33 | author: Jose Diaz-Gonzalez 34 | requirements: [ ] 35 | """ 36 | 37 | EXAMPLES = """ 38 | - name: ports:set hello-world http:80:80 39 | dokku_ports: 40 | app: hello-world 41 | mappings: 42 | - http:80:8080 43 | 44 | - name: ports:remove hello-world http:80:80 45 | dokku_ports: 46 | app: hello-world 47 | mappings: 48 | - http:80:8080 49 | state: absent 50 | 51 | - name: ports:clear hello-world 52 | dokku_ports: 53 | app: hello-world 54 | state: clear 55 | """ 56 | 57 | 58 | def dokku_proxy_port_mappings(data): 59 | mappings = [] 60 | 61 | if use_legacy_command(): 62 | command = "dokku --quiet proxy:report {0}".format(data["app"]) 63 | else: 64 | command = "dokku --quiet ports:report {0} --ports-map".format(data["app"]) 65 | 66 | output, error = subprocess_check_output(command) 67 | if error is None: 68 | if use_legacy_command(): 69 | for line in output: 70 | match = re.match("Proxy port map:(?P.+)", line.strip()) 71 | if match: 72 | mappings = match.group("mapping").strip().split(" ") 73 | else: 74 | if output: 75 | mappings = output[0].strip().split(" ") 76 | 77 | return mappings, error 78 | 79 | 80 | def dokku_proxy_ports_absent(data): 81 | is_error = True 82 | has_changed = False 83 | meta = {"present": True} 84 | 85 | if "mappings" not in data: 86 | meta["error"] = "missing required arguments: mappings" 87 | return (is_error, has_changed, meta) 88 | 89 | existing, error = dokku_proxy_port_mappings(data) 90 | if error: 91 | meta["error"] = error 92 | return (is_error, has_changed, meta) 93 | 94 | to_remove = [m for m in data["mappings"] if m in existing] 95 | to_remove = [pipes.quote(m) for m in to_remove] 96 | 97 | if len(to_remove) == 0: 98 | is_error = False 99 | meta["present"] = False 100 | return (is_error, has_changed, meta) 101 | 102 | if use_legacy_command(): 103 | subcommand = "proxy:ports-remove" 104 | else: 105 | subcommand = "ports:remove" 106 | 107 | command = "dokku --quiet {0} {1} {2}".format( 108 | subcommand, data["app"], " ".join(to_remove) 109 | ) 110 | try: 111 | subprocess.check_call(command, shell=True) 112 | is_error = False 113 | has_changed = True 114 | meta["present"] = False 115 | except subprocess.CalledProcessError as e: 116 | meta["error"] = str(e) 117 | 118 | return (is_error, has_changed, meta) 119 | 120 | 121 | def dokku_proxy_ports_clear(data): 122 | is_error = True 123 | has_changed = False 124 | meta = {"present": True} 125 | 126 | if use_legacy_command(): 127 | subcommand = "proxy:ports-clear" 128 | else: 129 | subcommand = "ports:clear" 130 | 131 | command = "dokku --quiet {0} {1}".format(subcommand, data["app"]) 132 | try: 133 | subprocess.check_call(command, shell=True) 134 | is_error = False 135 | has_changed = True 136 | meta["present"] = False 137 | except subprocess.CalledProcessError as e: 138 | meta["error"] = str(e) 139 | 140 | return (is_error, has_changed, meta) 141 | 142 | 143 | def dokku_proxy_ports_present(data): 144 | is_error = True 145 | has_changed = False 146 | meta = {"present": False} 147 | 148 | if "mappings" not in data: 149 | meta["error"] = "missing required arguments: mappings" 150 | return (is_error, has_changed, meta) 151 | 152 | existing, error = dokku_proxy_port_mappings(data) 153 | if error: 154 | meta["error"] = error 155 | return (is_error, has_changed, meta) 156 | 157 | to_add = [m for m in data["mappings"] if m not in existing] 158 | to_set = [pipes.quote(m) for m in data["mappings"]] 159 | 160 | if len(to_add) == 0: 161 | is_error = False 162 | meta["present"] = True 163 | return (is_error, has_changed, meta) 164 | 165 | if use_legacy_command(): 166 | subcommand = "proxy:ports-set" 167 | else: 168 | subcommand = "ports:set" 169 | 170 | command = "dokku {0} {1} {2}".format(subcommand, data["app"], " ".join(to_set)) 171 | try: 172 | subprocess.check_call(command, shell=True) 173 | is_error = False 174 | has_changed = True 175 | meta["present"] = True 176 | except subprocess.CalledProcessError as e: 177 | meta["error"] = str(e) 178 | 179 | return (is_error, has_changed, meta) 180 | 181 | 182 | def use_legacy_command() -> bool: 183 | """ 184 | The commands for managing ports changed with dokku version 0.31.0. 185 | Use the legacy commands if the installed version of dokku is older than that. 186 | 187 | https://github.com/dokku/dokku/blob/master/docs/networking/port-management.md#port-management 188 | """ 189 | dokku_version = get_dokku_version() 190 | return dokku_version < (0, 31, 0) 191 | 192 | 193 | def main(): 194 | fields = { 195 | "app": {"required": True, "type": "str"}, 196 | "mappings": {"required": False, "type": "list"}, 197 | "state": { 198 | "required": False, 199 | "default": "present", 200 | "choices": ["absent", "clear", "present"], 201 | "type": "str", 202 | }, 203 | } 204 | choice_map = { 205 | "absent": dokku_proxy_ports_absent, 206 | "clear": dokku_proxy_ports_clear, 207 | "present": dokku_proxy_ports_present, 208 | } 209 | 210 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 211 | is_error, has_changed, result = choice_map.get(module.params["state"])( 212 | module.params 213 | ) 214 | 215 | if is_error: 216 | module.fail_json(msg=result["error"], meta=result) 217 | module.exit_json(changed=has_changed, meta=result) 218 | 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /library/dokku_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | 7 | DOCUMENTATION = """ 8 | --- 9 | module: dokku_proxy 10 | short_description: Enable or disable the proxy for a dokku app 11 | options: 12 | app: 13 | description: 14 | - The name of the app 15 | required: True 16 | default: null 17 | aliases: [] 18 | state: 19 | description: 20 | - The state of the proxy 21 | required: False 22 | default: present 23 | choices: [ "present", "absent" ] 24 | aliases: [] 25 | author: Jose Diaz-Gonzalez 26 | requirements: [ ] 27 | """ 28 | 29 | EXAMPLES = """ 30 | - name: Enable the default proxy 31 | dokku_proxy: 32 | app: hello-world 33 | 34 | - name: Disable the default proxy 35 | dokku_proxy: 36 | app: hello-world 37 | state: absent 38 | """ 39 | 40 | 41 | def dokku_proxy(data): 42 | command = "dokku --quiet config:get {0} DOKKU_DISABLE_PROXY" 43 | response, error = subprocess_check_output(command.format(data["app"])) 44 | if error: 45 | return "0", error 46 | return response[0], error 47 | 48 | 49 | def dokku_proxy_present(data): 50 | is_error = True 51 | has_changed = False 52 | meta = {"present": False} 53 | 54 | disabled, error = dokku_proxy(data) 55 | if disabled == "0": 56 | is_error = False 57 | meta["present"] = True 58 | return (is_error, has_changed, meta) 59 | 60 | command = "dokku --quiet proxy:enable {0}".format(data["app"]) 61 | try: 62 | subprocess.check_call(command, shell=True) 63 | is_error = False 64 | has_changed = True 65 | meta["present"] = True 66 | except subprocess.CalledProcessError as e: 67 | meta["error"] = str(e) 68 | 69 | return (is_error, has_changed, meta) 70 | 71 | 72 | def dokku_proxy_absent(data=None): 73 | is_error = True 74 | has_changed = False 75 | meta = {"present": True} 76 | 77 | disabled, error = dokku_proxy(data) 78 | if disabled == "1": 79 | is_error = False 80 | meta["present"] = False 81 | return (is_error, has_changed, meta) 82 | 83 | command = "dokku --force proxy:disable {0}".format(data["app"]) 84 | try: 85 | subprocess.check_call(command, shell=True) 86 | is_error = False 87 | has_changed = True 88 | meta["present"] = False 89 | except subprocess.CalledProcessError as e: 90 | meta["error"] = str(e) 91 | 92 | return (is_error, has_changed, meta) 93 | 94 | 95 | def main(): 96 | fields = { 97 | "app": {"required": True, "type": "str"}, 98 | "state": { 99 | "required": False, 100 | "default": "present", 101 | "choices": ["present", "absent"], 102 | "type": "str", 103 | }, 104 | } 105 | choice_map = { 106 | "present": dokku_proxy_present, 107 | "absent": dokku_proxy_absent, 108 | } 109 | 110 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 111 | is_error, has_changed, result = choice_map.get(module.params["state"])( 112 | module.params 113 | ) 114 | 115 | if is_error: 116 | module.fail_json(msg=result["error"], meta=result) 117 | module.exit_json(changed=has_changed, meta=result) 118 | 119 | 120 | if __name__ == "__main__": 121 | main() 122 | -------------------------------------------------------------------------------- /library/dokku_ps_scale.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | import re 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_ps_scale 11 | short_description: Manage process scaling for a given dokku application 12 | options: 13 | app: 14 | description: 15 | - The name of the app 16 | required: True 17 | default: null 18 | aliases: [] 19 | scale: 20 | description: 21 | - A map of scale values where proctype => qty 22 | required: True 23 | default: {} 24 | aliases: [] 25 | skip_deploy: 26 | description: 27 | - Whether to skip the corresponding deploy or not. If the task is idempotent 28 | then leaving skip_deploy as false will not trigger a deploy. 29 | required: false 30 | default: false 31 | author: Gavin Ballard 32 | requirements: [ ] 33 | """ 34 | 35 | EXAMPLES = """ 36 | - name: scale web and worker processes 37 | dokku_ps_scale: 38 | app: hello-world 39 | scale: 40 | web: 4 41 | worker: 4 42 | 43 | - name: scale web and worker processes without deploy 44 | dokku_ps_scale: 45 | app: hello-world 46 | skip_deploy: true 47 | scale: 48 | web: 4 49 | worker: 4 50 | """ 51 | 52 | 53 | def dokku_ps_scale(data): 54 | command = "dokku --quiet ps:scale {0}".format(data["app"]) 55 | output, error = subprocess_check_output(command) 56 | 57 | if error is not None: 58 | return output, error 59 | 60 | # strip all spaces from output lines 61 | output = [re.sub(r"\s+", "", line) for line in output] 62 | 63 | scale = {} 64 | for line in output: 65 | if ":" not in line: 66 | continue 67 | proctype, qty = line.split(":", 1) 68 | scale[proctype] = int(qty) 69 | 70 | return scale, error 71 | 72 | 73 | def dokku_ps_scale_set(data): 74 | is_error = True 75 | has_changed = False 76 | meta = {"present": False} 77 | 78 | proctypes_to_scale = [] 79 | 80 | existing, error = dokku_ps_scale(data) 81 | 82 | for proctype, qty in data["scale"].items(): 83 | if qty == existing.get(proctype, None): 84 | continue 85 | proctypes_to_scale.append("{0}={1}".format(proctype, qty)) 86 | 87 | if len(proctypes_to_scale) == 0: 88 | is_error = False 89 | has_changed = False 90 | return (is_error, has_changed, meta) 91 | 92 | command = "dokku ps:scale {0}{1} {2}".format( 93 | "--skip-deploy " if data["skip_deploy"] is True else "", 94 | data["app"], 95 | " ".join(proctypes_to_scale), 96 | ) 97 | 98 | try: 99 | subprocess.check_call(command, shell=True) 100 | is_error = False 101 | has_changed = True 102 | except subprocess.CalledProcessError as e: 103 | meta["error"] = str(e) 104 | 105 | return (is_error, has_changed, meta) 106 | 107 | 108 | def main(): 109 | fields = { 110 | "app": {"required": True, "type": "str"}, 111 | "scale": {"required": True, "type": "dict", "no_log": True}, 112 | "skip_deploy": {"required": False, "type": "bool"}, 113 | } 114 | 115 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 116 | is_error, has_changed, result = dokku_ps_scale_set(module.params) 117 | 118 | if is_error: 119 | module.fail_json(msg=result["error"], meta=result) 120 | module.exit_json(changed=has_changed, meta=result) 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /library/dokku_registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import pipes 6 | import re 7 | import subprocess 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_registry 12 | short_description: Manage the registry configuration for a given dokku application 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: True 18 | default: null 19 | aliases: [] 20 | image: 21 | description: 22 | - Alternative to app name for image repository name 23 | required: False 24 | aliases: [] 25 | password: 26 | description: 27 | - The registry password (required for 'present' state) 28 | required: False 29 | aliases: [] 30 | server: 31 | description: 32 | - The registry server hostname (required for 'present' state) 33 | required: False 34 | aliases: [] 35 | username: 36 | description: 37 | - The registry username (required for 'present' state) 38 | required: False 39 | aliases: [] 40 | state: 41 | description: 42 | - The state of the registry integration 43 | required: False 44 | default: present 45 | choices: ["present", "absent" ] 46 | aliases: [] 47 | author: Jose Diaz-Gonzalez 48 | requirements: 49 | - the `dokku-registry` plugin 50 | """ 51 | 52 | EXAMPLES = """ 53 | - name: registry:enable hello-world 54 | dokku_registry: 55 | app: hello-world 56 | password: password 57 | server: localhost:8080 58 | username: user 59 | 60 | - name: registry:enable hello-world with args 61 | dokku_registry: 62 | app: hello-world 63 | image: other-image 64 | password: password 65 | server: localhost:8080 66 | username: user 67 | 68 | - name: registry:disable hello-world 69 | dokku_registry: 70 | app: hello-world 71 | state: absent 72 | """ 73 | 74 | 75 | def to_bool(v): 76 | return v.lower() == "true" 77 | 78 | 79 | def to_str(v): 80 | return "true" if v else "false" 81 | 82 | 83 | def dokku_module_set(command_prefix, data, key, value=None): 84 | has_changed = False 85 | error = None 86 | 87 | if value: 88 | command = "dokku --quiet {0}:set {1} {2} {3}".format( 89 | command_prefix, data["app"], key, pipes.quote(value) 90 | ) 91 | else: 92 | command = "dokku --quiet {0}:set {1} {2}".format( 93 | command_prefix, data["app"], key 94 | ) 95 | 96 | try: 97 | subprocess.check_call(command, shell=True) 98 | has_changed = True 99 | except subprocess.CalledProcessError as e: 100 | error = str(e) 101 | 102 | return (has_changed, error) 103 | 104 | 105 | def dokku_module_set_blank(command_prefix, data, setable_fields): 106 | error = None 107 | errors = [] 108 | changed_keys = [] 109 | has_changed = False 110 | 111 | for key in setable_fields: 112 | changed, error = dokku_module_set(command_prefix, data, key) 113 | if changed: 114 | has_changed = True 115 | changed_keys.append(key) 116 | if error: 117 | errors.append(error) 118 | 119 | if len(errors) > 0: 120 | error = ",".join(errors) 121 | 122 | return (has_changed, changed_keys, error) 123 | 124 | 125 | def dokku_module_set_values(command_prefix, data, report, setable_fields): 126 | error = None 127 | errors = [] 128 | changed_keys = [] 129 | has_changed = False 130 | 131 | if "enabled" in data: 132 | data["enabled"] = to_str(data["enabled"]) 133 | if "enabled" in report: 134 | report["enabled"] = to_str(report["enabled"]) 135 | 136 | for key, value in report.items(): 137 | if key not in setable_fields: 138 | continue 139 | if data.get(key, None) is None: 140 | continue 141 | if data[key] == value: 142 | continue 143 | 144 | changed, error = dokku_module_set(command_prefix, data, key, data[key]) 145 | if error: 146 | errors.append(error) 147 | if changed: 148 | has_changed = True 149 | changed_keys.append(key) 150 | 151 | if len(errors) > 0: 152 | error = ",".join(errors) 153 | 154 | return (has_changed, changed_keys, error) 155 | 156 | 157 | def dokku_module_require_fields(data, required_fields): 158 | error = None 159 | missing_keys = [] 160 | 161 | if len(required_fields) > 0: 162 | for key in required_fields: 163 | if data.get(key, None) is None: 164 | missing_keys.append(key) 165 | 166 | if len(missing_keys) > 0: 167 | error = "missing required arguments: {0}".format(", ".join(missing_keys)) 168 | 169 | return error 170 | 171 | 172 | def dokku_module_report(command_prefix, data, re_compiled, allowed_report_keys): 173 | command = "dokku --quiet {0}:report {1}".format(command_prefix, data["app"]) 174 | output, error = subprocess_check_output(command) 175 | if error is not None: 176 | return output, error 177 | 178 | output = [re.sub(r"\s\s+", "", line) for line in output] 179 | report = {} 180 | 181 | for line in output: 182 | if ":" not in line: 183 | continue 184 | key, value = line.split(":", 1) 185 | key = re_compiled.sub(r"", key.replace(" ", "-").lower()) 186 | if key not in allowed_report_keys: 187 | continue 188 | 189 | value = value.strip() 190 | if key == "enabled": 191 | value = to_bool(value) 192 | report[key] = value 193 | 194 | return report, error 195 | 196 | 197 | def dokku_module_absent( 198 | command_prefix, 199 | data, 200 | re_compiled, 201 | allowed_report_keys, 202 | required_present_fields, 203 | setable_fields, 204 | ): 205 | has_changed = False 206 | is_error = True 207 | meta = {"present": True, "changed": []} 208 | 209 | report, error = dokku_module_report( 210 | command_prefix, data, re_compiled, allowed_report_keys 211 | ) 212 | if error: 213 | meta["error"] = error 214 | return (is_error, has_changed, meta) 215 | 216 | if not report["enabled"]: 217 | is_error = False 218 | meta["present"] = False 219 | return (is_error, has_changed, meta) 220 | 221 | data["enabled"] = "false" 222 | has_changed, changed_keys, error = dokku_module_set_blank( 223 | command_prefix, data, setable_fields 224 | ) 225 | if error: 226 | meta["error"] = error 227 | else: 228 | is_error = False 229 | meta["present"] = False 230 | 231 | if len(changed_keys) > 0: 232 | meta["changed"] = changed_keys 233 | 234 | return (is_error, has_changed, meta) 235 | 236 | 237 | def dokku_module_present( 238 | command_prefix, 239 | data, 240 | re_compiled, 241 | allowed_report_keys, 242 | required_present_fields, 243 | setable_fields, 244 | ): 245 | is_error = True 246 | has_changed = False 247 | meta = {"present": False, "changed": []} 248 | 249 | data["enabled"] = "true" 250 | error = dokku_module_require_fields(data, required_present_fields) 251 | if error: 252 | meta["error"] = error 253 | return (is_error, has_changed, meta) 254 | 255 | report, error = dokku_module_report( 256 | command_prefix, data, re_compiled, allowed_report_keys 257 | ) 258 | if error: 259 | meta["error"] = error 260 | return (is_error, has_changed, meta) 261 | 262 | has_changed, changed_keys, error = dokku_module_set_values( 263 | command_prefix, data, report, setable_fields 264 | ) 265 | if error: 266 | meta["error"] = error 267 | else: 268 | is_error = False 269 | meta["present"] = True 270 | 271 | if len(changed_keys) > 0: 272 | meta["changed"] = changed_keys 273 | 274 | return (is_error, has_changed, meta) 275 | 276 | 277 | def main(): 278 | fields = { 279 | "app": {"required": True, "type": "str"}, 280 | "image": {"required": False, "type": "str"}, 281 | "password": {"required": True, "type": "str", "no_log": True}, 282 | "server": {"required": False, "type": "str"}, 283 | "username": {"required": True, "type": "str"}, 284 | "state": { 285 | "required": False, 286 | "default": "present", 287 | "choices": ["absent", "present"], 288 | "type": "str", 289 | }, 290 | } 291 | choice_map = { 292 | "absent": dokku_module_absent, 293 | "present": dokku_module_present, 294 | } 295 | 296 | allowed_report_keys = ["enabled", "password", "image", "server", "username"] 297 | command_prefix = "registry" 298 | required_present_fields = ["password", "server", "username"] 299 | setable_fields = ["image", "password", "server", "username"] 300 | RE_PREFIX = re.compile("^registry-") 301 | 302 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 303 | is_error, has_changed, result = choice_map.get(module.params["state"])( 304 | command_prefix=command_prefix, 305 | data=module.params, 306 | re_compiled=RE_PREFIX, 307 | allowed_report_keys=allowed_report_keys, 308 | required_present_fields=required_present_fields, 309 | setable_fields=setable_fields, 310 | ) 311 | 312 | if is_error: 313 | module.fail_json(msg=result["error"], meta=result) 314 | module.exit_json(changed=has_changed, meta=result) 315 | 316 | 317 | if __name__ == "__main__": 318 | main() 319 | -------------------------------------------------------------------------------- /library/dokku_resource_limit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | import re 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_resource_limit 11 | short_description: Manage resource limits for a given dokku application 12 | options: 13 | app: 14 | description: 15 | - The name of the app 16 | required: True 17 | default: null 18 | aliases: [] 19 | resources: 20 | description: 21 | - The Resource type and quantity (required when state=present) 22 | required: False 23 | default: null 24 | aliases: [] 25 | process_type: 26 | description: 27 | - The process type selector 28 | required: False 29 | default: null 30 | alias: [] 31 | clear_before: 32 | description: 33 | - Clear all resource limits before applying 34 | required: False 35 | default: "False" 36 | choices: [ "True", "False" ] 37 | aliases: [] 38 | state: 39 | description: 40 | - The state of the resource limits 41 | required: False 42 | default: present 43 | choices: [ "present", "absent" ] 44 | aliases: [] 45 | author: Alexandre Pavanello e Silva 46 | requirements: [ ] 47 | 48 | """ 49 | 50 | EXAMPLES = """ 51 | - name: Limit CPU and memory of a dokku app 52 | dokku_resource_limit: 53 | app: hello-world 54 | resources: 55 | cpu: 100 56 | memory: 100 57 | 58 | - name: name: Limit resources per process type of a dokku app 59 | dokku_resource_limit: 60 | app: hello-world 61 | process_type: web 62 | resources: 63 | cpu: 100 64 | memory: 100 65 | 66 | - name: Clear limits before applying new limits 67 | dokku_resource_limit: 68 | app: hello-world 69 | state: present 70 | clear_before: True 71 | resources: 72 | cpu: 100 73 | memory: 100 74 | 75 | - name: Remove all resource limits 76 | dokku_resource_limit: 77 | app: hello-world 78 | state: absent 79 | """ 80 | 81 | 82 | def dokku_resource_clear(data): 83 | error = None 84 | process_type = "" 85 | if data["process_type"]: 86 | process_type = "--process-type {0}".format(data["process_type"]) 87 | command = "dokku resource:limit-clear {0} {1}".format(process_type, data["app"]) 88 | try: 89 | subprocess.check_call(command, shell=True) 90 | except subprocess.CalledProcessError as e: 91 | error = str(e) 92 | return error 93 | 94 | 95 | def dokku_resource_limit_report(data): 96 | 97 | process_type = "" 98 | if data["process_type"]: 99 | process_type = "--process-type {0}".format(data["process_type"]) 100 | command = "dokku --quiet resource:limit {0} {1}".format(process_type, data["app"]) 101 | 102 | output, error = subprocess_check_output(command) 103 | if error is not None: 104 | return output, error 105 | output = [re.sub(r"\s+", "", line) for line in output] 106 | 107 | report = {} 108 | 109 | for line in output: 110 | if ":" not in line: 111 | continue 112 | key, value = line.split(":", 1) 113 | report[key] = value 114 | 115 | return report, error 116 | 117 | 118 | def dokku_resource_limit_present(data): 119 | is_error = True 120 | has_changed = False 121 | meta = {"present": False} 122 | 123 | if "resources" not in data: 124 | meta["error"] = "missing required arguments: resources" 125 | return (is_error, has_changed, meta) 126 | 127 | report, error = dokku_resource_limit_report(data) 128 | meta["debug"] = report.keys() 129 | if error: 130 | meta["error"] = error 131 | return (is_error, has_changed, meta) 132 | 133 | for k, v in data["resources"].items(): 134 | if k not in report.keys(): 135 | is_error = True 136 | has_changed = False 137 | meta["error"] = "Unknown resource {0}, choose one of: {1}".format( 138 | k, list(report.keys()) 139 | ) 140 | return (is_error, has_changed, meta) 141 | if report[k] != str(v): 142 | has_changed = True 143 | 144 | if data["clear_before"] is True: 145 | 146 | error = dokku_resource_clear(data) 147 | if error: 148 | meta["error"] = error 149 | is_error = True 150 | has_changed = False 151 | return (is_error, has_changed, meta) 152 | has_changed = True 153 | 154 | if not has_changed: 155 | meta["present"] = True 156 | is_error = False 157 | return (is_error, has_changed, meta) 158 | 159 | values = [] 160 | for key, value in data["resources"].items(): 161 | values.append("--{0} {1}".format(key, value)) 162 | 163 | process_type = "" 164 | if data["process_type"]: 165 | process_type = "--process-type {0}".format(data["process_type"]) 166 | 167 | command = "dokku resource:limit {0} {1} {2}".format( 168 | " ".join(values), process_type, data["app"] 169 | ) 170 | try: 171 | subprocess.check_call(command, shell=True) 172 | is_error = False 173 | has_changed = True 174 | meta["present"] = True 175 | except subprocess.CalledProcessError as e: 176 | meta["error"] = str(e) 177 | return (is_error, has_changed, meta) 178 | 179 | 180 | def dokku_resource_limit_absent(data): 181 | is_error = True 182 | has_changed = False 183 | meta = {"present": True} 184 | 185 | error = dokku_resource_clear(data) 186 | if error: 187 | meta["error"] = error 188 | is_error = True 189 | has_changed = False 190 | return (is_error, has_changed, meta) 191 | 192 | is_error = False 193 | has_changed = True 194 | meta = {"present": False} 195 | 196 | return (is_error, has_changed, meta) 197 | 198 | 199 | def main(): 200 | fields = { 201 | "app": {"required": True, "type": "str"}, 202 | "process_type": {"required": False, "type": "str"}, 203 | "resources": {"required": False, "type": "dict"}, 204 | "clear_before": {"required": False, "type": "bool"}, 205 | "state": { 206 | "required": False, 207 | "default": "present", 208 | "choices": ["present", "absent"], 209 | "type": "str", 210 | }, 211 | } 212 | choice_map = { 213 | "present": dokku_resource_limit_present, 214 | "absent": dokku_resource_limit_absent, 215 | } 216 | 217 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 218 | is_error, has_changed, result = choice_map.get(module.params["state"])( 219 | module.params 220 | ) 221 | 222 | if is_error: 223 | module.fail_json(msg=result["error"], meta=result) 224 | module.exit_json(changed=has_changed, meta=result) 225 | 226 | 227 | if __name__ == "__main__": 228 | main() 229 | -------------------------------------------------------------------------------- /library/dokku_resource_reserve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import subprocess 6 | import re 7 | 8 | DOCUMENTATION = """ 9 | --- 10 | module: dokku_resource_reserve 11 | short_description: Manage resource reservations for a given dokku application 12 | options: 13 | app: 14 | description: 15 | - The name of the app 16 | required: True 17 | default: null 18 | aliases: [] 19 | resources: 20 | description: 21 | - The Resource type and quantity (required when state=present) 22 | required: False 23 | default: null 24 | aliases: [] 25 | process_type: 26 | description: 27 | - The process type selector 28 | required: False 29 | default: null 30 | alias: [] 31 | clear_before: 32 | description: 33 | - Clear all reserves before apply 34 | required: False 35 | default: "False" 36 | choices: [ "True", "False" ] 37 | aliases: [] 38 | state: 39 | description: 40 | - The state of the resource reservations 41 | required: False 42 | default: present 43 | choices: [ "present", "absent" ] 44 | aliases: [] 45 | author: Alexandre Pavanello e Silva 46 | requirements: [ ] 47 | 48 | """ 49 | 50 | EXAMPLES = """ 51 | - name: Reserve CPU and memory for a dokku app 52 | dokku_resource_reserve: 53 | app: hello-world 54 | resources: 55 | cpu: 100 56 | memory: 100 57 | 58 | - name: Create a reservation per process type of a dokku app 59 | dokku_resource_reserve: 60 | app: hello-world 61 | process_type: web 62 | resources: 63 | cpu: 100 64 | memory: 100 65 | 66 | - name: Clear all reservations before applying 67 | dokku_resource_reserve: 68 | app: hello-world 69 | state: present 70 | clear_before: True 71 | resources: 72 | cpu: 100 73 | memory: 100 74 | 75 | - name: Remove all resource reservations 76 | dokku_resource_reserve: 77 | app: hello-world 78 | state: absent 79 | """ 80 | 81 | 82 | def dokku_resource_clear(data): 83 | error = None 84 | process_type = "" 85 | if data["process_type"]: 86 | process_type = "--process-type {0}".format(data["process_type"]) 87 | command = "dokku resource:reserve-clear {0} {1}".format(process_type, data["app"]) 88 | try: 89 | subprocess.check_call(command, shell=True) 90 | except subprocess.CalledProcessError as e: 91 | error = str(e) 92 | return error 93 | 94 | 95 | def dokku_resource_reserve_report(data): 96 | 97 | process_type = "" 98 | if data["process_type"]: 99 | process_type = "--process-type {0}".format(data["process_type"]) 100 | command = "dokku --quiet resource:reserve {0} {1}".format(process_type, data["app"]) 101 | 102 | output, error = subprocess_check_output(command) 103 | if error is not None: 104 | return output, error 105 | output = [re.sub(r"\s+", "", line) for line in output] 106 | 107 | report = {} 108 | 109 | for line in output: 110 | if ":" not in line: 111 | continue 112 | key, value = line.split(":", 1) 113 | report[key] = value 114 | 115 | return report, error 116 | 117 | 118 | def dokku_resource_reserve_present(data): 119 | is_error = True 120 | has_changed = False 121 | meta = {"present": False} 122 | 123 | if "resources" not in data: 124 | meta["error"] = "missing required arguments: resources" 125 | return (is_error, has_changed, meta) 126 | 127 | report, error = dokku_resource_reserve_report(data) 128 | if error: 129 | meta["error"] = error 130 | return (is_error, has_changed, meta) 131 | 132 | for k, v in data["resources"].items(): 133 | if k not in report.keys(): 134 | is_error = True 135 | has_changed = False 136 | meta["error"] = "Unknown resource {0}, choose one of: {1}".format( 137 | k, list(report.keys()) 138 | ) 139 | return (is_error, has_changed, meta) 140 | if report[k] != str(v): 141 | has_changed = True 142 | 143 | if data["clear_before"] is True: 144 | 145 | error = dokku_resource_clear(data) 146 | if error: 147 | meta["error"] = error 148 | is_error = True 149 | has_changed = False 150 | return (is_error, has_changed, meta) 151 | has_changed = True 152 | 153 | if not has_changed: 154 | meta["present"] = True 155 | is_error = False 156 | return (is_error, has_changed, meta) 157 | 158 | values = [] 159 | for key, value in data["resources"].items(): 160 | values.append("--{0} {1}".format(key, value)) 161 | 162 | process_type = "" 163 | if data["process_type"]: 164 | process_type = "--process-type {0}".format(data["process_type"]) 165 | 166 | command = "dokku resource:reserve {0} {1} {2}".format( 167 | " ".join(values), process_type, data["app"] 168 | ) 169 | try: 170 | subprocess.check_call(command, shell=True) 171 | is_error = False 172 | has_changed = True 173 | meta["present"] = True 174 | except subprocess.CalledProcessError as e: 175 | meta["error"] = str(e) 176 | return (is_error, has_changed, meta) 177 | 178 | 179 | def dokku_resource_reserve_absent(data): 180 | is_error = True 181 | has_changed = False 182 | meta = {"present": True} 183 | 184 | error = dokku_resource_clear(data) 185 | if error: 186 | meta["error"] = error 187 | is_error = True 188 | has_changed = False 189 | return (is_error, has_changed, meta) 190 | 191 | is_error = False 192 | has_changed = True 193 | meta = {"present": False} 194 | 195 | return (is_error, has_changed, meta) 196 | 197 | 198 | def main(): 199 | fields = { 200 | "app": {"required": True, "type": "str"}, 201 | "process_type": {"required": False, "type": "str"}, 202 | "resources": {"required": False, "type": "dict"}, 203 | "clear_before": {"required": False, "type": "bool"}, 204 | "state": { 205 | "required": False, 206 | "default": "present", 207 | "choices": ["present", "absent"], 208 | "type": "str", 209 | }, 210 | } 211 | choice_map = { 212 | "present": dokku_resource_reserve_present, 213 | "absent": dokku_resource_reserve_absent, 214 | } 215 | 216 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 217 | is_error, has_changed, result = choice_map.get(module.params["state"])( 218 | module.params 219 | ) 220 | 221 | if is_error: 222 | module.fail_json(msg=result["error"], meta=result) 223 | module.exit_json(changed=has_changed, meta=result) 224 | 225 | 226 | if __name__ == "__main__": 227 | main() 228 | -------------------------------------------------------------------------------- /library/dokku_service_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | import subprocess 5 | 6 | DOCUMENTATION = """ 7 | --- 8 | module: dokku_service_create 9 | short_description: Creates a given service 10 | options: 11 | name: 12 | description: 13 | - The name of the service 14 | required: True 15 | default: null 16 | aliases: [] 17 | service: 18 | description: 19 | - The type of service to create 20 | required: True 21 | default: null 22 | aliases: [] 23 | author: Jose Diaz-Gonzalez 24 | requirements: [ ] 25 | """ 26 | 27 | EXAMPLES = """ 28 | - name: redis:create default 29 | dokku_service_create: 30 | name: default 31 | service: redis 32 | 33 | - name: postgres:create default 34 | dokku_service_create: 35 | name: default 36 | service: postgres 37 | 38 | - name: postgres:create default with custom image 39 | environment: 40 | POSTGRES_IMAGE: postgis/postgis 41 | POSTGRES_IMAGE_VERSION: 13-master 42 | dokku_service_create: 43 | name: default 44 | service: postgres 45 | 46 | """ 47 | 48 | 49 | def dokku_service_exists(service, name): 50 | exists = False 51 | error = None 52 | command = "dokku --quiet {0}:exists {1}".format(service, name) 53 | try: 54 | subprocess.check_call(command, shell=True) 55 | exists = True 56 | except subprocess.CalledProcessError as e: 57 | error = str(e) 58 | return exists, error 59 | 60 | 61 | def dokku_service_create(data): 62 | is_error = True 63 | has_changed = False 64 | meta = {"present": False} 65 | 66 | exists, error = dokku_service_exists(data["service"], data["name"]) 67 | if exists: 68 | is_error = False 69 | meta["present"] = True 70 | return (is_error, has_changed, meta) 71 | 72 | command = "dokku {0}:create {1}".format(data["service"], data["name"]) 73 | try: 74 | subprocess.check_call(command, shell=True) 75 | is_error = False 76 | has_changed = True 77 | meta["present"] = True 78 | except subprocess.CalledProcessError as e: 79 | meta["error"] = str(e) 80 | 81 | return (is_error, has_changed, meta) 82 | 83 | 84 | def main(): 85 | fields = { 86 | "service": {"required": True, "type": "str"}, 87 | "name": {"required": True, "type": "str"}, 88 | } 89 | 90 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 91 | is_error, has_changed, result = dokku_service_create(module.params) 92 | 93 | if is_error: 94 | module.fail_json(msg=result["error"], meta=result) 95 | module.exit_json(changed=has_changed, meta=result) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /library/dokku_service_link.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_app import dokku_apps_exists 5 | import subprocess 6 | 7 | DOCUMENTATION = """ 8 | --- 9 | module: dokku_service_link 10 | short_description: Links and unlinks a given service to an application 11 | options: 12 | app: 13 | description: 14 | - The name of the app 15 | required: True 16 | default: null 17 | aliases: [] 18 | name: 19 | description: 20 | - The name of the service 21 | required: True 22 | default: null 23 | aliases: [] 24 | service: 25 | description: 26 | - The type of service to link 27 | required: True 28 | default: null 29 | aliases: [] 30 | state: 31 | description: 32 | - The state of the service link 33 | required: False 34 | default: present 35 | choices: [ "present", "absent" ] 36 | aliases: [] 37 | author: Jose Diaz-Gonzalez 38 | requirements: [ ] 39 | """ 40 | 41 | EXAMPLES = """ 42 | - name: redis:link default hello-world 43 | dokku_service_link: 44 | app: hello-world 45 | name: default 46 | service: redis 47 | 48 | - name: postgres:link default hello-world 49 | dokku_service_link: 50 | app: hello-world 51 | name: default 52 | service: postgres 53 | 54 | - name: redis:unlink default hello-world 55 | dokku_service_link: 56 | app: hello-world 57 | name: default 58 | service: redis 59 | state: absent 60 | """ 61 | 62 | 63 | def dokku_service_exists(service, name): 64 | exists = False 65 | error = None 66 | command = "dokku --quiet {0}:exists {1}".format(service, name) 67 | try: 68 | subprocess.check_call(command, shell=True) 69 | exists = True 70 | except subprocess.CalledProcessError as e: 71 | error = str(e) 72 | return exists, error 73 | 74 | 75 | def dokku_service_linked(service, name, app): 76 | linked = False 77 | error = None 78 | command = "dokku --quiet {0}:linked {1} {2}".format(service, name, app) 79 | try: 80 | subprocess.check_call(command, shell=True) 81 | linked = True 82 | except subprocess.CalledProcessError as e: 83 | error = str(e) 84 | return linked, error 85 | 86 | 87 | def dokku_service_link_absent(data): 88 | is_error = True 89 | has_changed = False 90 | meta = {"present": False} 91 | 92 | exists, error = dokku_service_exists(data["service"], data["name"]) 93 | if not exists: 94 | meta["error"] = error 95 | return (is_error, has_changed, meta) 96 | 97 | app_exists, error = dokku_apps_exists(data["app"]) 98 | if not app_exists: 99 | meta["error"] = error 100 | return (is_error, has_changed, meta) 101 | 102 | linked, error = dokku_service_linked(data["service"], data["name"], data["app"]) 103 | if not linked: 104 | is_error = False 105 | return (is_error, has_changed, meta) 106 | 107 | command = "dokku --quiet {0}:unlink {1} {2}".format( 108 | data["service"], data["name"], data["app"] 109 | ) 110 | try: 111 | subprocess.check_call(command, shell=True) 112 | is_error = False 113 | has_changed = True 114 | meta["present"] = True 115 | except subprocess.CalledProcessError as e: 116 | meta["error"] = str(e) 117 | 118 | return (is_error, has_changed, meta) 119 | 120 | 121 | def dokku_service_link_present(data): 122 | is_error = True 123 | has_changed = False 124 | meta = {"present": False} 125 | 126 | exists, error = dokku_service_exists(data["service"], data["name"]) 127 | if not exists: 128 | meta["error"] = error 129 | return (is_error, has_changed, meta) 130 | 131 | app_exists, error = dokku_apps_exists(data["app"]) 132 | if not app_exists: 133 | meta["error"] = error 134 | return (is_error, has_changed, meta) 135 | 136 | linked, error = dokku_service_linked(data["service"], data["name"], data["app"]) 137 | if linked: 138 | is_error = False 139 | return (is_error, has_changed, meta) 140 | 141 | command = "dokku --quiet {0}:link {1} {2}".format( 142 | data["service"], data["name"], data["app"] 143 | ) 144 | try: 145 | subprocess.check_call(command, shell=True) 146 | is_error = False 147 | has_changed = True 148 | meta["present"] = True 149 | except subprocess.CalledProcessError as e: 150 | meta["error"] = str(e) 151 | 152 | return (is_error, has_changed, meta) 153 | 154 | 155 | def main(): 156 | fields = { 157 | "app": {"required": True, "type": "str"}, 158 | "name": {"required": True, "type": "str"}, 159 | "service": {"required": True, "type": "str"}, 160 | "state": { 161 | "required": False, 162 | "default": "present", 163 | "choices": ["present", "absent"], 164 | "type": "str", 165 | }, 166 | } 167 | choice_map = { 168 | "present": dokku_service_link_present, 169 | "absent": dokku_service_link_absent, 170 | } 171 | 172 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 173 | is_error, has_changed, result = choice_map.get(module.params["state"])( 174 | module.params 175 | ) 176 | 177 | if is_error: 178 | module.fail_json(msg=result["error"], meta=result) 179 | module.exit_json(changed=has_changed, meta=result) 180 | 181 | 182 | if __name__ == "__main__": 183 | main() 184 | -------------------------------------------------------------------------------- /library/dokku_storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from ansible.module_utils.basic import AnsibleModule 4 | from ansible.module_utils.dokku_utils import subprocess_check_output 5 | import os 6 | import pwd 7 | import subprocess 8 | 9 | DOCUMENTATION = """ 10 | --- 11 | module: dokku_storage 12 | short_description: Manage storage for dokku applications 13 | options: 14 | app: 15 | description: 16 | - The name of the app 17 | required: False 18 | default: null 19 | aliases: [] 20 | create_host_dir: 21 | description: 22 | - Whether to create the host directory or not 23 | required: False 24 | default: False 25 | aliases: [] 26 | group: 27 | description: 28 | - A group or gid that should own the created folder 29 | required: False 30 | default: 32767 31 | aliases: [] 32 | mounts: 33 | description: 34 | - | 35 | A list of mounts to create, colon (:) delimited, in the format: `host_dir:container_dir` 36 | required: False 37 | default: [] 38 | aliases: [] 39 | user: 40 | description: 41 | - A user or uid that should own the created folder 42 | required: False 43 | default: 32767 44 | aliases: [] 45 | state: 46 | description: 47 | - The state of the service link 48 | required: False 49 | default: present 50 | choices: [ "present", "absent" ] 51 | aliases: [] 52 | author: Jose Diaz-Gonzalez 53 | requirements: [ ] 54 | """ 55 | 56 | EXAMPLES = """ 57 | - name: mount a path 58 | dokku_storage: 59 | app: hello-world 60 | mounts: 61 | - /var/lib/dokku/data/storage/hello-world:/data 62 | 63 | - name: mount a path and create the host_dir directory 64 | dokku_storage: 65 | app: hello-world 66 | mounts: 67 | - /var/lib/dokku/data/storage/hello-world:/data 68 | create_host_dir: true 69 | 70 | - name: unmount a path 71 | dokku_storage: 72 | app: hello-world 73 | mounts: 74 | - /var/lib/dokku/data/storage/hello-world:/data 75 | state: absent 76 | 77 | - name: unmount a path and destroy the host_dir directory (and contents) 78 | dokku_storage: 79 | app: hello-world 80 | mounts: 81 | - /var/lib/dokku/data/storage/hello-world:/data 82 | destroy_host_dir: true 83 | state: absent 84 | """ 85 | 86 | 87 | def get_gid(group): 88 | gid = group 89 | try: 90 | gid = int(group) 91 | except ValueError: 92 | gid = pwd.getpwnam(group).pw_gid 93 | return gid 94 | 95 | 96 | def get_state(b_path): 97 | """Find out current state""" 98 | 99 | if os.path.lexists(b_path): 100 | if os.path.islink(b_path): 101 | return "link" 102 | elif os.path.isdir(b_path): 103 | return "directory" 104 | elif os.stat(b_path).st_nlink > 1: 105 | return "hard" 106 | # could be many other things, but defaulting to file 107 | return "file" 108 | 109 | return "absent" 110 | 111 | 112 | def get_uid(user): 113 | uid = user 114 | try: 115 | uid = int(user) 116 | except ValueError: 117 | uid = pwd.getpwnam(user).pw_uid 118 | return uid 119 | 120 | 121 | def dokku_storage_list(data): 122 | command = "dokku --quiet storage:list {0}".format(data["app"]) 123 | return subprocess_check_output(command) 124 | 125 | 126 | def dokku_storage_mount_exists(data): 127 | state = get_state("/home/dokku/{0}".format(data["app"])) 128 | 129 | if state not in ["directory", "file"]: 130 | error = "app {0} does not exist".format(data["app"]) 131 | return False, error 132 | 133 | output, error = dokku_storage_list(data) 134 | if error: 135 | return False, error 136 | 137 | mount = "{0}:{1}".format(data["host_dir"], data["container_dir"]) 138 | if mount in output: 139 | return True, None 140 | 141 | return False, None 142 | 143 | 144 | def dokku_storage_create_dir(data, is_error, has_changed, meta): 145 | if not data["create_host_dir"]: 146 | return (is_error, has_changed, meta) 147 | 148 | old_state = get_state(data["host_dir"]) 149 | if old_state not in ["absent", "directory"]: 150 | is_error = True 151 | meta["error"] = "host directory is {0}".format(old_state) 152 | return (is_error, has_changed, meta) 153 | 154 | try: 155 | if old_state == "absent": 156 | os.makedirs(data["host_dir"], 0o777) 157 | os.chmod(data["host_dir"], 0o777) 158 | uid = get_uid(data["user"]) 159 | gid = get_gid(data["group"]) 160 | os.chown(data["host_dir"], uid, gid) 161 | except OSError as exc: 162 | is_error = True 163 | meta["error"] = str(exc) 164 | return (is_error, has_changed, meta) 165 | 166 | if old_state != get_state(data["host_dir"]): 167 | has_changed = True 168 | 169 | return (is_error, has_changed, meta) 170 | 171 | 172 | def dokku_storage_destroy_dir(data, is_error, has_changed, meta): 173 | if not data["destroy_host_dir"]: 174 | return (is_error, has_changed, meta) 175 | 176 | old_state = get_state(data["host_dir"]) 177 | if old_state not in ["absent", "directory"]: 178 | is_error = True 179 | meta["error"] = "host directory is {0}".format(old_state) 180 | return (is_error, has_changed, meta) 181 | 182 | try: 183 | if old_state == "directory": 184 | os.rmdir(data["host_dir"]) 185 | except OSError as exc: 186 | is_error = True 187 | meta["error"] = str(exc) 188 | return (is_error, has_changed, meta) 189 | 190 | if old_state != get_state(data["host_dir"]): 191 | has_changed = True 192 | 193 | return (is_error, has_changed, meta) 194 | 195 | 196 | def dokku_storage_absent(data): 197 | is_error = False 198 | has_changed = False 199 | meta = {"present": False} 200 | 201 | mounts = data.get("mounts", []) or [] 202 | if len(mounts) == 0: 203 | is_error = True 204 | meta["error"] = "missing required arguments: mounts" 205 | return (is_error, has_changed, meta) 206 | 207 | for mount in mounts: 208 | data["host_dir"], data["container_dir"] = mount.split(":", 1) 209 | is_error, has_changed, meta = dokku_storage_destroy_dir( 210 | data, is_error, has_changed, meta 211 | ) 212 | 213 | if is_error: 214 | return (is_error, has_changed, meta) 215 | 216 | exists, error = dokku_storage_mount_exists(data) 217 | if error: 218 | is_error = True 219 | meta["error"] = error 220 | return (is_error, has_changed, meta) 221 | elif not exists: 222 | is_error = False 223 | continue 224 | 225 | command = "dokku --quiet storage:unmount {0} {1}:{2}".format( 226 | data["app"], data["host_dir"], data["container_dir"] 227 | ) 228 | try: 229 | subprocess.check_call(command, shell=True) 230 | is_error = False 231 | has_changed = True 232 | except subprocess.CalledProcessError as e: 233 | is_error = True 234 | meta["error"] = str(e) 235 | meta["present"] = True 236 | 237 | if is_error: 238 | return (is_error, has_changed, meta) 239 | return (is_error, has_changed, meta) 240 | 241 | 242 | def dokku_storage_present(data): 243 | is_error = False 244 | has_changed = False 245 | meta = {"present": False} 246 | 247 | mounts = data.get("mounts", []) or [] 248 | if len(mounts) == 0: 249 | is_error = True 250 | meta["error"] = "missing required arguments: mounts" 251 | return (is_error, has_changed, meta) 252 | 253 | for mount in mounts: 254 | data["host_dir"], data["container_dir"] = mount.split(":", 1) 255 | is_error, has_changed, meta = dokku_storage_create_dir( 256 | data, is_error, has_changed, meta 257 | ) 258 | 259 | if is_error: 260 | return (is_error, has_changed, meta) 261 | 262 | exists, error = dokku_storage_mount_exists(data) 263 | if error: 264 | is_error = True 265 | meta["error"] = error 266 | return (is_error, has_changed, meta) 267 | elif exists: 268 | is_error = False 269 | continue 270 | 271 | command = "dokku --quiet storage:mount {0} {1}:{2}".format( 272 | data["app"], data["host_dir"], data["container_dir"] 273 | ) 274 | try: 275 | subprocess.check_call(command, shell=True) 276 | is_error = False 277 | has_changed = True 278 | meta["present"] = True 279 | except subprocess.CalledProcessError as e: 280 | is_error = True 281 | meta["error"] = str(e) 282 | 283 | if is_error: 284 | return (is_error, has_changed, meta) 285 | return (is_error, has_changed, meta) 286 | 287 | 288 | def main(): 289 | fields = { 290 | "app": {"required": True, "type": "str"}, 291 | "state": { 292 | "required": False, 293 | "default": "present", 294 | "choices": ["present", "absent"], 295 | "type": "str", 296 | }, 297 | "mounts": {"required": False, "type": "list", "default": []}, 298 | "create_host_dir": {"required": False, "default": False, "type": "bool"}, 299 | "destroy_host_dir": {"required": False, "default": False, "type": "bool"}, 300 | "user": {"required": False, "default": "32767", "type": "str"}, 301 | "group": {"required": False, "default": "32767", "type": "str"}, 302 | } 303 | choice_map = { 304 | "present": dokku_storage_present, 305 | "absent": dokku_storage_absent, 306 | } 307 | 308 | module = AnsibleModule(argument_spec=fields, supports_check_mode=False) 309 | is_error, has_changed, result = choice_map.get(module.params["state"])( 310 | module.params 311 | ) 312 | 313 | if is_error: 314 | module.fail_json(msg=result["error"], meta=result) 315 | module.exit_json(changed=has_changed, meta=result) 316 | 317 | 318 | if __name__ == "__main__": 319 | main() 320 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | role_name: ansible_dokku 3 | namespace: dokku_bot 4 | author: "Jose Diaz-Gonzalez" 5 | description: | 6 | This Ansible role helps install Dokku on Debian/Ubuntu variants. Apart 7 | from installing Dokku, it also provides various modules that can be 8 | used to interface with dokku from your own Ansible playbooks. 9 | license: MIT License 10 | min_ansible_version: '2.2' 11 | # See e.g. https://galaxy.ansible.com/api/v1/platforms/?name=Ubuntu 12 | platforms: 13 | - name: Ubuntu 14 | versions: 15 | - noble 16 | - jammy 17 | - focal 18 | - name: Debian 19 | versions: 20 | - bookworm 21 | - bullseye 22 | galaxy_tags: 23 | - networking 24 | - packaging 25 | - system 26 | dependencies: 27 | - role: geerlingguy.docker 28 | - role: nginxinc.nginx 29 | -------------------------------------------------------------------------------- /module_utils/dokku_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """Shared functions for managing dokku apps""" 4 | 5 | import subprocess 6 | 7 | 8 | def dokku_apps_exists(app): 9 | exists = False 10 | error = None 11 | command = "dokku --quiet apps:exists {0}".format(app) 12 | try: 13 | subprocess.check_call(command, shell=True) 14 | exists = True 15 | except subprocess.CalledProcessError as e: 16 | # we do not distinguish non-zero exit codes 17 | error = str(e) 18 | return exists, error 19 | 20 | 21 | def dokku_app_ensure_present(data): 22 | """Create app if it does not exist.""" 23 | is_error = True 24 | has_changed = False 25 | meta = {"present": False} 26 | 27 | exists, _error = dokku_apps_exists(data["app"]) 28 | if exists: 29 | is_error = False 30 | meta["present"] = True 31 | return (is_error, has_changed, meta) 32 | 33 | command = "dokku apps:create {0}".format(data["app"]) 34 | try: 35 | subprocess.check_call(command, shell=True) 36 | is_error = False 37 | has_changed = True 38 | meta["present"] = True 39 | except subprocess.CalledProcessError as e: 40 | meta["error"] = str(e.output) 41 | 42 | return (is_error, has_changed, meta) 43 | 44 | 45 | def dokku_app_ensure_absent(data=None): 46 | """Remove app if it exists.""" 47 | is_error = True 48 | has_changed = False 49 | meta = {"present": True} 50 | 51 | exists, _error = dokku_apps_exists(data["app"]) 52 | if not exists: 53 | is_error = False 54 | meta["present"] = False 55 | return (is_error, has_changed, meta) 56 | 57 | command = "dokku --force apps:destroy {0}".format(data["app"]) 58 | try: 59 | subprocess.check_call(command, shell=True) 60 | is_error = False 61 | has_changed = True 62 | meta["present"] = False 63 | except subprocess.CalledProcessError as e: 64 | meta["error"] = str(e) 65 | 66 | return (is_error, has_changed, meta) 67 | -------------------------------------------------------------------------------- /module_utils/dokku_git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """Utility functions for dokku git related plugins""" 4 | import subprocess 5 | 6 | 7 | def dokku_git_sha(app): 8 | """Get SHA of current app repository. 9 | 10 | Returns `None` if app does not exist. 11 | """ 12 | command_git_report = "dokku git:report {app} --git-sha".format(app=app) 13 | try: 14 | sha = subprocess.check_output( 15 | command_git_report, stderr=subprocess.STDOUT, shell=True 16 | ) 17 | except subprocess.CalledProcessError: 18 | sha = None 19 | 20 | return sha 21 | -------------------------------------------------------------------------------- /module_utils/dokku_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """Utility functions for the dokku library""" 4 | import subprocess 5 | import re 6 | from typing import Tuple 7 | 8 | 9 | def force_list(var): 10 | if isinstance(var, list): 11 | return var 12 | return list(var) 13 | 14 | 15 | # Add an option to redirect stderr to stdout, because some dokku commands output to stderr 16 | def subprocess_check_output(command, split="\n", redirect_stderr=False): 17 | error = None 18 | output = [] 19 | try: 20 | if redirect_stderr: 21 | output = subprocess.check_output( 22 | command, shell=True, stderr=subprocess.STDOUT 23 | ) 24 | else: 25 | output = subprocess.check_output(command, shell=True) 26 | if isinstance(output, bytes): 27 | output = output.decode("utf-8") 28 | output = str(output).rstrip("\n") 29 | if split is None: 30 | return output, error 31 | output = output.split(split) 32 | output = force_list(filter(None, output)) 33 | output = [o.strip() for o in output] 34 | except subprocess.CalledProcessError as e: 35 | error = str(e) 36 | return output, error 37 | 38 | 39 | # Get the version of dokku installed 40 | # Example: (0, 31, 4) 41 | def get_dokku_version() -> Tuple[int, int, int]: 42 | command = "dokku --version" 43 | output = subprocess.run(command, shell=True, stdout=subprocess.PIPE, text=True) 44 | pattern = r"\d+\.\d+\.\d+" 45 | match = re.search(pattern, output.stdout) 46 | if match is None: 47 | raise ValueError("Could not find Dokku version in command output.") 48 | version_data = match.group().split(".") 49 | version = tuple(map(int, version_data)) 50 | return version 51 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | - name: Converge 2 | hosts: all 3 | become: true 4 | 5 | pre_tasks: 6 | - name: Update apt cache. 7 | apt: update_cache=yes cache_valid_time=600 8 | when: ansible_os_family == 'Debian' 9 | 10 | roles: 11 | - role: dokku_bot.ansible_dokku # noqa 12 | vars: 13 | dokku_plugins: 14 | - name: global-cert 15 | url: https://github.com/josegonzalez/dokku-global-cert 16 | - name: http-auth 17 | url: https://github.com/dokku/dokku-http-auth 18 | - name: acl 19 | url: https://github.com/dokku-community/dokku-acl 20 | dokku_hostname: test.domain 21 | dokku_users: 22 | - name: Giuseppe Verdi 23 | username: gverdi 24 | ssh_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC+G029J1r06FCB0rvavqdHcFZickiGcSH5a4un/DT5Gt4xVThM66WhkoEBY/F5yXTH49r5D2ky5G1aACPQeewfkeseV8A0y07fmLPyPjtKz/bOX7904GqFGNV3q+SBHkiYMk0JhUbOJM1C6Iyq03c4rmU4EVTI2hX/uZg3R65ezI/H94BHJyt/U/Nrip1FFhV9EoltAhLNhvMO8cxET+xqJUorjIiHA0JIUZQ02GTH2uwH2Qycv93vA0G7TJPTwHO1WJFpKr+2SWVW4auvA8zBx6epWuRxQO45nD3cQwpjCOt/YbVxt7Q2PJDVy+1OB3p1Q/NkuvDR5ht056quxwOVLYYSllSmEbDgml+5LOIsEqRw+OAbv1Y8FQq+Gr+J53RTmqkPQZLgyhYLWJ/sQKB2rMOntIVfpEzPI6ikFIG63BxwxPnRb9zr1VKOxWMKWEqtpE0YLy3JoDn8oi0oSDfCITqsf9pa5NDnjWPuxyKz6FwHXvrCmiG4tsRyLD7AOp8= verdi@doremi" 25 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | dependency: 2 | name: galaxy 3 | options: 4 | role-file: ansible-role-requirements.yml 5 | driver: 6 | name: docker 7 | platforms: 8 | - name: instance 9 | image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2004}-ansible:latest" 10 | command: ${MOLECULE_DOCKER_COMMAND:-""} 11 | volumes: 12 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 13 | - /var/lib/containerd 14 | cgroupns_mode: host 15 | privileged: true 16 | pre_build_image: true 17 | provisioner: 18 | name: ansible 19 | playbooks: 20 | converge: converge.yml 21 | -------------------------------------------------------------------------------- /molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | - name: Verify 2 | hosts: all 3 | become: true 4 | 5 | tasks: 6 | - name: Run role without running any tasks 7 | include_role: 8 | name: dokku_bot.ansible_dokku 9 | tasks_from: init.yml 10 | 11 | # Testing dokku_global_cert 12 | - name: Check that dokku_global_cert module can parse output 13 | dokku_global_cert: 14 | state: absent 15 | 16 | # Testing dokku_hostname 17 | - name: Get dokku_hostname # noqa 301 18 | command: dokku domains:report --global 19 | register: dokku_domains 20 | 21 | - name: Check that dokku_hostname is set correctly 22 | assert: 23 | that: 24 | - "'test.domain' in dokku_domains.stdout" 25 | msg: | 26 | hostname 'test.domain' not found in output of 'dokku domains': 27 | {{ dokku_domains.stdout }} 28 | 29 | # check apt sources configuration 30 | # Note: cannot use apt module for this https://stackoverflow.com/questions/51410696 31 | - name: Run apt update # noqa 303 301 32 | command: apt-get update 33 | register: apt_update 34 | 35 | - name: Check that apt update output does not contain warnings 36 | assert: 37 | that: 38 | - "'W:' not in apt_update.stderr" 39 | msg: | 40 | Found warnings in output of `apt update`: 41 | {{ apt_update.stderr }} 42 | 43 | # Testing dokku_app 44 | - name: create example-app 45 | dokku_app: 46 | app: example-app 47 | 48 | # Testing dokku_acl_app 49 | - name: Let gverdi manage example-app 50 | dokku_acl_app: 51 | app: example-app 52 | users: 53 | - gverdi 54 | 55 | - name: List ACLs for example-app # noqa 301 56 | command: dokku acl:list example-app 57 | register: dokku_acl_list 58 | 59 | - name: Check that gverdi is in list of ACLs for example-app 60 | assert: 61 | that: 62 | # Note: As of Nov 2021, `dokku acl:list` writes to stderr 63 | # Feel free to remove this the stderr case when the following issue is fixed 64 | # https://github.com/dokku-community/dokku-acl/issues/34 65 | - ("'gverdi' in dokku_acl_list.stdout") or ("'gverdi' in dokku_acl_list.stderr") 66 | msg: |- 67 | 'gverdi' not found in output of 'dokku acl:list example-app': 68 | {{ dokku_acl_list.stdout }} 69 | 70 | - name: Remove permission for gverdi to manage example-app 71 | dokku_acl_app: 72 | app: example-app 73 | users: 74 | - gverdi 75 | state: absent 76 | 77 | - name: List ACLs for example-app # noqa 301 78 | command: dokku acl:list example-app 79 | register: dokku_acl_list 80 | 81 | - name: Check that gverdi is no longer in list of ACLs for example-app 82 | assert: 83 | that: 84 | # Note: As of Nov 2021, `dokku acl:list` writes to stderr 85 | # Feel free to remove this the stderr case when the following issue is fixed 86 | # https://github.com/dokku-community/dokku-acl/issues/34 87 | - ("'gverdi' not in dokku_acl_list.stdout") and ("'gverdi' not in dokku_acl_list.stderr") 88 | msg: |- 89 | 'gverdi' found in output of 'dokku acl:list example-app': 90 | {{ dokku_acl_list.stdout }} 91 | 92 | 93 | # Testing dokku_clone 94 | - name: clone example-app 95 | dokku_clone: 96 | app: example-app 97 | repository: https://github.com/heroku/node-js-getting-started 98 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 99 | 100 | - name: Get list of apps # noqa 301 101 | command: dokku apps:list 102 | register: dokku_apps 103 | 104 | - name: Check that example-app is in list of apps 105 | assert: 106 | that: 107 | - "'example-app' in dokku_apps.stdout" 108 | msg: | 109 | 'example-app' not found in output of 'dokku apps:list': 110 | {{ dokku_apps.stdout }} 111 | 112 | - name: clone example app again 113 | dokku_clone: 114 | app: example-app 115 | repository: https://github.com/heroku/node-js-getting-started 116 | version: b10a4d7a20a6bbe49655769c526a2b424e0e5d0b 117 | build: false 118 | register: example_app 119 | 120 | - name: Check that re-cloning example app did not change anything 121 | assert: 122 | that: 123 | - not example_app.changed 124 | msg: | 125 | Re-cloning example app resulted in changed status 126 | 127 | # Testing dokku_network 128 | - name: Create a network # noqa 301 129 | dokku_network: 130 | name: example-network 131 | register: example_network 132 | 133 | - name: Get list of networks # noqa 301 134 | command: dokku network:list 135 | register: dokku_networks 136 | 137 | - name: Check that example-network is in list of networks 138 | assert: 139 | that: 140 | - "'example-network' in dokku_networks.stdout" 141 | msg: |- 142 | 'example-network' not found in output of 'dokku network:list': 143 | {{ dokku_networks.stdout }} 144 | 145 | - name: Create a network that already exists # noqa 301 146 | dokku_network: 147 | name: example-network 148 | register: example_network 149 | 150 | - name: Check that re-creating network example-network did not change anything 151 | assert: 152 | that: 153 | - not example_network.changed 154 | msg: | 155 | Re-creating network example-network resulted in changed status 156 | 157 | # Testing dokku_network_property 158 | - name: Setting a network property for an app 159 | dokku_network_property: 160 | app: example-app 161 | property: attach-post-create 162 | value: example-network 163 | 164 | - name: Get network report for app example-app # noqa 301 165 | command: dokku network:report example-app 166 | register: example_app_network_report 167 | 168 | - name: Check that property 'attach-post-create' of app 'example-app' has been set 169 | assert: 170 | that: 171 | - example_app_network_report.stdout is search("attach post create:\s+example-network") 172 | msg: |- 173 | 'attach post create: example-network' not found in outpot of 'dokku network:report example-app': 174 | {{ example_app_network_report.stdout }} 175 | 176 | - name: Clearing a network property for an app 177 | dokku_network_property: 178 | app: example-app 179 | property: attach-post-create 180 | 181 | - name: Get network report for app example-app # noqa 301 182 | command: dokku network:report example-app 183 | register: example_app_network_report 184 | 185 | - name: Check that property 'attach-post-create' of app 'example-app' has been cleared 186 | assert: 187 | that: 188 | - example_app_network_report.stdout is search("post create:\s+\n") 189 | msg: |- 190 | 'attach post create:' not found in output of 'dokku network:report example-app': 191 | {{ example_app_network_report.stdout }} 192 | 193 | # Testing dokku_ports 194 | - name: Set port mapping for an app 195 | dokku_ports: 196 | app: example-app 197 | mappings: 198 | - http:80:5000 199 | - http:8080:5000 200 | 201 | - name: Get proxy output 202 | command: dokku ports:report example-app --ports-map 203 | register: dokku_proxy_ports 204 | 205 | - name: Check that port mapping was set 206 | assert: 207 | that: 208 | - "'http:80:5000' in dokku_proxy_ports.stdout" 209 | - "'http:8080:5000' in dokku_proxy_ports.stdout" 210 | msg: |- 211 | port mapping 'http:80:5000' or 'http:8080:5000' was not set in output of 'dokku ports:report': 212 | {{ dokku_proxy_ports.stdout }} 213 | 214 | - name: Set port mapping that already exists 215 | dokku_ports: 216 | app: example-app 217 | mappings: 218 | - http:80:5000 219 | register: existing_proxy_ports 220 | 221 | - name: Check that setting existing port mapping did not change anything 222 | assert: 223 | that: 224 | - not existing_proxy_ports.changed 225 | msg: | 226 | Setting existing port mapping resulted in changed status 227 | 228 | - name: Remove port mapping 229 | dokku_ports: 230 | app: example-app 231 | mappings: 232 | - http:8080:5000 233 | state: absent 234 | 235 | - name: Get proxy output 236 | command: dokku ports:report example-app --ports-map 237 | register: dokku_proxy_ports 238 | 239 | - name: Check that the port mapping was removed 240 | assert: 241 | that: 242 | - "'http:8080:5000' not in dokku_proxy_ports.stdout" 243 | msg: |- 244 | port mapping 'http:8080:5000' was not removed in output of 'dokku ports:report': 245 | {{ dokku_proxy_ports.stdout }} 246 | 247 | # Testing dokku_ps_scale 248 | - name: Scaling application processes 249 | dokku_ps_scale: 250 | app: example-app 251 | scale: 252 | web: 2 253 | 254 | - name: Get current scale values for application # noqa 301 255 | command: dokku ps:scale example-app 256 | register: example_app_scale_values 257 | 258 | - name: Check that 'web' process of app 'example-app' has been scaled to '2' 259 | assert: 260 | that: 261 | - "'web: 2' in example_app_scale_values.stdout" 262 | msg: |- 263 | 'web: 2' not found in output of 'dokku ps:scale example-app': 264 | {{ example_app_scale_values.stdout }} 265 | 266 | # Testing dokku_image 267 | - name: Create a dummy directory for testing 268 | file: 269 | path: /home/dokku/test 270 | state: directory 271 | 272 | - name: Deploy meilisearch using dokku_image 273 | dokku_image: 274 | app: ms 275 | user_name: Elliot Alderson 276 | user_email: elliotalderson@protonmail.ch 277 | build_dir: /home/dokku/test 278 | image: getmeili/meilisearch:latest 279 | 280 | - name: Get list of apps # noqa 301 281 | command: dokku apps:list 282 | register: dokku_apps 283 | 284 | - name: Check that ms is in list of apps 285 | assert: 286 | that: 287 | - "'ms' in dokku_apps.stdout" 288 | msg: | 289 | 'ms' not found in output of 'dokku apps:list': 290 | {{ dokku_apps.stdout }} 291 | 292 | # Testing dokku_builder 293 | - name: Configuring the builder for an app 294 | dokku_builder: 295 | app: example-app 296 | property: build-dir 297 | value: /app 298 | 299 | - name: Get builder output # noqa 301 300 | command: dokku builder:report example-app 301 | register: dokku_builder 302 | 303 | - name: Check that the build dir was set correctly 304 | assert: 305 | that: 306 | - "'/app' in dokku_builder.stdout" 307 | msg: |- 308 | build-dir '/app' not found in output of 'dokku builder': 309 | {{ dokku_builder.stdout }} 310 | 311 | # Testing dokku_http_auth 312 | - name: Enabling http-auth for an app 313 | dokku_http_auth: 314 | app: example-app 315 | state: present 316 | username: samsepi0l 317 | password: hunter2 318 | 319 | - name: Get http-auth output # noqa 301 320 | command: dokku --quiet http-auth:report example-app 321 | register: dokku_http_auth_on 322 | 323 | - name: Check that the HTTP Basic Authentication was enabled correctly 324 | assert: 325 | that: 326 | - "'true' in dokku_http_auth_on.stdout" 327 | msg: |- 328 | 'true' not found in output of 'dokku http-auth:report': 329 | {{ dokku_http_auth_on.stdout }} 330 | 331 | - name: Disabling http-auth for an app 332 | dokku_http_auth: 333 | app: example-app 334 | state: absent 335 | 336 | - name: Get http-auth output # noqa 301 337 | command: dokku --quiet http-auth:report example-app 338 | register: dokku_http_auth_off 339 | 340 | - name: Check that the HTTP Basic Authentication was disabled correctly 341 | assert: 342 | that: 343 | - "'false' in dokku_http_auth_off.stdout" 344 | msg: |- 345 | 'false' not found in output of 'dokku http-auth:report': 346 | {{ dokku_http_auth_off.stdout }} 347 | 348 | # Testing dokku_checks 349 | - name: Disabling the Zero Downtime deployment 350 | dokku_checks: 351 | app: example-app 352 | state: absent 353 | 354 | - name: Get checks output # noqa 301 355 | command: dokku checks:report example-app 356 | register: dokku_checks 357 | 358 | - name: Check that the checks were disabled 359 | assert: 360 | that: 361 | - "'_all_' in dokku_checks.stdout" 362 | msg: |- 363 | checks were not disabled in output of 'dokku checks': 364 | {{ dokku_checks.stdout }} 365 | 366 | - name: Re-enabling the Zero Downtime deployment 367 | dokku_checks: 368 | app: example-app 369 | state: present 370 | 371 | - name: Get checks output # noqa 301 372 | command: dokku checks:report example-app 373 | register: dokku_checks 374 | 375 | - name: Check that the checks were re-enabled 376 | assert: 377 | that: 378 | - "'none' in dokku_checks.stdout" 379 | msg: |- 380 | checks were not enabled in output of 'dokku checks': 381 | {{ dokku_checks.stdout }} 382 | 383 | # Testing dokku_docker_options 384 | - name: Set docker build options 385 | dokku_docker_options: 386 | app: example-app 387 | phase: build 388 | option: "--pull" 389 | 390 | - name: Get docker-options output # noqa 301 391 | command: dokku docker-options:report example-app 392 | register: dokku_docker_options 393 | 394 | - name: Check that the docker options were set 395 | assert: 396 | that: 397 | - "'--pull' in dokku_docker_options.stdout" 398 | msg: |- 399 | docker option '--pull' was not set in output of 'dokku docker-options': 400 | {{ dokku_docker_options.stdout }} 401 | 402 | - name: Set docker build options that already exist # noqa 301 403 | dokku_docker_options: 404 | app: example-app 405 | phase: build 406 | option: "--pull" 407 | register: existing_docker_options 408 | 409 | - name: Check that setting existing docker options did not change anything 410 | assert: 411 | that: 412 | - not existing_docker_options.changed 413 | msg: | 414 | Setting existing docker options resulted in changed status 415 | 416 | - name: Remove docker build options 417 | dokku_docker_options: 418 | app: example-app 419 | phase: build 420 | option: "--pull" 421 | state: absent 422 | 423 | - name: Get docker-options output # noqa 301 424 | command: dokku docker-options:report example-app 425 | register: dokku_docker_options 426 | 427 | - name: Check that the docker options were removed 428 | assert: 429 | that: 430 | - "'--pull' not in dokku_docker_options.stdout" 431 | msg: |- 432 | docker option '--pull' was not removed in output of 'dokku docker-options': 433 | {{ dokku_docker_options.stdout }} 434 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # for running tests 2 | molecule~=24.12.0 3 | molecule-plugins[docker]~=23.5.3 4 | requests==2.30.0 5 | docker~=7.1.0 6 | ansible~=11.2.0 7 | # for development 8 | pre-commit 9 | # for README generation 10 | pyyaml 11 | -------------------------------------------------------------------------------- /tasks/dokku-daemon.yml: -------------------------------------------------------------------------------- 1 | - name: Install dokku-daemon 2 | when: dokku_daemon_install 3 | block: 4 | - name: ensure github.com is a known host 5 | lineinfile: 6 | dest: /root/.ssh/known_hosts 7 | create: true 8 | state: present 9 | line: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" 10 | regexp: "^github\\.com" 11 | mode: 0644 12 | tags: 13 | - dokku-daemon 14 | 15 | # Needed for Debian, see https://github.com/dokku/dokku-daemon/issues/27 16 | - name: install socat package 17 | apt: 18 | name: socat 19 | tags: 20 | - dokku-daemon 21 | 22 | - name: clone dokku-daemon 23 | git: 24 | repo: https://github.com/dokku/dokku-daemon.git 25 | dest: /var/lib/dokku-daemon 26 | update: false 27 | version: "{{ dokku_daemon_version }}" 28 | tags: 29 | - dokku-daemon 30 | 31 | - name: install make for building dokku-daemon 32 | apt: 33 | name: make 34 | state: present 35 | tags: 36 | - dokku-daemon 37 | 38 | - name: install dokku-daemon 39 | command: make install 40 | args: 41 | chdir: /var/lib/dokku-daemon 42 | creates: /usr/bin/dokku-daemon 43 | notify: 44 | - start dokku-daemon 45 | tags: 46 | - dokku-daemon 47 | -------------------------------------------------------------------------------- /tasks/init.yml: -------------------------------------------------------------------------------- 1 | # Dummy task file used to import the role without running any tasks 2 | -------------------------------------------------------------------------------- /tasks/install-pin.yml: -------------------------------------------------------------------------------- 1 | - name: remove pin from {{ item.key }} package 2 | file: 3 | path: /etc/apt/preferences.d/ansible-hold-{{ item.key }} 4 | state: absent 5 | when: not item.value 6 | 7 | - name: pin {{ item.key }} package 8 | copy: 9 | dest: /etc/apt/preferences.d/ansible-hold-{{ item.key }} 10 | content: | 11 | Package: {{ item.key }} 12 | Pin: version {{ item.value }} 13 | Pin-Priority: 1001 14 | mode: 0644 15 | when: item.value 16 | -------------------------------------------------------------------------------- /tasks/install.yml: -------------------------------------------------------------------------------- 1 | - name: packagecloud dokku apt key 2 | get_url: 3 | url: https://packagecloud.io/dokku/dokku/gpgkey 4 | dest: /etc/apt/trusted.gpg.d/dokku.asc 5 | mode: '0644' 6 | force: true 7 | tags: 8 | - dokku 9 | 10 | - name: dokku repo 11 | apt_repository: 12 | filename: dokku 13 | repo: 'deb https://packagecloud.io/dokku/dokku/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} main' 14 | state: present 15 | tags: 16 | - dokku 17 | 18 | - name: debconf dokku/hostname 19 | debconf: 20 | name: dokku 21 | question: 'dokku/hostname' 22 | value: '{{ dokku_hostname }}' 23 | vtype: 'string' 24 | tags: 25 | - dokku 26 | 27 | - name: debconf dokku/key_file 28 | debconf: 29 | name: dokku 30 | question: 'dokku/key_file' 31 | value: '{{ dokku_key_file }}' 32 | vtype: 'string' 33 | tags: 34 | - dokku 35 | 36 | - name: debconf dokku/skip_key_file 37 | debconf: 38 | name: dokku 39 | question: 'dokku/skip_key_file' 40 | value: '{{ dokku_skip_key_file }}' 41 | vtype: 'boolean' 42 | tags: 43 | - dokku 44 | 45 | - name: debconf dokku/vhost_enable 46 | debconf: 47 | name: dokku 48 | question: 'dokku/vhost_enable' 49 | value: '{{ dokku_vhost_enable }}' 50 | vtype: 'boolean' 51 | tags: 52 | - dokku 53 | 54 | - name: debconf dokku/web_config 55 | debconf: 56 | name: dokku 57 | question: 'dokku/web_config' 58 | value: '{{ dokku_web_config }}' 59 | vtype: 'boolean' 60 | tags: 61 | - dokku 62 | 63 | # this can be removed in July 2021 64 | # (package pinning deprecated). 65 | - name: package pinning 66 | include_tasks: install-pin.yml 67 | with_dict: 68 | plugn: "{{ plugn_version }}" 69 | sshcommand: "{{ sshcommand_version }}" 70 | herokuish: "{{ herokuish_version }}" 71 | dokku: "{{ dokku_version }}" 72 | tags: 73 | - dokku 74 | - dokku-install 75 | 76 | # note: this is necessary since the apt module with "state: present" (below) 77 | # does *not* check whether the package version agrees with the pinned version. 78 | # Cannot be done in the pinning loop since all packages need to be unpinned 79 | # (or you can get version conflicts). 80 | - name: install pinned {{ item.key }} package 81 | apt: 82 | name: "{{ item.key }}={{ item.value }}" 83 | with_dict: 84 | plugn: "{{ plugn_version }}" 85 | sshcommand: "{{ sshcommand_version }}" 86 | herokuish: "{{ herokuish_version }}" 87 | dokku: "{{ dokku_version }}" 88 | when: item.value 89 | 90 | - name: install dokku packages 91 | apt: 92 | name: 93 | - plugn 94 | - sshcommand 95 | - herokuish 96 | - dokku 97 | state: "{{ dokku_packages_state }}" 98 | tags: 99 | - dokku 100 | - dokku-install 101 | 102 | - name: write vhost 103 | when: dokku_vhost_enable | bool 104 | copy: 105 | content: "{{ dokku_hostname }}" 106 | dest: /home/dokku/VHOST 107 | mode: preserve 108 | tags: 109 | - dokku 110 | - dokku-install 111 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | - import_tasks: init.yml 2 | 3 | - import_tasks: nginx.yml 4 | tags: 5 | - dokku 6 | - dokku-nginx 7 | 8 | - import_tasks: install.yml 9 | tags: 10 | - dokku 11 | - dokku-install 12 | 13 | - import_tasks: dokku-daemon.yml 14 | tags: 15 | - dokku 16 | - dokku-daemon 17 | 18 | - import_tasks: ssh-keys.yml 19 | when: dokku_users is defined 20 | tags: 21 | - dokku 22 | - dokku-ssh-keys 23 | 24 | - name: dokku plugin:list 25 | command: dokku plugin:list 26 | changed_when: false 27 | register: installed_dokku_plugins 28 | tags: 29 | - dokku 30 | - dokku-plugins 31 | 32 | - name: install apt packages 33 | apt: 34 | name: 35 | - moreutils # for plugin updates 36 | - rsync # for dokku git:sync 37 | - cron # missing on debian11 38 | tags: 39 | - dokku-plugins 40 | 41 | - name: dokku:plugin install 42 | command: dokku plugin:install {{ item.url }} --name {{ item.name }} 43 | tags: 44 | - dokku 45 | - dokku-plugins 46 | when: dokku_plugins is defined and installed_dokku_plugins.stdout.find(item.name) == -1 47 | changed_when: true 48 | with_items: "{{ dokku_plugins }}" 49 | 50 | - name: dokku plugin:update 51 | cron: 52 | name: "dokku plugin:update {{ item.name }}" 53 | minute: "0" 54 | hour: "12" 55 | user: "root" 56 | job: "/usr/bin/chronic /usr/bin/dokku plugin:update {{ item.name }}" 57 | cron_file: "dokku-plugin-update-{{ item.name }}" 58 | when: dokku_plugins is defined and not item.url.endswith(".tar.gz") 59 | with_items: "{{ dokku_plugins }}" 60 | tags: 61 | - dokku 62 | - dokku-plugins 63 | 64 | - name: dokku:plugin install-dependencies # noqa: no-changed-when 65 | command: dokku plugin:install-dependencies 66 | tags: 67 | - dokku 68 | - dokku-plugins 69 | - molecule-idempotence-notest 70 | when: dokku_plugins is defined 71 | -------------------------------------------------------------------------------- /tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | - block: 2 | - name: nginx:disable default site 3 | file: 4 | dest: /etc/nginx/{{ item }}/default 5 | state: absent 6 | notify: 7 | - reload nginx 8 | tags: 9 | - dokku 10 | with_items: 11 | - sites-enabled 12 | - sites-available 13 | 14 | - name: Ensure nginx sites directories exists 15 | file: 16 | path: /etc/nginx/{{ item }} 17 | state: directory 18 | owner: root 19 | group: root 20 | mode: 0755 21 | with_items: 22 | - sites-enabled 23 | - sites-available 24 | 25 | - name: nginx:new default 26 | copy: 27 | src: nginx.default 28 | dest: /etc/nginx/sites-available/00-default 29 | owner: root 30 | group: root 31 | mode: 0644 32 | tags: 33 | - dokku 34 | 35 | - name: nginx:enable default 36 | file: 37 | src: /etc/nginx/sites-available/00-default 38 | dest: /etc/nginx/sites-enabled/00-default 39 | state: link 40 | notify: 41 | - reload nginx 42 | tags: 43 | - dokku 44 | when: dokku_manage_nginx 45 | -------------------------------------------------------------------------------- /tasks/ssh-key.yml: -------------------------------------------------------------------------------- 1 | - name: store sha256 hash of ssh key for user {{ username }} 2 | shell: ssh-keygen -lf <(echo "{{ ssh_key }}") | awk '{print $2}' 3 | no_log: true 4 | args: 5 | executable: /bin/bash 6 | changed_when: false 7 | register: sha256 8 | 9 | - name: dokku ssh-keys:add for user {{ username }} 10 | shell: echo "{{ ssh_key }}" | dokku ssh-keys:add {{ username }} 11 | no_log: true 12 | when: force_add or ssh_key_list.find(sha256.stdout) == -1 13 | changed_when: true 14 | -------------------------------------------------------------------------------- /tasks/ssh-keys.yml: -------------------------------------------------------------------------------- 1 | - name: sshcommand list dokku 2 | command: sshcommand list dokku 3 | changed_when: false 4 | register: sshcommand_users 5 | tags: 6 | - dokku 7 | - dokku-ssh-keys 8 | ignore_errors: true 9 | 10 | - name: add ssh key for user {{ item.username }} 11 | include_tasks: ssh-key.yml 12 | tags: 13 | - dokku 14 | - dokku-ssh-keys 15 | with_items: "{{ dokku_users }}" 16 | vars: 17 | username: "{{ item.username }}" 18 | ssh_key: "{{ item.ssh_key }}" 19 | ssh_key_list: "{{ sshcommand_users.stdout }}" 20 | force_add: "{{ sshcommand_users is skipped or sshcommand_users is failed or sshcommand_users.stdout.find(item.username) == -1 }}" 21 | --------------------------------------------------------------------------------