├── .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 | [](https://galaxy.ansible.com/dokku_bot/ansible_dokku) [](https://github.com/dokku/ansible-dokku/releases) [](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:*
- **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - enable
- disable
- clear
- **present** (default)
- absent
- set
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - clear
- **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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:* - **present** (default)
- absent
|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 | "[](https://galaxy.ansible.com/dokku_bot/ansible_dokku) "
225 | + "[](https://github.com/dokku/ansible-dokku/releases) "
226 | + "[](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 |
--------------------------------------------------------------------------------