pax_global_header00006660000000000000000000000064146367432400014523gustar00rootroot0000000000000052 comment=d38b26bb015a5991dad445d25a2af6fa2b61caca podman-compose-1.2.0/000077500000000000000000000000001463674324000144445ustar00rootroot00000000000000podman-compose-1.2.0/.codespellignore000066400000000000000000000000111463674324000176130ustar00rootroot00000000000000assertIn podman-compose-1.2.0/.codespellrc000066400000000000000000000002031463674324000167370ustar00rootroot00000000000000[codespell] skip = .git,*.pdf,*.svg,requirements.txt,test-requirements.txt # poped - loved variable name ignore-words-list = poped podman-compose-1.2.0/.coveragerc000066400000000000000000000000251463674324000165620ustar00rootroot00000000000000[run] parallel=True podman-compose-1.2.0/.editorconfig000066400000000000000000000004141463674324000171200ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = tab tab_width = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.py] indent_style = space podman-compose-1.2.0/.github/000077500000000000000000000000001463674324000160045ustar00rootroot00000000000000podman-compose-1.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001463674324000201675ustar00rootroot00000000000000podman-compose-1.2.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000025441463674324000226660ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. Please make sure it's not a bug in podman (in that case report it to podman) or your understanding of docker-compose or how rootless containers work (for example, it's normal for rootless container not to be able to listen for port less than 1024 like 80) please try to reproduce the bug in latest devel branch **To Reproduce** Steps to reproduce the behavior: 1. what is the content of the current working directory (ex. `docker-compose.yml`, `.env`, `Dockerfile`, ...etc.) 2. what is the sequence of commands you typed please use [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) for example give me a small busybox-based compose yaml **Expected behavior** A clear and concise description of what you expected to happen. **Actual behavior** What is the behavior you actually got and that should not happen. **Output** ``` $ podman-compose version using podman version: 3.4.0 podman-compose version 0.1.7dev podman --version podman version 3.4.0 $ podman-compose up ... ``` **Environment:** - OS: Linux / WSL / Mac - podman version: - podman compose version: (git hex) **Additional context** Add any other context about the problem here. podman-compose-1.2.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341463674324000237130ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. podman-compose-1.2.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000006101463674324000216020ustar00rootroot00000000000000 ## Contributor Checklist: If this PR adds a new feature that improves compatibility with docker-compose, please add a link to the exact part of compose spec that the PR touches. For any user-visible change please add a release note to newsfragments directory, e.g. newsfragments/my_feature.feature. See newsfragments/README.md for more details. All changes require additional unit tests. podman-compose-1.2.0/.github/dependabot.yml000066400000000000000000000001661463674324000206370ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" podman-compose-1.2.0/.github/workflows/000077500000000000000000000000001463674324000200415ustar00rootroot00000000000000podman-compose-1.2.0/.github/workflows/codespell.yml000066400000000000000000000005631463674324000225420ustar00rootroot00000000000000--- name: Codespell on: push: pull_request: permissions: contents: read jobs: codespell: name: Check for spelling errors runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Codespell uses: codespell-project/actions-codespell@v2 with: ignore_words_file: .codespellignore podman-compose-1.2.0/.github/workflows/static-checks.yml000066400000000000000000000011731463674324000233130ustar00rootroot00000000000000name: Static checks on: - push - pull_request jobs: static-checks: runs-on: ubuntu-latest container: image: docker.io/library/python:3.11-bookworm # cgroupns needed to address the following error: # write /sys/fs/cgroup/cgroup.subtree_control: operation not supported options: --privileged --cgroupns=host steps: - uses: actions/checkout@v4 - name: Analysing the code with ruff run: | set -e pip install -r test-requirements.txt ruff format --check ruff check - name: Analysing the code with pylint run: | pylint podman_compose.py podman-compose-1.2.0/.github/workflows/test.yml000066400000000000000000000022611463674324000215440ustar00rootroot00000000000000name: Tests on: push: pull_request: jobs: test: strategy: fail-fast: false matrix: python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] runs-on: ubuntu-latest container: image: "docker.io/library/python:${{ matrix.python-version }}-bookworm" # cgroupns needed to address the following error: # write /sys/fs/cgroup/cgroup.subtree_control: operation not supported options: --privileged --cgroupns=host steps: - uses: actions/checkout@v4 - name: Install dependencies run: | set -e apt update && apt install -y podman python -m pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - name: Run tests in tests/ run: | python -m unittest -v tests/*.py env: TESTS_DEBUG: 1 - name: Run tests in pytests/ run: | coverage run --source podman_compose -m unittest pytests/*.py - name: Report coverage run: | coverage combine coverage report --format=markdown | tee -a $GITHUB_STEP_SUMMARY podman-compose-1.2.0/.gitignore000066400000000000000000000023521463674324000164360ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ .idea/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ test-compose.yaml test-compose-?.yaml # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .vscode podman-compose-1.2.0/.pre-commit-config.yaml000066400000000000000000000020051463674324000207220ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black # It is recommended to specify the latest version of Python # supported by your project here, or alternatively use # pre-commit's default_language_version, see # https://pre-commit.com/#top_level-default_language_version language_version: python3.10 types: [python] args: [ "--check", # Don't apply changes automatically ] - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 types: [python] - repo: local hooks: - id: pylint name: pylint entry: pylint language: system types: [python] args: [ "-rn", # Only display messages "-sn", # Don't display the score "--rcfile=.pylintrc", # Link to your config file ] - repo: https://github.com/codespell-project/codespell rev: v2.2.5 hooks: - id: codespell podman-compose-1.2.0/.pylintrc000066400000000000000000000017501463674324000163140ustar00rootroot00000000000000[MESSAGES CONTROL] # C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin # consider-using-with: we need it for color formatter pipe disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment # allow _ for ignored variables # allow generic names like a,b,c and i,j,k,l,m,n and x,y,z # allow k,v for key/value # allow e for exceptions, it for iterator, ix for index # allow ip for ip address # allow w,h for width, height # allow op for operation/operator/opcode # allow t, t0, t1, t2, and t3 for time # allow dt for delta time # allow db for database # allow ls for list # allow p for pipe # allow ex for examples, exists ..etc good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,ix,ip,op,p,ex podman-compose-1.2.0/CODE-OF-CONDUCT.md000066400000000000000000000003151463674324000170760ustar00rootroot00000000000000## The Podman Compose Project Community Code of Conduct The Podman Compose project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md). podman-compose-1.2.0/CONTRIBUTING.md000066400000000000000000000105621463674324000167010ustar00rootroot00000000000000# Contributing to podman-compose ## Who can contribute? - Users that found a bug, - Users that want to propose new functionalities or enhancements, - Users that want to help other users to troubleshoot their environments, - Developers that want to fix bugs, - Developers that want to implement new functionalities or enhancements. ## Branches Please request your pull request to be merged into the `devel` branch. Changes to the `stable` branch are managed by the repository maintainers. ## Development environment setup Note: Some steps are OPTIONAL but all are RECOMMENDED. 1. Fork the project repository and clone it: ```shell $ git clone https://github.com/USERNAME/podman-compose.git $ cd podman-compose ``` 2. (OPTIONAL) Create a Python virtual environment. Example using [virtualenv wrapper](https://virtualenvwrapper.readthedocs.io/en/latest/): ```shell $ mkvirtualenv podman-compose ``` 3. Install the project runtime and development requirements: ```shell $ pip install '.[devel]' ``` 4. (OPTIONAL) Install `pre-commit` git hook scripts (https://pre-commit.com/#3-install-the-git-hook-scripts): ```shell $ pre-commit install ``` 5. Create a new branch, develop and add tests when possible. 6. Run linting and testing before committing code. Ensure all the hooks are passing. ```shell $ pre-commit run --all-files ``` 7. Run code coverage: ```shell $ coverage run --source podman_compose -m unittest pytests/*.py $ python -m unittest tests/*.py $ coverage combine $ coverage report $ coverage html ``` 8. Commit your code to your fork's branch. - Make sure you include a `Signed-off-by` message in your commits. Read [this guide](https://github.com/containers/common/blob/main/CONTRIBUTING.md#sign-your-prs) to learn how to sign your commits. - In the commit message, reference the Issue ID that your code fixes and a brief description of the changes. Example: `Fixes #516: Allow empty network` 9. Open a pull request to `containers/podman-compose:devel` and wait for a maintainer to review your work. ## Adding new commands To add a command, you need to add a function that is decorated with `@cmd_run`. The decorated function must be declared `async` and should accept two arguments: The compose instance and the command-specific arguments (resulted from the Python's `argparse` package). In this function, you can run Podman (e.g. `await compose.podman.run(['inspect', 'something'])`), access `compose.pods`, `compose.containers` etc. Here is an example: ```python @cmd_run(podman_compose, 'build', 'build images defined in the stack') async def compose_build(compose, args): await compose.podman.run(['build', 'something']) ``` ## Command arguments parsing To add arguments to be parsed by a command, you need to add a function that is decorated with `@cmd_parse` which accepts the compose instance and the command's name (as a string list or as a single string). The decorated function should accept a single argument: An instance of `argparse`. In this function, you can call `parser.add_argument()` to add a new argument to the command. Note you can add such a function multiple times. Here is an example: ```python @cmd_parse(podman_compose, 'build') def compose_build_parse(parser): parser.add_argument("--pull", help="attempt to pull a newer version of the image", action='store_true') parser.add_argument("--pull-always", help="Attempt to pull a newer version of the image, " "raise an error even if the image is present locally.", action='store_true') ``` NOTE: `@cmd_parse` should be after `@cmd_run`. ## Calling a command from another one If you need to call `podman-compose down` from `podman-compose up`, do something like: ```python @cmd_run(podman_compose, 'up', 'up desc') async def compose_up(compose, args): await compose.commands['down'](compose, args) # or await compose.commands['down'](argparse.Namespace(foo=123)) ``` ## Missing Commands (help needed) ``` bundle Generate a Docker bundle from the Compose file create Create services events Receive real time events from containers images List images rm Remove stopped containers scale Set number of containers for a service top Display the running processes ``` podman-compose-1.2.0/LICENSE000066400000000000000000000432541463674324000154610ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. podman-compose-1.2.0/README.md000066400000000000000000000105321463674324000157240ustar00rootroot00000000000000# Podman Compose ## [![Tests](https://github.com/containers/podman-compose/actions/workflows/test.yml/badge.svg)](https://github.com/containers/podman-compose/actions/workflows/test.yml) An implementation of [Compose Spec](https://compose-spec.io/) with [Podman](https://podman.io/) backend. This project focuses on: * rootless * daemon-less process model, we directly execute podman, no running daemon. This project only depends on: * `podman` * [podman dnsname plugin](https://github.com/containers/dnsname): It is usually found in the `podman-plugins` or `podman-dnsname` distro packages, those packages are not pulled by default and you need to install them. This allows containers to be able to resolve each other if they are on the same CNI network. This is not necessary when podman is using netavark as a network backend. * Python3 * [PyYAML](https://pyyaml.org/) * [python-dotenv](https://pypi.org/project/python-dotenv/) And it's formed as a single Python file script that you can drop into your PATH and run. ## References: * [spec.md](https://github.com/compose-spec/compose-spec/blob/master/spec.md) * [docker-compose compose-file-v3](https://docs.docker.com/compose/compose-file/compose-file-v3/) * [docker-compose compose-file-v2](https://docs.docker.com/compose/compose-file/compose-file-v2/) ## Alternatives As in [this article](https://fedoramagazine.org/use-docker-compose-with-podman-to-orchestrate-containers-on-fedora/) you can setup a `podman.socket` and use unmodified `docker-compose` that talks to that socket but in this case you lose the process-model (ex. `docker-compose build` will send a possibly large context tarball to the daemon) For production-like single-machine containerized environment consider - [k3s](https://k3s.io) | [k3s github](https://github.com/rancher/k3s) - [MiniKube](https://minikube.sigs.k8s.io/) For the real thing (multi-node clusters) check any production OpenShift/Kubernetes distribution like [OKD](https://www.okd.io/). ## Versions If you have legacy version of `podman` (before 3.1.0) you might need to stick with legacy `podman-compose` `0.1.x` branch. The legacy branch 0.1.x uses mappings and workarounds to compensate for rootless limitations. Modern podman versions (>=3.4) do not have those limitations, and thus you can use latest and stable 1.x branch. If you are upgrading from `podman-compose` version `0.1.x` then we no longer have global option `-t` to set mapping type like `hostnet`. If you desire that behavior, pass it the standard way like `network_mode: host` in the YAML. ## Installation ### Pip Install the latest stable version from PyPI: ```bash pip3 install podman-compose ``` pass `--user` to install inside regular user home without being root. Or latest development version from GitHub: ```bash pip3 install https://github.com/containers/podman-compose/archive/main.tar.gz ``` ### Homebrew ```bash brew install podman-compose ``` ### Manual ```bash curl -o /usr/local/bin/podman-compose https://raw.githubusercontent.com/containers/podman-compose/main/podman_compose.py chmod +x /usr/local/bin/podman-compose ``` or inside your home ```bash curl -o ~/.local/bin/podman-compose https://raw.githubusercontent.com/containers/podman-compose/main/podman_compose.py chmod +x ~/.local/bin/podman-compose ``` or install from Fedora (starting from f31) repositories: ```bash sudo dnf install podman-compose ``` ## Basic Usage We have included fully functional sample stacks inside `examples/` directory. You can get more examples from [awesome-compose](https://github.com/docker/awesome-compose). A quick example would be ```bash cd examples/busybox podman-compose --help podman-compose up --help podman-compose up ``` A more rich example can be found in [examples/awx3](examples/awx3) which have - A Postgres Database - RabbitMQ server - MemCached server - a django web server - a django tasks When testing the `AWX3` example, if you got errors, just wait for db migrations to end. There is also AWX 17.1.0 ## Tests Inside `tests/` directory we have many useless docker-compose stacks that are meant to test as many cases as we can to make sure we are compatible ### Unit tests with unittest run a unittest with following command ```shell python -m unittest pytests/*.py ``` # Contributing guide If you are a user or a developer and want to contribute please check the [CONTRIBUTING](CONTRIBUTING.md) section podman-compose-1.2.0/SECURITY.md000066400000000000000000000003751463674324000162420ustar00rootroot00000000000000## Security and Disclosure Information Policy for the Podman Compose Project The Podman Compose Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects. podman-compose-1.2.0/completion/000077500000000000000000000000001463674324000166155ustar00rootroot00000000000000podman-compose-1.2.0/completion/bash/000077500000000000000000000000001463674324000175325ustar00rootroot00000000000000podman-compose-1.2.0/completion/bash/podman-compose000066400000000000000000000273601463674324000224060ustar00rootroot00000000000000# Naming convention: # * _camelCase for function names # * snake_case for variable names # all functions will return 0 if they successfully complete the argument # (or establish there is no need or no way to complete), and something # other than 0 if that's not the case # complete arguments to global options _completeGlobalOptArgs() { # arguments to options that take paths as arguments: complete paths for el in ${path_arg_global_opts}; do if [[ ${prev} == ${el} ]]; then COMPREPLY=( $(compgen -f -- ${cur}) ) return 0 fi done # arguments to options that take generic arguments: don't complete for el in ${generic_arg_global_opts}; do if [[ ${prev} == ${el} ]]; then return 0 fi done return 1 } # complete root subcommands and options _completeRoot() { # if we're completing an option if [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${global_opts}" -- ${cur}) ) return 0 fi # complete root commands COMPREPLY=( $(compgen -W "${root_commands}" -- ${cur}) ) return 0 } # complete names of Compose services _completeServiceNames() { # ideally we should complete service names, # but parsing the compose spec file in the # completion script is quite complex return 0 } # complete commands to run inside containers _completeCommand() { # we would need to complete commands to run inside # a container return 0 } # complete the arguments for `podman-compose up` and return 0 _completeUpArgs() { up_opts="${help_opts} -d --detach --no-color --quiet-pull --no-deps --force-recreate --always-recreate-deps --no-recreate --no-build --no-start --build --abort-on-container-exit -t --timeout -V --renew-anon-volumes --remove-orphans --scale --exit-code-from --pull --pull-always --build-arg --no-cache" if [[ ${prev} == "--scale" || ${prev} == "-t" || ${prev} == "--timeout" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${up_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose exec` and return 0 _completeExecArgs() { exec_opts="${help_opts} -d --detach --privileged -u --user -T --index -e --env -w --workdir" if [[ ${prev} == "-u" || ${prev} == "--user" || ${prev} == "--index" || ${prev} == "-e" || ${prev} == "--env" || ${prev} == "-w" || ${prev} == "--workdir" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${exec_opts}" -- ${cur}) ) return 0 elif [[ ${comp_cword_adj} -eq 2 ]]; then # complete service name _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi elif [[ ${comp_cword_adj} -eq 3 ]]; then _completeCommand if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose down` and return 0 _completeDownArgs() { down_opts="${help_opts} -v --volumes -t --timeout --remove-orphans" if [[ ${prev} == "-t" || ${prev} == "--timeout" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${down_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose build` and return 0 _completeBuildArgs() { build_opts="${help_opts} --pull --pull-always --build-arg --no-cache" if [[ ${prev} == "--build-arg" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${build_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose logs` and return 0 _completeLogsArgs() { logs_opts="${help_opts} -f --follow -l --latest -n --names --since -t --timestamps --tail --until" if [[ ${prev} == "--since" || ${prev} == "--tail" || ${prev} == "--until" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${logs_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose ps` and return 0 _completePsArgs() { ps_opts="${help_opts} -q --quiet" if [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${ps_opts}" -- ${cur}) ) return 0 else return 0 fi } # complete the arguments for `podman-compose pull` and return 0 _completePullArgs() { pull_opts="${help_opts} --force-local" if [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${pull_opts}" -- ${cur}) ) return 0 else return 0 fi } # complete the arguments for `podman-compose push` and return 0 _completePushArgs() { push_opts="${help_opts} --ignore-push-failures" if [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${push_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose restart` and return 0 _completeRestartArgs() { restart_opts="${help_opts} -t --timeout" if [[ ${prev} == "-t" || ${prev} == "--timeout" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${restart_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose stop` and return 0 _completeStopArgs() { stop_opts="${help_opts} -t --timeout" if [[ ${prev} == "-t" || ${prev} == "--timeout" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${stop_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose start` and return 0 _completeStartArgs() { start_opts="${help_opts}" if [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${start_opts}" -- ${cur}) ) return 0 else _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi return 0 fi } # complete the arguments for `podman-compose run` and return 0 _completeRunArgs() { run_opts="${help_opts} -d --detach --privileged -u --user -T --index -e --env -w --workdir" if [[ ${prev} == "-u" || ${prev} == "--user" || ${prev} == "--index" || ${prev} == "-e" || ${prev} == "--env" || ${prev} == "-w" || ${prev} == "--workdir" ]]; then return 0 elif [[ ${cur} == -* ]]; then COMPREPLY=( $(compgen -W "${run_opts}" -- ${cur}) ) return 0 elif [[ ${comp_cword_adj} -eq 2 ]]; then # complete service name _completeServiceNames if [[ $? -eq 0 ]]; then return 0 fi elif [[ ${comp_cword_adj} -eq 3 ]]; then _completeCommand if [[ $? -eq 0 ]]; then return 0 fi fi } _podmanCompose() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" root_commands="help version pull push build up down ps run exec start stop restart logs" # options to output help text (used as global and subcommand options) help_opts="-h --help" # global options that don't take additional arguments basic_global_opts="${help_opts} -v --no-ansi --no-cleanup --dry-run" # global options that take paths as arguments path_arg_global_opts="-f --file --podman-path" path_arg_global_opts_array=($arg_global_opts) # global options that take arguments that are not files generic_arg_global_opts="-p --project-name --podman-path --podman-args --podman-pull-args --podman-push-args --podman-build-args --podman-inspect-args --podman-run-args --podman-start-args --podman-stop-args --podman-rm-args --podman-volume-args" generic_arg_global_opts_array=($generic_arg_global_opts) # all global options that take arguments arg_global_opts="${path_arg_global_opts} ${generic_arg_global_opts}" arg_global_opts_array=($arg_global_opts) # all global options global_opts="${basic_global_opts} ${arg_global_opts}" chosen_root_command="" _completeGlobalOptArgs if [[ $? -eq 0 ]]; then return 0 fi # computing comp_cword_adj, which thruthfully tells us how deep in the subcommands tree we are # additionally, set the chosen_root_command if possible comp_cword_adj=${COMP_CWORD} if [[ ${COMP_CWORD} -ge 2 ]]; then skip_next="no" for el in ${COMP_WORDS[@]}; do # if the user has asked for help text there's no need to complete further if [[ ${el} == "-h" || ${el} == "--help" ]]; then return 0 fi if [[ ${skip_next} == "yes" ]]; then let "comp_cword_adj--" skip_next="no" continue fi if [[ ${el} == -* && ${el} != ${cur} ]]; then let "comp_cword_adj--" for opt in ${arg_global_opts_array[@]}; do if [[ ${el} == ${opt} ]]; then skip_next="yes" fi done elif [[ ${el} != ${cur} && ${el} != ${COMP_WORDS[0]} && ${chosen_root_command} == "" ]]; then chosen_root_command=${el} fi done fi if [[ ${comp_cword_adj} -eq 1 ]]; then _completeRoot # Given that we check the value of comp_cword_adj outside # of it, at the moment _completeRoot should always return # 0, this is just here in case changes are made. The same # will apply to similar functions below if [[ $? -eq 0 ]]; then return 0 fi fi case $chosen_root_command in up) _completeUpArgs if [[ $? -eq 0 ]]; then return 0 fi ;; down) _completeDownArgs if [[ $? -eq 0 ]]; then return 0 fi ;; exec) _completeExecArgs if [[ $? -eq 0 ]]; then return 0 fi ;; build) _completeBuildArgs if [[ $? -eq 0 ]]; then return 0 fi ;; logs) _completeLogsArgs if [[ $? -eq 0 ]]; then return 0 fi ;; ps) _completePsArgs if [[ $? -eq 0 ]]; then return 0 fi ;; pull) _completePullArgs if [[ $? -eq 0 ]]; then return 0 fi ;; push) _completePushArgs if [[ $? -eq 0 ]]; then return 0 fi ;; restart) _completeRestartArgs if [[ $? -eq 0 ]]; then return 0 fi ;; start) _completeStartArgs if [[ $? -eq 0 ]]; then return 0 fi ;; stop) _completeStopArgs if [[ $? -eq 0 ]]; then return 0 fi ;; run) _completeRunArgs if [[ $? -eq 0 ]]; then return 0 fi ;; esac } complete -F _podmanCompose podman-compose podman-compose-1.2.0/docs/000077500000000000000000000000001463674324000153745ustar00rootroot00000000000000podman-compose-1.2.0/docs/Changelog-1.1.0.md000066400000000000000000000031321463674324000202370ustar00rootroot00000000000000Version v1.1.0 (2024-04-17) =========================== Bug fixes --------- - Fixed support for values with equals sign in `-e` argument of `run` and `exec` commands. - Fixed duplicate arguments being emitted in `stop` and `restart` commands. - Removed extraneous debug output. `--verbose` flag has been added to preserve verbose output. - Links aliases are now added to service aliases. - Fixed image build process to use defined environmental variables. - Empty list is now allowed to be `COMMAND` and `ENTRYPOINT`. - Environment files are now resolved relative to current working directory. - Exit code of container build is now preserved as return code of `build` command. New features ------------ - Added support for `uidmap`, `gidmap`, `http_proxy` and `runtime` service configuration keys. - Added support for `enable_ipv6` network configuration key. - Added `--parallel` option to support parallel pulling and building of images. - Implemented support for maps in `sysctls` container configuration key. - Implemented `stats` command. - Added `--no-normalize` flag to `config` command. - Added support for `include` global configuration key. - Added support for `build` command. - Added support to start containers with multiple networks. - Added support for `profile` argument. - Added support for starting podman in existing network namespace. - Added IPAM driver support. - Added support for file secrets being passed to `podman build` via `--secret` argument. - Added support for multiple networks with separately specified IP and MAC address. - Added support for `service.build.ulimits` when building image. podman-compose-1.2.0/docs/Changelog-1.2.0.md000066400000000000000000000031131463674324000202370ustar00rootroot00000000000000Version v1.2.0 (2024-06-26) =========================== Bug fixes --------- - Fixed handling of `--in-pod` argument. Previously it was hard to provide false value to it. - podman-compose no longer creates pods when registering systemd unit. - Fixed warning `RuntimeWarning: coroutine 'create_pods' was never awaited` - Fixed error when setting up IPAM network with default driver. - Fixed support for having list and dictionary `depends_on` sections in related compose files. - Fixed logging of failed build message. - Fixed support for multiple entries in `include` section. - Fixed environment variable precedence order. Changes ------- - `x-podman` dictionary in container root has been migrated to `x-podman.*` fields in container root. New features ------------ - Added support for `--publish` in `podman-compose run`. - Added support for Podman external root filesystem management (`--rootfs` option). - Added support for `podman-compose images` command. - Added support for `env_file` being configured via dictionaries. - Added support for enabling GPU access. - Added support for selinux in verbose mount specification. - Added support for `additional_contexts` section. - Added support for multi-line environment files. - Added support for passing contents of `podman-compose.yml` via stdin. - Added support for specifying the value for `--in-pod` setting in `podman-compose.yml` file. - Added support for environmental secrets. Documentation ------------- - Added instructions on how to install podman-compose on Homebrew. - Added explanation that netavark is an alternative to dnsname plugin podman-compose-1.2.0/docs/Extensions.md000066400000000000000000000064401463674324000200610ustar00rootroot00000000000000# Podman specific extensions to the docker-compose format Podman-compose supports the following extension to the docker-compose format. These extensions are generally specified under fields with "x-podman" prefix in the compose file. ## Container management The following extension keys are available under container configuration: * `x-podman.uidmap` - Run the container in a new user namespace using the supplied UID mapping. * `x-podman.gidmap` - Run the container in a new user namespace using the supplied GID mapping. * `x-podman.rootfs` - Run the container without requiring any image management; the rootfs of the container is assumed to be managed externally. For example, the following docker-compose.yml allows running a podman container with externally managed rootfs. ```yml version: "3" services: my_service: command: ["/bin/busybox"] x-podman.rootfs: "/path/to/rootfs" ``` For explanations of these extensions, please refer to the [Podman Documentation](https://docs.podman.io/). ## Per-network MAC-addresses Generic docker-compose files support specification of the MAC address on the container level. If the container has multiple network interfaces, the specified MAC address is applied to the first specified network. Podman-compose in addition supports the specification of MAC addresses on a per-network basis. This is done by adding a `x-podman.mac_address` key to the network configuration in the container. The value of the `x-podman.mac_address` key is the MAC address to be used for the network interface. Specifying a MAC address for the container and for individual networks at the same time is not supported. Example: ```yaml --- version: "3" networks: net0: driver: "bridge" ipam: config: - subnet: "192.168.0.0/24" net1: driver: "bridge" ipam: config: - subnet: "192.168.1.0/24" services: webserver image: "busybox" command: ["/bin/busybox", "httpd", "-f", "-h", "/etc", "-p", "8001"] networks: net0: ipv4_address: "192.168.0.10" x-podman.mac_address: "02:aa:aa:aa:aa:aa" net1: ipv4_address: "192.168.1.10" x-podman.mac_address: "02:bb:bb:bb:bb:bb" ``` ## Podman-specific network modes Generic docker-compose supports the following values for `network-mode` for a container: - `bridge` - `host` - `none` - `service` - `container` In addition, podman-compose supports the following podman-specific values for `network-mode`: - `slirp4netns[:,...]` - `ns:` - `pasta[:,...]` - `private` The options to the network modes are passed to the `--network` option of the `podman create` command as-is. ## Custom pods management Podman-compose can have containers in pods. This can be controlled by extension key x-podman in_pod. It allows providing custom value for --in-pod and is especially relevant when --userns has to be set. For example, the following docker-compose.yml allows using userns_mode by overriding the default value of --in-pod (unless it was specifically provided by "--in-pod=True" in command line interface). ```yml version: "3" services: cont: image: nopush/podman-compose-test userns_mode: keep-id:uid=1000 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] x-podman: in_pod: false ``` podman-compose-1.2.0/docs/Mappings.md000066400000000000000000000011141463674324000174710ustar00rootroot00000000000000# Overview * `1podfw` - create all containers in one pod (inter-container communication is done via `localhost`), doing port mapping in that pod * `1pod` - create all containers in one pod, doing port mapping in each container (does not work) * `identity` - no mapping * `hostnet` - use host network, and inter-container communication is done via host gateway and published ports * `cntnet` - create a container and use it via `--network container:name` (inter-container communication via `localhost`) * `publishall` - publish all ports to host (using `-P`) and communicate via gateway podman-compose-1.2.0/examples/000077500000000000000000000000001463674324000162625ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/000077500000000000000000000000001463674324000172315ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/README.md000066400000000000000000000023321463674324000205100ustar00rootroot00000000000000# AWX Compose the directory roles is taken from [here](https://github.com/ansible/awx/tree/17.1.0/installer/roles/local_docker) also look at https://github.com/ansible/awx/tree/17.1.0/tools/docker-compose ``` mkdir deploy awx17 ansible localhost \ -e host_port=8080 \ -e awx_secret_key='awx,secret.123' \ -e secret_key='awx,secret.123' \ -e admin_user='admin' \ -e admin_password='admin' \ -e pg_password='awx,123.' \ -e pg_username='awx' \ -e pg_database='awx' \ -e pg_port='5432' \ -e redis_image="docker.io/library/redis:6-alpine" \ -e postgres_data_dir="./data/pg" \ -e compose_start_containers=false \ -e dockerhub_base='docker.io/ansible' \ -e awx_image='docker.io/ansible/awx' \ -e awx_version='17.1.0' \ -e dockerhub_version='17.1.0' \ -e docker_deploy_base_path=$PWD/deploy \ -e docker_compose_dir=$PWD/awx17 \ -e awx_task_hostname=awx \ -e awx_web_hostname=awxweb \ -m include_role -a name=local_docker cp awx17/docker-compose.yml awx17/docker-compose.yml.orig sed -i -re "s#- \"$PWD/awx17/(.*):/#- \"./\1:/#" awx17/docker-compose.yml cd awx17 podman-compose run --rm --service-ports task awx-manage migrate --no-input podman-compose up -d ``` podman-compose-1.2.0/examples/awx17/roles/000077500000000000000000000000001463674324000203555ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/roles/local_docker/000077500000000000000000000000001463674324000227765ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/roles/local_docker/defaults/000077500000000000000000000000001463674324000246055ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/roles/local_docker/defaults/main.yml000066400000000000000000000003711463674324000262550ustar00rootroot00000000000000--- dockerhub_version: "{{ lookup('file', playbook_dir + '/../VERSION') }}" awx_image: "awx" redis_image: "redis" postgresql_version: "12" postgresql_image: "postgres:{{postgresql_version}}" compose_start_containers: true upgrade_postgres: false podman-compose-1.2.0/examples/awx17/roles/local_docker/tasks/000077500000000000000000000000001463674324000241235ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/roles/local_docker/tasks/compose.yml000066400000000000000000000042751463674324000263230ustar00rootroot00000000000000--- - name: Create {{ docker_compose_dir }} directory file: path: "{{ docker_compose_dir }}" state: directory - name: Create Redis socket directory file: path: "{{ docker_compose_dir }}/redis_socket" state: directory mode: 0777 - name: Create Docker Compose Configuration template: src: "{{ item.file }}.j2" dest: "{{ docker_compose_dir }}/{{ item.file }}" mode: "{{ item.mode }}" loop: - file: environment.sh mode: "0600" - file: credentials.py mode: "0600" - file: docker-compose.yml mode: "0600" - file: nginx.conf mode: "0600" - file: redis.conf mode: "0664" register: awx_compose_config - name: Render SECRET_KEY file copy: content: "{{ secret_key }}" dest: "{{ docker_compose_dir }}/SECRET_KEY" mode: 0600 register: awx_secret_key - block: - name: Remove AWX containers before migrating postgres so that the old postgres container does not get used docker_compose: project_src: "{{ docker_compose_dir }}" state: absent ignore_errors: true - name: Run migrations in task container shell: docker-compose run --rm --service-ports task awx-manage migrate --no-input args: chdir: "{{ docker_compose_dir }}" - name: Start the containers docker_compose: project_src: "{{ docker_compose_dir }}" restarted: "{{ awx_compose_config is changed or awx_secret_key is changed }}" register: awx_compose_start - name: Update CA trust in awx_web container command: docker exec awx_web '/usr/bin/update-ca-trust' when: awx_compose_config.changed or awx_compose_start.changed - name: Update CA trust in awx_task container command: docker exec awx_task '/usr/bin/update-ca-trust' when: awx_compose_config.changed or awx_compose_start.changed - name: Wait for launch script to create user wait_for: timeout: 10 delegate_to: localhost - name: Create Preload data command: docker exec awx_task bash -c "/usr/bin/awx-manage create_preload_data" when: create_preload_data|bool register: cdo changed_when: "'added' in cdo.stdout" when: compose_start_containers|bool podman-compose-1.2.0/examples/awx17/roles/local_docker/tasks/main.yml000066400000000000000000000006161463674324000255750ustar00rootroot00000000000000--- - name: Generate broadcast websocket secret set_fact: broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true when: broadcast_websocket_secret is not defined - import_tasks: upgrade_postgres.yml when: - postgres_data_dir is defined - pg_hostname is not defined - import_tasks: set_image.yml - import_tasks: compose.yml podman-compose-1.2.0/examples/awx17/roles/local_docker/tasks/set_image.yml000066400000000000000000000034761463674324000266150ustar00rootroot00000000000000--- - name: Manage AWX Container Images block: - name: Export Docker awx image if it isn't local and there isn't a registry defined docker_image: name: "{{ awx_image }}" tag: "{{ awx_version }}" archive_path: "{{ awx_local_base_config_path|default('/tmp') }}/{{ awx_image }}_{{ awx_version }}.tar" when: inventory_hostname != "localhost" and docker_registry is not defined delegate_to: localhost - name: Set docker base path set_fact: docker_deploy_base_path: "{{ awx_base_path|default('/tmp') }}/docker_deploy" when: ansible_connection != "local" and docker_registry is not defined - name: Ensure directory exists file: path: "{{ docker_deploy_base_path }}" state: directory when: ansible_connection != "local" and docker_registry is not defined - name: Copy awx image to docker execution copy: src: "{{ awx_local_base_config_path|default('/tmp') }}/{{ awx_image }}_{{ awx_version }}.tar" dest: "{{ docker_deploy_base_path }}/{{ awx_image }}_{{ awx_version }}.tar" when: ansible_connection != "local" and docker_registry is not defined - name: Load awx image docker_image: name: "{{ awx_image }}" tag: "{{ awx_version }}" load_path: "{{ docker_deploy_base_path }}/{{ awx_image }}_{{ awx_version }}.tar" timeout: 300 when: ansible_connection != "local" and docker_registry is not defined - name: Set full image path for local install set_fact: awx_docker_actual_image: "{{ awx_image }}:{{ awx_version }}" when: docker_registry is not defined when: dockerhub_base is not defined - name: Set DockerHub Image Paths set_fact: awx_docker_actual_image: "{{ dockerhub_base }}/awx:{{ dockerhub_version }}" when: dockerhub_base is defined podman-compose-1.2.0/examples/awx17/roles/local_docker/tasks/upgrade_postgres.yml000066400000000000000000000040741463674324000302300ustar00rootroot00000000000000--- - name: Create {{ postgres_data_dir }} directory file: path: "{{ postgres_data_dir }}" state: directory - name: Get full path of postgres data dir shell: "echo {{ postgres_data_dir }}" register: fq_postgres_data_dir - name: Register temporary docker container set_fact: container_command: "docker run --rm -v '{{ fq_postgres_data_dir.stdout }}:/var/lib/postgresql' centos:8 bash -c " - name: Check for existing Postgres data (run from inside the container for access to file) shell: cmd: | {{ container_command }} "[[ -f /var/lib/postgresql/10/data/PG_VERSION ]] && echo 'exists'" register: pg_version_file ignore_errors: true - name: Record Postgres version shell: | {{ container_command }} "cat /var/lib/postgresql/10/data/PG_VERSION" register: old_pg_version when: pg_version_file is defined and pg_version_file.stdout == 'exists' - name: Determine whether to upgrade postgres set_fact: upgrade_postgres: "{{ old_pg_version.stdout == '10' }}" when: old_pg_version.changed - name: Set up new postgres paths pre-upgrade shell: | {{ container_command }} "mkdir -p /var/lib/postgresql/12/data/" when: upgrade_postgres | bool - name: Stop AWX before upgrading postgres docker_compose: project_src: "{{ docker_compose_dir }}" stopped: true when: upgrade_postgres | bool - name: Upgrade Postgres shell: | docker run --rm \ -v {{ postgres_data_dir }}/10/data:/var/lib/postgresql/10/data \ -v {{ postgres_data_dir }}/12/data:/var/lib/postgresql/12/data \ -e PGUSER={{ pg_username }} -e POSTGRES_INITDB_ARGS="-U {{ pg_username }}" \ tianon/postgres-upgrade:10-to-12 --username={{ pg_username }} when: upgrade_postgres | bool - name: Copy old pg_hba.conf shell: | {{ container_command }} "cp /var/lib/postgresql/10/data/pg_hba.conf /var/lib/postgresql/12/data/pg_hba.conf" when: upgrade_postgres | bool - name: Remove old data directory shell: | {{ container_command }} "rm -rf /var/lib/postgresql/10/data" when: - upgrade_postgres | bool - compose_start_containers|bool podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/000077500000000000000000000000001463674324000247745ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/credentials.py.j2000066400000000000000000000006271463674324000301620ustar00rootroot00000000000000DATABASES = { 'default': { 'ATOMIC_REQUESTS': True, 'ENGINE': 'django.db.backends.postgresql', 'NAME': "{{ pg_database }}", 'USER': "{{ pg_username }}", 'PASSWORD': "{{ pg_password }}", 'HOST': "{{ pg_hostname | default('postgres') }}", 'PORT': "{{ pg_port }}", } } BROADCAST_WEBSOCKET_SECRET = "{{ broadcast_websocket_secret | b64encode }}" podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/docker-compose.yml.j2000066400000000000000000000174541463674324000307560ustar00rootroot00000000000000#jinja2: lstrip_blocks: True version: '2' services: web: image: {{ awx_docker_actual_image }} container_name: awx_web depends_on: - redis {% if pg_hostname is not defined %} - postgres {% endif %} {% if (host_port is defined) or (host_port_ssl is defined) %} ports: {% if (host_port_ssl is defined) and (ssl_certificate is defined) %} - "{{ host_port_ssl }}:8053" {% endif %} {% if host_port is defined %} - "{{ host_port }}:8052" {% endif %} {% endif %} hostname: {{ awx_web_hostname }} user: root restart: unless-stopped {% if (awx_web_container_labels is defined) and (',' in awx_web_container_labels) %} {% set awx_web_container_labels_list = awx_web_container_labels.split(',') %} labels: {% for awx_web_container_label in awx_web_container_labels_list %} - {{ awx_web_container_label }} {% endfor %} {% elif awx_web_container_labels is defined %} labels: - {{ awx_web_container_labels }} {% endif %} volumes: - supervisor-socket:/var/run/supervisor - rsyslog-socket:/var/run/awx-rsyslog/ - rsyslog-config:/var/lib/awx/rsyslog/ - "{{ docker_compose_dir }}/SECRET_KEY:/etc/tower/SECRET_KEY" - "{{ docker_compose_dir }}/environment.sh:/etc/tower/conf.d/environment.sh" - "{{ docker_compose_dir }}/credentials.py:/etc/tower/conf.d/credentials.py" - "{{ docker_compose_dir }}/nginx.conf:/etc/nginx/nginx.conf:ro" - "{{ docker_compose_dir }}/redis_socket:/var/run/redis/:rw" {% if project_data_dir is defined %} - "{{ project_data_dir +':/var/lib/awx/projects:rw' }}" {% endif %} {% if custom_venv_dir is defined %} - "{{ custom_venv_dir +':'+ custom_venv_dir +':rw' }}" {% endif %} {% if ca_trust_dir is defined %} - "{{ ca_trust_dir +':/etc/pki/ca-trust/source/anchors:ro' }}" {% endif %} {% if (ssl_certificate is defined) and (ssl_certificate_key is defined) %} - "{{ ssl_certificate +':/etc/nginx/awxweb.pem:ro' }}" - "{{ ssl_certificate_key +':/etc/nginx/awxweb_key.pem:ro' }}" {% elif (ssl_certificate is defined) and (ssl_certificate_key is not defined) %} - "{{ ssl_certificate +':/etc/nginx/awxweb.pem:ro' }}" {% endif %} {% if (awx_container_search_domains is defined) and (',' in awx_container_search_domains) %} {% set awx_container_search_domains_list = awx_container_search_domains.split(',') %} dns_search: {% for awx_container_search_domain in awx_container_search_domains_list %} - {{ awx_container_search_domain }} {% endfor %} {% elif awx_container_search_domains is defined %} dns_search: "{{ awx_container_search_domains }}" {% endif %} {% if (awx_alternate_dns_servers is defined) and (',' in awx_alternate_dns_servers) %} {% set awx_alternate_dns_servers_list = awx_alternate_dns_servers.split(',') %} dns: {% for awx_alternate_dns_server in awx_alternate_dns_servers_list %} - {{ awx_alternate_dns_server }} {% endfor %} {% elif awx_alternate_dns_servers is defined %} dns: "{{ awx_alternate_dns_servers }}" {% endif %} {% if (docker_compose_extra_hosts is defined) and (':' in docker_compose_extra_hosts) %} {% set docker_compose_extra_hosts_list = docker_compose_extra_hosts.split(',') %} extra_hosts: {% for docker_compose_extra_host in docker_compose_extra_hosts_list %} - "{{ docker_compose_extra_host }}" {% endfor %} {% endif %} environment: http_proxy: {{ http_proxy | default('') }} https_proxy: {{ https_proxy | default('') }} no_proxy: {{ no_proxy | default('') }} {% if docker_logger is defined %} logging: driver: {{ docker_logger }} {% endif %} task: image: {{ awx_docker_actual_image }} container_name: awx_task depends_on: - redis - web {% if pg_hostname is not defined %} - postgres {% endif %} command: /usr/bin/launch_awx_task.sh hostname: {{ awx_task_hostname }} user: root restart: unless-stopped volumes: - supervisor-socket:/var/run/supervisor - rsyslog-socket:/var/run/awx-rsyslog/ - rsyslog-config:/var/lib/awx/rsyslog/ - "{{ docker_compose_dir }}/SECRET_KEY:/etc/tower/SECRET_KEY" - "{{ docker_compose_dir }}/environment.sh:/etc/tower/conf.d/environment.sh" - "{{ docker_compose_dir }}/credentials.py:/etc/tower/conf.d/credentials.py" - "{{ docker_compose_dir }}/redis_socket:/var/run/redis/:rw" {% if project_data_dir is defined %} - "{{ project_data_dir +':/var/lib/awx/projects:rw' }}" {% endif %} {% if custom_venv_dir is defined %} - "{{ custom_venv_dir +':'+ custom_venv_dir +':rw' }}" {% endif %} {% if ca_trust_dir is defined %} - "{{ ca_trust_dir +':/etc/pki/ca-trust/source/anchors:ro' }}" {% endif %} {% if ssl_certificate is defined %} - "{{ ssl_certificate +':/etc/nginx/awxweb.pem:ro' }}" {% endif %} {% if (awx_container_search_domains is defined) and (',' in awx_container_search_domains) %} {% set awx_container_search_domains_list = awx_container_search_domains.split(',') %} dns_search: {% for awx_container_search_domain in awx_container_search_domains_list %} - {{ awx_container_search_domain }} {% endfor %} {% elif awx_container_search_domains is defined %} dns_search: "{{ awx_container_search_domains }}" {% endif %} {% if (awx_alternate_dns_servers is defined) and (',' in awx_alternate_dns_servers) %} {% set awx_alternate_dns_servers_list = awx_alternate_dns_servers.split(',') %} dns: {% for awx_alternate_dns_server in awx_alternate_dns_servers_list %} - {{ awx_alternate_dns_server }} {% endfor %} {% elif awx_alternate_dns_servers is defined %} dns: "{{ awx_alternate_dns_servers }}" {% endif %} {% if (docker_compose_extra_hosts is defined) and (':' in docker_compose_extra_hosts) %} {% set docker_compose_extra_hosts_list = docker_compose_extra_hosts.split(',') %} extra_hosts: {% for docker_compose_extra_host in docker_compose_extra_hosts_list %} - "{{ docker_compose_extra_host }}" {% endfor %} {% endif %} environment: AWX_SKIP_MIGRATIONS: "1" http_proxy: {{ http_proxy | default('') }} https_proxy: {{ https_proxy | default('') }} no_proxy: {{ no_proxy | default('') }} SUPERVISOR_WEB_CONFIG_PATH: '/etc/supervisord.conf' redis: image: {{ redis_image }} container_name: awx_redis restart: unless-stopped environment: http_proxy: {{ http_proxy | default('') }} https_proxy: {{ https_proxy | default('') }} no_proxy: {{ no_proxy | default('') }} command: ["/usr/local/etc/redis/redis.conf"] volumes: - "{{ docker_compose_dir }}/redis.conf:/usr/local/etc/redis/redis.conf:ro" - "{{ docker_compose_dir }}/redis_socket:/var/run/redis/:rw" {% if docker_logger is defined %} logging: driver: {{ docker_logger }} {% endif %} {% if pg_hostname is not defined %} postgres: image: {{ postgresql_image }} container_name: awx_postgres restart: unless-stopped volumes: - "{{ postgres_data_dir }}/12/data/:/var/lib/postgresql/data:Z" environment: POSTGRES_USER: {{ pg_username }} POSTGRES_PASSWORD: {{ pg_password }} POSTGRES_DB: {{ pg_database }} http_proxy: {{ http_proxy | default('') }} https_proxy: {{ https_proxy | default('') }} no_proxy: {{ no_proxy | default('') }} {% if docker_logger is defined %} logging: driver: {{ docker_logger }} {% endif %} {% endif %} {% if docker_compose_subnet is defined %} networks: default: driver: bridge ipam: driver: default config: - subnet: {{ docker_compose_subnet }} {% endif %} volumes: supervisor-socket: rsyslog-socket: rsyslog-config: podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/environment.sh.j2000066400000000000000000000006611463674324000302110ustar00rootroot00000000000000DATABASE_USER={{ pg_username|quote }} DATABASE_NAME={{ pg_database|quote }} DATABASE_HOST={{ pg_hostname|default('postgres')|quote }} DATABASE_PORT={{ pg_port|default('5432')|quote }} DATABASE_PASSWORD={{ pg_password|default('awxpass')|quote }} {% if pg_admin_password is defined %} DATABASE_ADMIN_PASSWORD={{ pg_admin_password|quote }} {% endif %} AWX_ADMIN_USER={{ admin_user|quote }} AWX_ADMIN_PASSWORD={{ admin_password|quote }} podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/nginx.conf.j2000066400000000000000000000072731463674324000273110ustar00rootroot00000000000000#user awx; worker_processes 1; pid /tmp/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; server_tokens off; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /dev/stdout main; map $http_upgrade $connection_upgrade { default upgrade; '' close; } sendfile on; #tcp_nopush on; #gzip on; upstream uwsgi { server 127.0.0.1:8050; } upstream daphne { server 127.0.0.1:8051; } {% if ssl_certificate is defined %} server { listen 8052 default_server; server_name _; # Redirect all HTTP links to the matching HTTPS page return 301 https://$host$request_uri; } {%endif %} server { {% if (ssl_certificate is defined) and (ssl_certificate_key is defined) %} listen 8053 ssl; ssl_certificate /etc/nginx/awxweb.pem; ssl_certificate_key /etc/nginx/awxweb_key.pem; {% elif (ssl_certificate is defined) and (ssl_certificate_key is not defined) %} listen 8053 ssl; ssl_certificate /etc/nginx/awxweb.pem; ssl_certificate_key /etc/nginx/awxweb.pem; {% else %} listen 8052 default_server; {% endif %} # If you have a domain name, this is where to add it server_name _; keepalive_timeout 65; # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) add_header Strict-Transport-Security max-age=15768000; # Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009) add_header X-Frame-Options "DENY"; location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; } location /static/ { alias /var/lib/awx/public/static/; } location /favicon.ico { alias /var/lib/awx/public/static/favicon.ico; } location /websocket { # Pass request to the upstream alias proxy_pass http://daphne; # Require http version 1.1 to allow for upgrade requests proxy_http_version 1.1; # We want proxy_buffering off for proxying to websockets. proxy_buffering off; # http://en.wikipedia.org/wiki/X-Forwarded-For proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # enable this if you use HTTPS: proxy_set_header X-Forwarded-Proto https; # pass the Host: header from the client for the sake of redirects proxy_set_header Host $http_host; # We've set the Host header, so we don't need Nginx to muddle # about with redirects proxy_redirect off; # Depending on the request value, set the Upgrade and # connection headers proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } location / { # Add trailing / if missing rewrite ^(.*)$http_host(.*[^/])$ $1$http_host$2/ permanent; uwsgi_read_timeout 120s; uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; {%- if extra_nginx_include is defined %} include {{ extra_nginx_include }}; {%- endif %} proxy_set_header X-Forwarded-Port 443; uwsgi_param HTTP_X_FORWARDED_PORT 443; } } } podman-compose-1.2.0/examples/awx17/roles/local_docker/templates/redis.conf.j2000066400000000000000000000001161463674324000272610ustar00rootroot00000000000000unixsocket /var/run/redis/redis.sock unixsocketperm 660 port 0 bind 127.0.0.1 podman-compose-1.2.0/examples/awx3/000077500000000000000000000000001463674324000171445ustar00rootroot00000000000000podman-compose-1.2.0/examples/awx3/docker-compose.yml000066400000000000000000000027131463674324000226040ustar00rootroot00000000000000version: '3' services: postgres: image: "postgres:9.6" environment: POSTGRES_USER: awx POSTGRES_PASSWORD: awxpass POSTGRES_DB: awx rabbitmq: image: "rabbitmq:3" environment: RABBITMQ_DEFAULT_VHOST: awx memcached: image: "memcached:alpine" awx_web: # image: "geerlingguy/awx_web:latest" image: "ansible/awx_web:3.0.1" links: - rabbitmq - memcached - postgres ports: - "8080:8052" hostname: awxweb user: root environment: SECRET_KEY: aabbcc DATABASE_USER: awx DATABASE_PASSWORD: awxpass DATABASE_NAME: awx DATABASE_PORT: 5432 DATABASE_HOST: postgres RABBITMQ_USER: guest RABBITMQ_PASSWORD: guest RABBITMQ_HOST: rabbitmq RABBITMQ_PORT: 5672 RABBITMQ_VHOST: awx MEMCACHED_HOST: memcached MEMCACHED_PORT: 11211 awx_task: # image: "geerlingguy/awx_task:latest" image: "ansible/awx_task:3.0.1" links: - rabbitmq - memcached - awx_web:awxweb - postgres hostname: awx user: root environment: SECRET_KEY: aabbcc DATABASE_USER: awx DATABASE_PASSWORD: awxpass DATABASE_NAME: awx DATABASE_PORT: 5432 DATABASE_HOST: postgres RABBITMQ_USER: guest RABBITMQ_PASSWORD: guest RABBITMQ_HOST: rabbitmq RABBITMQ_PORT: 5672 RABBITMQ_VHOST: awx MEMCACHED_HOST: memcached MEMCACHED_PORT: 11211 podman-compose-1.2.0/examples/azure-vote/000077500000000000000000000000001463674324000203635ustar00rootroot00000000000000podman-compose-1.2.0/examples/azure-vote/README.md000066400000000000000000000005261463674324000216450ustar00rootroot00000000000000# Azure Vote Example This example have two containers: * backend: `redis` used as storage * frontend: having supervisord, nginx, uwsgi/python ``` echo "HOST_PORT=8080" > .env podman-compose up ``` after typing the commands above open your browser on the host port you picked above like [http://localhost:8080/](http://localhost:8080/) podman-compose-1.2.0/examples/azure-vote/docker-compose.yaml000066400000000000000000000007041463674324000241620ustar00rootroot00000000000000--- # from https://github.com/Azure-Samples/azure-voting-app-redis/blob/master/docker-compose.yaml version: '3' services: azure-vote-back: image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 container_name: azure-vote-back environment: ALLOW_EMPTY_PASSWORD: "yes" azure-vote-front: image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 environment: REDIS: azure-vote-back ports: - "${HOST_PORT:-8080}:80" podman-compose-1.2.0/examples/busybox/000077500000000000000000000000001463674324000177555ustar00rootroot00000000000000podman-compose-1.2.0/examples/busybox/docker-compose.yaml000066400000000000000000000013111463674324000235470ustar00rootroot00000000000000version: "2" services: redis: image: redis:alpine ports: - "6379" environment: - SECRET_KEY=aabbcc - ENV_IS_SET frontend: image: busybox #entrypoint: [] command: ["/bin/busybox", "httpd", "-f", "-p", "8080"] working_dir: / environment: SECRET_KEY2: aabbcc ENV_IS_SET2: ports: - "8080" links: - redis:myredis labels: my.label: my_value #tmpfs: /run #tmpfs: # - /run # - /tmp #user: postgresql #working_dir: /code #domainname: foo.com #hostname: foo #ipc: host #mac_address: 02:42:ac:11:65:43 #privileged: true #read_only: true #shm_size: 64M #stdin_open: true #tty: true podman-compose-1.2.0/examples/echo/000077500000000000000000000000001463674324000172005ustar00rootroot00000000000000podman-compose-1.2.0/examples/echo/README.md000066400000000000000000000007571463674324000204700ustar00rootroot00000000000000# Echo Service example ``` podman-compose up ``` Test the service with `curl like this` ``` $ curl -X POST -d "foobar" http://localhost:8080/; echo CLIENT VALUES: client_address=10.89.31.2 command=POST real path=/ query=nil request_version=1.1 request_uri=http://localhost:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* content-length=6 content-type=application/x-www-form-urlencoded host=localhost:8080 user-agent=curl/7.76.1 BODY: foobar ``` podman-compose-1.2.0/examples/echo/docker-compose.yaml000066400000000000000000000001671463674324000230020ustar00rootroot00000000000000--- version: '3' services: web: image: k8s.gcr.io/echoserver:1.4 ports: - "${HOST_PORT:-8080}:8080" podman-compose-1.2.0/examples/hello-app-redis/000077500000000000000000000000001463674324000212475ustar00rootroot00000000000000podman-compose-1.2.0/examples/hello-app-redis/README.md000066400000000000000000000005741463674324000225340ustar00rootroot00000000000000# GCR Hello App Redis A 6-node redis cluster using [Bitnami](https://github.com/bitnami/bitnami-docker-redis-cluster) with a [simple hit counter](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/main/hello-app-redis) that persists on that redis cluster ``` podman-compose up ``` then open your browser on [http://localhost:8080/](http://localhost:8080/) podman-compose-1.2.0/examples/hello-app-redis/docker-compose.yaml000066400000000000000000000037531463674324000250550ustar00rootroot00000000000000--- version: '3' volumes: redis-node1-data: redis-node2-data: redis-node3-data: redis-node4-data: redis-node5-data: redis-data: services: web: image: gcr.io/google-samples/hello-app-redis:1.0 depends_on: - redis-cluster ports: - "${HOST_PORT:-8080}:8080" redis-node1: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-node1-data:/bitnami/redis/data environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster redis-node2: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-node2-data:/bitnami/redis/data environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster redis-node3: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-node3-data:/bitnami/redis/data environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster redis-node4: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-node4-data:/bitnami/redis/data environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster redis-node5: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-node5-data:/bitnami/redis/data environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster redis-cluster: image: docker.io/bitnami/redis-cluster:6.2 volumes: - redis-data:/bitnami/redis/data depends_on: - redis-node1 - redis-node2 - redis-node3 - redis-node4 - redis-node5 environment: - ALLOW_EMPTY_PASSWORD=yes - REDIS_NODES=redis-node1 redis-node2 redis-node3 redis-node4 redis-node5 redis-cluster - REDIS_CLUSTER_CREATOR=yes podman-compose-1.2.0/examples/hello-app/000077500000000000000000000000001463674324000201435ustar00rootroot00000000000000podman-compose-1.2.0/examples/hello-app/README.md000066400000000000000000000002221463674324000214160ustar00rootroot00000000000000# GCR Hello App A small ~2MB image, type ``` podman-compose up ``` then open your browser on [http://localhost:8080/](http://localhost:8080/) podman-compose-1.2.0/examples/hello-app/docker-compose.yaml000066400000000000000000000002011463674324000237320ustar00rootroot00000000000000--- version: '3' services: web: image: gcr.io/google-samples/hello-app:1.0 ports: - "${HOST_PORT:-8080}:8080" podman-compose-1.2.0/examples/hello-python/000077500000000000000000000000001463674324000207045ustar00rootroot00000000000000podman-compose-1.2.0/examples/hello-python/Dockerfile000066400000000000000000000002641463674324000227000ustar00rootroot00000000000000FROM python:3.9-alpine WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [ "python", "-m", "app.web" ] EXPOSE 8080 podman-compose-1.2.0/examples/hello-python/README.md000066400000000000000000000001721463674324000221630ustar00rootroot00000000000000# Simple Python Demo ## A Redis counter ``` podman-compose up -d curl localhost:8080/ curl localhost:8080/hello.json ``` podman-compose-1.2.0/examples/hello-python/app/000077500000000000000000000000001463674324000214645ustar00rootroot00000000000000podman-compose-1.2.0/examples/hello-python/app/__init__.py000066400000000000000000000000001463674324000235630ustar00rootroot00000000000000podman-compose-1.2.0/examples/hello-python/app/web.py000066400000000000000000000016331463674324000226160ustar00rootroot00000000000000# pylint: disable=import-error # pylint: disable=unused-import import asyncio # noqa: F401 import os import aioredis from aiohttp import web REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379")) REDIS_DB = int(os.environ.get("REDIS_DB", "0")) redis = aioredis.from_url(f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}") app = web.Application() routes = web.RouteTableDef() @routes.get("/") async def hello(request): # pylint: disable=unused-argument counter = await redis.incr("mycounter") return web.Response(text=f"counter={counter}") @routes.get("/hello.json") async def hello_json(request): # pylint: disable=unused-argument counter = await redis.incr("mycounter") data = {"counter": counter} return web.json_response(data) app.add_routes(routes) def main(): web.run_app(app, port=8080) if __name__ == "__main__": main() podman-compose-1.2.0/examples/hello-python/docker-compose.yaml000066400000000000000000000005711463674324000245050ustar00rootroot00000000000000--- version: '3' volumes: redis: services: redis: read_only: true image: docker.io/redis:alpine command: ["redis-server", "--appendonly", "yes", "--notify-keyspace-events", "Ex"] volumes: - redis:/data web: read_only: true build: context: . image: hello-py-aioweb ports: - 8080:8080 environment: REDIS_HOST: redis podman-compose-1.2.0/examples/hello-python/requirements.txt000066400000000000000000000000451463674324000241670ustar00rootroot00000000000000aiohttp aioredis # aioredis[hiredis] podman-compose-1.2.0/examples/nodeproj/000077500000000000000000000000001463674324000201025ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/.eslintrc.json000066400000000000000000000030621463674324000226770ustar00rootroot00000000000000{ "env": { "node": true, "es6": true }, "settings": { "import/resolver": { "node": { "extensions": [".js", ".mjs", ".ts", ".cjs"] } } }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2020, "sourceType": "module", "allowImportExportEverywhere": true }, "extends": [ "eslint:recommended", "plugin:import/errors", "plugin:import/warnings", "plugin:import/typescript", "plugin:promise/recommended", "google", "plugin:security/recommended" ], "plugins": ["promise", "security", "import"], "overrides": [ { "files": "public/**/*.min.js", "env": { "browser": true, "node": false, "es6": false }, "parserOptions": { "sourceType": "script" }, "extends": ["plugin:compat/recommended"], "plugins": [], "rules": { "no-var": ["off"] } } ], "rules": { "security/detect-non-literal-fs-filename":["off"], "security/detect-object-injection":["off"], "camelcase": ["off"], "no-console": ["off"], "require-jsdoc": ["off"], "one-var": ["off"], "guard-for-in": ["off"], "max-len": [ "warn", { "ignoreComments": true, "ignoreTrailingComments": true, "ignoreUrls": true, "code": 200 } ], "indent": ["warn", 4], "no-unused-vars": ["warn"], "no-extra-semi": ["warn"], "linebreak-style": ["error", "unix"], "quotes": ["warn", "double"], "semi": ["error", "always"] } } podman-compose-1.2.0/examples/nodeproj/.gitignore000066400000000000000000000000431463674324000220670ustar00rootroot00000000000000local.env .env *.pid node_modules podman-compose-1.2.0/examples/nodeproj/.home/000077500000000000000000000000001463674324000211105ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/.home/.gitignore000066400000000000000000000000021463674324000230700ustar00rootroot00000000000000* podman-compose-1.2.0/examples/nodeproj/README.md000066400000000000000000000003321463674324000213570ustar00rootroot00000000000000# How to run example ``` cp example.local.env local.env cp example.env .env cat local.env cat .env echo "UID=$UID" >> .env cat .env podman-compose build podman-compose run --rm --no-deps init podman-compose up ``` podman-compose-1.2.0/examples/nodeproj/containers/000077500000000000000000000000001463674324000222475ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/containers/node16-runtime/000077500000000000000000000000001463674324000250245ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/containers/node16-runtime/Dockerfile000066400000000000000000000010641463674324000270170ustar00rootroot00000000000000FROM registry.fedoraproject.org/fedora-minimal:35 ARG NODE_VER=16 # microdnf -y module enable nodejs:${NODE_VER} RUN \ echo -e "[nodejs]\nname=nodejs\nstream=${NODE_VER}\nprofiles=\nstate=enabled\n" > /etc/dnf/modules.d/nodejs.module && \ microdnf -y install shadow-utils nodejs zopfli findutils busybox && \ microdnf clean all RUN adduser -d /app app && mkdir -p /app/code/.home && chown app:app -R /app/code && chmod 711 /app /app/code/.home && usermod -d /app/code/.home app ENV XDG_CONFIG_HOME=/app/code/.home ENV HOME=/app/code/.home WORKDIR /app/code podman-compose-1.2.0/examples/nodeproj/docker-compose.yml000066400000000000000000000017441463674324000235450ustar00rootroot00000000000000version: '3' volumes: redis: services: redis: read_only: true image: docker.io/redis:alpine command: ["redis-server", "--appendonly", "yes", "--notify-keyspace-events", "Ex"] volumes: - redis:/data tmpfs: - /tmp - /var/run - /run init: read_only: true #userns_mode: keep-id user: ${UID:-1000} build: context: ./containers/${NODE_IMG:-node16-runtime} image: ${NODE_IMG:-node16-runtime} env_file: - local.env volumes: - .:/app/code command: ["/bin/sh", "-c", "mkdir -p ~/; [ -d ./node_modules ] && echo '** node_modules exists' || npm install"] tmpfs: - /tmp - /run task: extends: service: init command: ["npm", "run", "cli", "--", "task"] links: - redis depends_on: - redis web: extends: service: init command: ["npm", "run", "cli", "--", "web"] ports: - ${WEB_LISTEN_PORT:-3000}:3000 depends_on: - redis links: - mongo podman-compose-1.2.0/examples/nodeproj/example.env000066400000000000000000000000601463674324000222430ustar00rootroot00000000000000WEB_LISTEN_PORT=3000 # pass UID= your IDE user podman-compose-1.2.0/examples/nodeproj/example.local.env000066400000000000000000000000221463674324000233320ustar00rootroot00000000000000REDIS_HOST=redis podman-compose-1.2.0/examples/nodeproj/index.js000066400000000000000000000001131463674324000215420ustar00rootroot00000000000000#! /usr/bin/env node "use strict"; import {start} from "./lib"; start(); podman-compose-1.2.0/examples/nodeproj/jsconfig.json000066400000000000000000000004021463674324000225730ustar00rootroot00000000000000{ "compilerOptions": { "target": "es2020", "module": "es2020", "moduleResolution": "node", "allowSyntheticDefaultImports": true }, "files": [ "index.js" ], "include": [ "lib/**/*.js" ] }podman-compose-1.2.0/examples/nodeproj/lib/000077500000000000000000000000001463674324000206505ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/lib/commands/000077500000000000000000000000001463674324000224515ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/lib/commands/task.js000066400000000000000000000013061463674324000237510ustar00rootroot00000000000000"use strict"; import {proj} from "../proj"; async function loop() { const poped = await proj.predis.blpop("queue", 5); const task_desc_s = poped[1]; let task_desc; try { task_desc = JSON.parse(task_desc_s); } catch (e) { console.exception(e); } console.info("got task "+task_desc.func); const func = task_desc.func; const args = task_desc.args; if (typeof(proj.tasks[func])!="function") { console.log(`task ${func} not found`); process.exit(-1) } try { await ((this.tasks[func])(...args)); } catch (e) { console.exception(e); } } export async function start() { while(true) { loop(); } } podman-compose-1.2.0/examples/nodeproj/lib/commands/web.js000066400000000000000000000010261463674324000235630ustar00rootroot00000000000000"use strict"; import {proj} from "../proj"; import http from "http"; import express from "express"; export async function start() { const app = express(); const server = http.createServer(app); // Routing app.use(express.static(proj.config.basedir + "/public")); app.get("/healthz", function(req, res) { res.send("ok@"+Date.now()); }); server.listen(proj.config.LISTEN_PORT, proj.config.LISTEN_HOST, function() { console.warn(`listening at port ${proj.config.LISTEN_PORT}`); }); } podman-compose-1.2.0/examples/nodeproj/package.json000066400000000000000000000007561463674324000224000ustar00rootroot00000000000000{ "name": "nodeproj", "version": "0.0.1", "description": "nodejs example project", "exports": { ".": "./index.js", "./lib": "./lib" }, "main": "index.js", "type": "module", "scripts": { "cli": "nodemon -w lib -w index.js --es-module-specifier-resolution=node ./index.js" }, "dependencies": { "express": "~4.16.4", "redis": "^3.1.2" }, "private": true, "author": "", "license": "proprietary", "devDependencies": { "nodemon": "^2.0.14" } } podman-compose-1.2.0/examples/nodeproj/public/000077500000000000000000000000001463674324000213605ustar00rootroot00000000000000podman-compose-1.2.0/examples/nodeproj/public/index.html000066400000000000000000000006271463674324000233620ustar00rootroot00000000000000 Vote

This is a Heading

This is a paragraph.

podman-compose-1.2.0/examples/nvidia-smi/000077500000000000000000000000001463674324000203225ustar00rootroot00000000000000podman-compose-1.2.0/examples/nvidia-smi/docker-compose.yaml000066400000000000000000000003651463674324000241240ustar00rootroot00000000000000services: test: image: nvidia/cuda:12.3.1-base-ubuntu20.04 command: nvidia-smi deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] podman-compose-1.2.0/examples/wordpress/000077500000000000000000000000001463674324000203125ustar00rootroot00000000000000podman-compose-1.2.0/examples/wordpress/docker-compose.yaml000066400000000000000000000011301463674324000241030ustar00rootroot00000000000000--- volumes: db_data: services: wordpress: image: docker.io/library/wordpress:latest ports: - 8080:80 environment: - WORDPRESS_DB_HOST=db - WORDPRESS_DB_USER=wordpress - WORDPRESS_DB_PASSWORD=password - WORDPRESS_DB_NAME=wordpress db: image: docker.io/library/mariadb:10.6.4-focal command: '--default-authentication-plugin=mysql_native_password' volumes: - db_data:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=somewordpress - MYSQL_DATABASE=wordpress - MYSQL_USER=wordpress - MYSQL_PASSWORD=password podman-compose-1.2.0/newsfragments/000077500000000000000000000000001463674324000173275ustar00rootroot00000000000000podman-compose-1.2.0/newsfragments/README.txt000066400000000000000000000011101463674324000210160ustar00rootroot00000000000000This is the directory for news fragments used by towncrier: https://github.com/hawkowl/towncrier You create a news fragment in this directory when you make a change, and the file gets removed from this directory when the news is published. towncrier has a few standard types of news fragments, signified by the file extension. These are: .feature: Signifying a new feature. .bugfix: Signifying a bug fix. .doc: Signifying a documentation improvement. .removal: Signifying a deprecation or removal of public API. .change: Signifying a change of behavior .misc: Miscellaneous change podman-compose-1.2.0/podman_compose.py000077500000000000000000003644021463674324000200350ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0 # https://docs.docker.com/compose/compose-file/#service-configuration-reference # https://docs.docker.com/samples/ # https://docs.docker.com/compose/gettingstarted/ # https://docs.docker.com/compose/django/ # https://docs.docker.com/compose/wordpress/ # TODO: podman pod logs --color -n -f pod_testlogs import argparse import asyncio.subprocess import getpass import glob import hashlib import itertools import json import logging import os import random import re import shlex import signal import subprocess import sys from asyncio import Task try: from shlex import quote as cmd_quote except ImportError: from pipes import quote as cmd_quote # pylint: disable=deprecated-module # import fnmatch # fnmatch.fnmatchcase(env, "*_HOST") import yaml from dotenv import dotenv_values __version__ = "1.2.0" script = os.path.realpath(sys.argv[0]) # helper functions def is_str(string_object): return isinstance(string_object, str) def is_dict(dict_object): return isinstance(dict_object, dict) def is_list(list_object): return not is_str(list_object) and not is_dict(list_object) and hasattr(list_object, "__iter__") # identity filter def filteri(a): return filter(lambda i: i, a) def try_int(i, fallback=None): try: return int(i) except ValueError: pass except TypeError: pass return fallback def try_float(i, fallback=None): try: return float(i) except ValueError: pass except TypeError: pass return fallback log = logging.getLogger(__name__) dir_re = re.compile(r"^[~/\.]") propagation_re = re.compile( "^(?:z|Z|O|U|r?shared|r?slave|r?private|r?unbindable|r?bind|(?:no)?(?:exec|dev|suid))$" ) norm_re = re.compile("[^-_a-z0-9]") num_split_re = re.compile(r"(\d+|\D+)") PODMAN_CMDS = ( "pull", "push", "build", "inspect", "run", "start", "stop", "rm", "volume", ) t_re = re.compile(r"^(?:(\d+)[m:])?(?:(\d+(?:\.\d+)?)s?)?$") STOP_GRACE_PERIOD = "10" def str_to_seconds(txt): if not txt: return None if isinstance(txt, (int, float)): return txt match = t_re.match(txt.strip()) if not match: return None mins, sec = match[1], match[2] mins = int(mins) if mins else 0 sec = float(sec) if sec else 0 # "podman stop" takes only int # Error: invalid argument "3.0" for "-t, --time" flag: strconv.ParseUint: parsing "3.0": # invalid syntax return int(mins * 60.0 + sec) def ver_as_list(a): return [try_int(i, i) for i in num_split_re.findall(a)] def strverscmp_lt(a, b): a_ls = ver_as_list(a or "") b_ls = ver_as_list(b or "") return a_ls < b_ls def parse_short_mount(mount_str, basedir): mount_a = mount_str.split(":") mount_opt_dict = {} mount_opt = None if len(mount_a) == 1: # Anonymous: Just specify a path and let the engine creates the volume # - /var/lib/mysql mount_src, mount_dst = None, mount_str elif len(mount_a) == 2: mount_src, mount_dst = mount_a # dest must start with / like /foo:/var/lib/mysql # otherwise it's option like /var/lib/mysql:rw if not mount_dst.startswith("/"): mount_dst, mount_opt = mount_a mount_src = None elif len(mount_a) == 3: mount_src, mount_dst, mount_opt = mount_a else: raise ValueError("could not parse mount " + mount_str) if mount_src and dir_re.match(mount_src): # Specify an absolute path mapping # - /opt/data:/var/lib/mysql # Path on the host, relative to the Compose file # - ./cache:/tmp/cache # User-relative path # - ~/configs:/etc/configs/:ro mount_type = "bind" if os.name != 'nt' or (os.name == 'nt' and ".sock" not in mount_src): mount_src = os.path.abspath(os.path.join(basedir, os.path.expanduser(mount_src))) else: # Named volume # - datavolume:/var/lib/mysql mount_type = "volume" mount_opts = filteri((mount_opt or "").split(",")) propagation_opts = [] for opt in mount_opts: if opt == "ro": mount_opt_dict["read_only"] = True elif opt == "rw": mount_opt_dict["read_only"] = False elif opt in ("consistent", "delegated", "cached"): mount_opt_dict["consistency"] = opt elif propagation_re.match(opt): propagation_opts.append(opt) else: # TODO: ignore raise ValueError("unknown mount option " + opt) mount_opt_dict["bind"] = {"propagation": ",".join(propagation_opts)} return { "type": mount_type, "source": mount_src, "target": mount_dst, **mount_opt_dict, } # NOTE: if a named volume is used but not defined it # gives ERROR: Named volume "abc" is used in service "xyz" # but no declaration was found in the volumes section. # unless it's anonymous-volume def fix_mount_dict(compose, mount_dict, proj_name, srv_name): """ in-place fix mount dictionary to: - define _vol to be the corresponding top-level volume - if name is missing it would be source prefixed with project - if no source it would be generated """ # if already applied nothing todo if "_vol" in mount_dict: return mount_dict if mount_dict["type"] == "volume": vols = compose.vols source = mount_dict.get("source", None) vol = (vols.get(source, None) or {}) if source else {} name = vol.get("name", None) mount_dict["_vol"] = vol # handle anonymous or implied volume if not source: # missing source vol["name"] = "_".join([ proj_name, srv_name, hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(), ]) elif not name: external = vol.get("external", None) if isinstance(external, dict): vol["name"] = external.get("name", f"{source}") elif external: vol["name"] = f"{source}" else: vol["name"] = f"{proj_name}_{source}" return mount_dict # docker and docker-compose support subset of bash variable substitution # https://docs.docker.com/compose/compose-file/#variable-substitution # https://docs.docker.com/compose/env-file/ # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html # $VARIABLE # ${VARIABLE} # ${VARIABLE:-default} default if not set or empty # ${VARIABLE-default} default if not set # ${VARIABLE:?err} raise error if not set or empty # ${VARIABLE?err} raise error if not set # $$ means $ var_re = re.compile( r""" \$(?: (?P\$) | (?P[_a-zA-Z][_a-zA-Z0-9]*) | (?:{ (?P[_a-zA-Z][_a-zA-Z0-9]*) (?:(?P:)?(?: (?:-(?P[^}]*)) | (?:\?(?P[^}]*)) ))? }) ) """, re.VERBOSE, ) def rec_subs(value, subs_dict): """ do bash-like substitution in value and if list of dictionary do that recursively """ if is_dict(value): value = {k: rec_subs(v, subs_dict) for k, v in value.items()} elif is_str(value): def convert(m): if m.group("escaped") is not None: return "$" name = m.group("named") or m.group("braced") value = subs_dict.get(name) if value == "" and m.group("empty"): value = None if value is not None: return str(value) if m.group("err") is not None: raise RuntimeError(m.group("err")) return m.group("default") or "" value = var_re.sub(convert, value) elif hasattr(value, "__iter__"): value = [rec_subs(i, subs_dict) for i in value] return value def norm_as_list(src): """ given a dictionary {key1:value1, key2: None} or list return a list of ["key1=value1", "key2"] """ if src is None: dst = [] elif is_dict(src): dst = [(f"{k}={v}" if v is not None else k) for k, v in src.items()] elif is_list(src): dst = list(src) else: dst = [src] return dst def norm_as_dict(src): """ given a list ["key1=value1", "key2"] return a dictionary {key1:value1, key2: None} """ if src is None: dst = {} elif is_dict(src): dst = dict(src) elif is_list(src): dst = [i.split("=", 1) for i in src if i] dst = [(a if len(a) == 2 else (a[0], None)) for a in dst] dst = dict(dst) elif is_str(src): key, value = src.split("=", 1) if "=" in src else (src, None) dst = {key: value} else: raise ValueError("dictionary or iterable is expected") return dst def norm_ulimit(inner_value): if is_dict(inner_value): if not inner_value.keys() & {"soft", "hard"}: raise ValueError("expected at least one soft or hard limit") soft = inner_value.get("soft", inner_value.get("hard", None)) hard = inner_value.get("hard", inner_value.get("soft", None)) return f"{soft}:{hard}" if is_list(inner_value): return norm_ulimit(norm_as_dict(inner_value)) # if int or string return as is return inner_value # def tr_identity(project_name, given_containers): # pod_name = f'pod_{project_name}' # pod = dict(name=pod_name) # containers = [] # for cnt in given_containers: # containers.append(dict(cnt, pod=pod_name)) # return [pod], containers def transform(args, project_name, given_containers): if not args.in_pod_bool: pod_name = None pods = [] else: pod_name = f"pod_{project_name}" pod = {"name": pod_name} pods = [pod] containers = [] for cnt in given_containers: containers.append(dict(cnt, pod=pod_name)) return pods, containers async def assert_volume(compose, mount_dict): """ inspect volume to get directory create volume if needed """ vol = mount_dict.get("_vol", None) if mount_dict["type"] == "bind": basedir = os.path.realpath(compose.dirname) mount_src = mount_dict["source"] mount_src = os.path.realpath(os.path.join(basedir, os.path.expanduser(mount_src))) if not os.path.exists(mount_src): try: os.makedirs(mount_src, exist_ok=True) except OSError: pass return if mount_dict["type"] != "volume" or not vol or not vol.get("name", None): return proj_name = compose.project_name vol_name = vol["name"] is_ext = vol.get("external", None) log.debug("podman volume inspect %s || podman volume create %s", vol_name, vol_name) # TODO: might move to using "volume list" # podman volume list --format '{{.Name}}\t{{.MountPoint}}' \ # -f 'label=io.podman.compose.project=HERE' try: _ = (await compose.podman.output([], "volume", ["inspect", vol_name])).decode("utf-8") except subprocess.CalledProcessError as e: if is_ext: raise RuntimeError(f"External volume [{vol_name}] does not exists") from e labels = vol.get("labels", None) or [] args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", ] for item in norm_as_list(labels): args.extend(["--label", item]) driver = vol.get("driver", None) if driver: args.extend(["--driver", driver]) driver_opts = vol.get("driver_opts", None) or {} for opt, value in driver_opts.items(): args.extend(["--opt", f"{opt}={value}"]) args.append(vol_name) await compose.podman.output([], "volume", args) _ = (await compose.podman.output([], "volume", ["inspect", vol_name])).decode("utf-8") def mount_desc_to_mount_args(compose, mount_desc, srv_name, cnt_name): # pylint: disable=unused-argument mount_type = mount_desc.get("type", None) vol = mount_desc.get("_vol", None) if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source", None) target = mount_desc["target"] opts = [] if mount_desc.get(mount_type, None): # TODO: we might need to add mount_dict[mount_type]["propagation"] = "z" mount_prop = mount_desc.get(mount_type, {}).get("propagation", None) if mount_prop: opts.append(f"{mount_type}-propagation={mount_prop}") if mount_desc.get("read_only", False): opts.append("ro") if mount_type == "tmpfs": tmpfs_opts = mount_desc.get("tmpfs", {}) tmpfs_size = tmpfs_opts.get("size", None) if tmpfs_size: opts.append(f"tmpfs-size={tmpfs_size}") tmpfs_mode = tmpfs_opts.get("mode", None) if tmpfs_mode: opts.append(f"tmpfs-mode={tmpfs_mode}") if mount_type == "bind": bind_opts = mount_desc.get("bind", {}) selinux = bind_opts.get("selinux", None) if selinux is not None: opts.append(selinux) opts = ",".join(opts) if mount_type == "bind": return f"type=bind,source={source},destination={target},{opts}".rstrip(",") if mount_type == "volume": return f"type=volume,source={source},destination={target},{opts}".rstrip(",") if mount_type == "tmpfs": return f"type=tmpfs,destination={target},{opts}".rstrip(",") raise ValueError("unknown mount type:" + mount_type) def ulimit_to_ulimit_args(ulimit, podman_args): if ulimit is not None: # ulimit can be a single value, i.e. ulimit: host if is_str(ulimit): podman_args.extend(["--ulimit", ulimit]) # or a dictionary or list: else: ulimit = norm_as_dict(ulimit) ulimit = [ "{}={}".format(ulimit_key, norm_ulimit(inner_value)) for ulimit_key, inner_value in ulimit.items() ] for i in ulimit: podman_args.extend(["--ulimit", i]) def container_to_ulimit_args(cnt, podman_args): ulimit_to_ulimit_args(cnt.get("ulimits", []), podman_args) def container_to_ulimit_build_args(cnt, podman_args): build = cnt.get("build", None) if build is not None: ulimit_to_ulimit_args(build.get("ulimits", []), podman_args) def mount_desc_to_volume_args(compose, mount_desc, srv_name, cnt_name): # pylint: disable=unused-argument mount_type = mount_desc["type"] if mount_type not in ("bind", "volume"): raise ValueError("unknown mount type:" + mount_type) vol = mount_desc.get("_vol", None) if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source", None) if not source: raise ValueError(f"missing mount source for {mount_type} on {srv_name}") target = mount_desc["target"] opts = [] propagations = set(filteri(mount_desc.get(mount_type, {}).get("propagation", "").split(","))) if mount_type != "bind": propagations.update(filteri(mount_desc.get("bind", {}).get("propagation", "").split(","))) opts.extend(propagations) # --volume, -v[=[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]] # [rw|ro] # [z|Z] # [[r]shared|[r]slave|[r]private]|[r]unbindable # [[r]bind] # [noexec|exec] # [nodev|dev] # [nosuid|suid] # [O] # [U] read_only = mount_desc.get("read_only", None) if read_only is not None: opts.append("ro" if read_only else "rw") if mount_type == "bind": bind_opts = mount_desc.get("bind", {}) selinux = bind_opts.get("selinux", None) if selinux is not None: opts.append(selinux) args = f"{source}:{target}" if opts: args += ":" + ",".join(opts) return args def get_mnt_dict(compose, cnt, volume): proj_name = compose.project_name srv_name = cnt["_service"] basedir = compose.dirname if is_str(volume): volume = parse_short_mount(volume, basedir) return fix_mount_dict(compose, volume, proj_name, srv_name) async def get_mount_args(compose, cnt, volume): volume = get_mnt_dict(compose, cnt, volume) # proj_name = compose.project_name srv_name = cnt["_service"] mount_type = volume["type"] await assert_volume(compose, volume) if compose.prefer_volume_over_mount: if mount_type == "tmpfs": # TODO: --tmpfs /tmp:rw,size=787448k,mode=1777 args = volume["target"] tmpfs_opts = volume.get("tmpfs", {}) opts = [] size = tmpfs_opts.get("size", None) if size: opts.append(f"size={size}") mode = tmpfs_opts.get("mode", None) if mode: opts.append(f"mode={mode}") if opts: args += ":" + ",".join(opts) return ["--tmpfs", args] args = mount_desc_to_volume_args(compose, volume, srv_name, cnt["name"]) return ["-v", args] args = mount_desc_to_mount_args(compose, volume, srv_name, cnt["name"]) return ["--mount", args] def get_secret_args(compose, cnt, secret, podman_is_building=False): """ podman_is_building: True if we are preparing arguments for an invocation of "podman build" False if we are preparing for something else like "podman run" """ secret_name = secret if is_str(secret) else secret.get("source", None) if not secret_name or secret_name not in compose.declared_secrets.keys(): raise ValueError(f'ERROR: undeclared secret: "{secret}", service: {cnt["_service"]}') declared_secret = compose.declared_secrets[secret_name] source_file = declared_secret.get("file", None) dest_file = "" secret_opts = "" secret_target = None if is_str(secret) else secret.get("target", None) secret_uid = None if is_str(secret) else secret.get("uid", None) secret_gid = None if is_str(secret) else secret.get("gid", None) secret_mode = None if is_str(secret) else secret.get("mode", None) secret_type = None if is_str(secret) else secret.get("type", None) if source_file: # assemble path for source file first, because we need it for all cases basedir = compose.dirname source_file = os.path.realpath(os.path.join(basedir, os.path.expanduser(source_file))) if podman_is_building: # pass file secrets to "podman build" with param --secret if not secret_target: secret_id = secret_name elif "/" in secret_target: raise ValueError( f'ERROR: Build secret "{secret_name}" has invalid target "{secret_target}". ' + "(Expected plain filename without directory as target.)" ) else: secret_id = secret_target volume_ref = ["--secret", f"id={secret_id},src={source_file}"] else: # pass file secrets to "podman run" as volumes if not secret_target: dest_file = "/run/secrets/{}".format(secret_name) elif not secret_target.startswith("/"): sec = secret_target if secret_target else secret_name dest_file = f"/run/secrets/{sec}" else: dest_file = secret_target volume_ref = ["--volume", f"{source_file}:{dest_file}:ro,rprivate,rbind"] if secret_uid or secret_gid or secret_mode: sec = secret_target if secret_target else secret_name log.warning( "WARNING: Service %s uses secret %s with uid, gid, or mode." + " These fields are not supported by this implementation of the Compose file", cnt["_service"], sec, ) return volume_ref # v3.5 and up added external flag, earlier the spec # only required a name to be specified. # docker-compose does not support external secrets outside of swarm mode. # However accessing these via podman is trivial # since these commands are directly translated to # podman-create commands, albeit we can only support a 1:1 mapping # at the moment if declared_secret.get("external", False) or declared_secret.get("name", None): secret_opts += f",uid={secret_uid}" if secret_uid else "" secret_opts += f",gid={secret_gid}" if secret_gid else "" secret_opts += f",mode={secret_mode}" if secret_mode else "" secret_opts += f",type={secret_type}" if secret_type else "" secret_opts += f",target={secret_target}" if secret_target and secret_type == "env" else "" # The target option is only valid for type=env, # which in an ideal world would work # for type=mount as well. # having a custom name for the external secret # has the same problem as well ext_name = declared_secret.get("name", None) err_str = ( 'ERROR: Custom name/target reference "{}" ' 'for mounted external secret "{}" is not supported' ) if ext_name and ext_name != secret_name: raise ValueError(err_str.format(secret_name, ext_name)) if secret_target and secret_target != secret_name and secret_type != 'env': raise ValueError(err_str.format(secret_target, secret_name)) if secret_target and secret_type != 'env': log.warning( 'WARNING: Service "%s" uses target: "%s" for secret: "%s".' + " That is un-supported and a no-op and is ignored.", cnt["_service"], secret_target, secret_name, ) return ["--secret", "{}{}".format(secret_name, secret_opts)] raise ValueError( 'ERROR: unparsable secret: "{}", service: "{}"'.format(secret_name, cnt["_service"]) ) def container_to_res_args(cnt, podman_args): container_to_cpu_res_args(cnt, podman_args) container_to_gpu_res_args(cnt, podman_args) def container_to_gpu_res_args(cnt, podman_args): # https://docs.docker.com/compose/gpu-support/ # https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html deploy = cnt.get("deploy", None) or {} res = deploy.get("resources", None) or {} reservations = res.get("reservations", None) or {} devices = reservations.get("devices", []) gpu_on = False for device in devices: driver = device.get("driver", None) if driver is None: continue capabilities = device.get("capabilities", None) if capabilities is None: continue if driver != "nvidia" or "gpu" not in capabilities: continue count = device.get("count", "all") device_ids = device.get("device_ids", "all") if device_ids != "all" and len(device_ids) > 0: for device_id in device_ids: podman_args.extend(( "--device", f"nvidia.com/gpu={device_id}", )) gpu_on = True continue if count != "all": for device_id in range(count): podman_args.extend(( "--device", f"nvidia.com/gpu={device_id}", )) gpu_on = True continue podman_args.extend(( "--device", "nvidia.com/gpu=all", )) gpu_on = True if gpu_on: podman_args.append("--security-opt=label=disable") def container_to_cpu_res_args(cnt, podman_args): # v2: https://docs.docker.com/compose/compose-file/compose-file-v2/#cpu-and-other-resources # cpus, cpu_shares, mem_limit, mem_reservation cpus_limit_v2 = try_float(cnt.get("cpus", None), None) cpu_shares_v2 = try_int(cnt.get("cpu_shares", None), None) mem_limit_v2 = cnt.get("mem_limit", None) mem_res_v2 = cnt.get("mem_reservation", None) # v3: https://docs.docker.com/compose/compose-file/compose-file-v3/#resources # spec: https://github.com/compose-spec/compose-spec/blob/master/deploy.md#resources # deploy.resources.{limits,reservations}.{cpus, memory} deploy = cnt.get("deploy", None) or {} res = deploy.get("resources", None) or {} limits = res.get("limits", None) or {} cpus_limit_v3 = try_float(limits.get("cpus", None), None) mem_limit_v3 = limits.get("memory", None) reservations = res.get("reservations", None) or {} # cpus_res_v3 = try_float(reservations.get('cpus', None), None) mem_res_v3 = reservations.get("memory", None) # add args cpus = cpus_limit_v3 or cpus_limit_v2 if cpus: podman_args.extend(( "--cpus", str(cpus), )) if cpu_shares_v2: podman_args.extend(( "--cpu-shares", str(cpu_shares_v2), )) mem = mem_limit_v3 or mem_limit_v2 if mem: podman_args.extend(( "-m", str(mem).lower(), )) mem_res = mem_res_v3 or mem_res_v2 if mem_res: podman_args.extend(( "--memory-reservation", str(mem_res).lower(), )) def port_dict_to_str(port_desc): # NOTE: `mode: host|ingress` is ignored cnt_port = port_desc.get("target", None) published = port_desc.get("published", None) or "" host_ip = port_desc.get("host_ip", None) protocol = port_desc.get("protocol", None) or "tcp" if not cnt_port: raise ValueError("target container port must be specified") if host_ip: ret = f"{host_ip}:{published}:{cnt_port}" else: ret = f"{published}:{cnt_port}" if published else f"{cnt_port}" if protocol != "tcp": ret += f"/{protocol}" return ret def norm_ports(ports_in): if not ports_in: ports_in = [] if isinstance(ports_in, str): ports_in = [ports_in] ports_out = [] for port in ports_in: if isinstance(port, dict): port = port_dict_to_str(port) elif isinstance(port, int): port = str(port) elif not isinstance(port, str): raise TypeError("port should be either string or dict") ports_out.append(port) return ports_out def get_network_create_args(net_desc, proj_name, net_name): args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", ] # TODO: add more options here, like dns, ipv6, etc. labels = net_desc.get("labels", None) or [] for item in norm_as_list(labels): args.extend(["--label", item]) if net_desc.get("internal", None): args.append("--internal") driver = net_desc.get("driver", None) if driver: args.extend(("--driver", driver)) driver_opts = net_desc.get("driver_opts", None) or {} for key, value in driver_opts.items(): args.extend(("--opt", f"{key}={value}")) ipam = net_desc.get("ipam", None) or {} ipam_driver = ipam.get("driver", None) if ipam_driver and ipam_driver != "default": args.extend(("--ipam-driver", ipam_driver)) ipam_config_ls = ipam.get("config", None) or [] if net_desc.get("enable_ipv6", None): args.append("--ipv6") if is_dict(ipam_config_ls): ipam_config_ls = [ipam_config_ls] for ipam_config in ipam_config_ls: subnet = ipam_config.get("subnet", None) ip_range = ipam_config.get("ip_range", None) gateway = ipam_config.get("gateway", None) if subnet: args.extend(("--subnet", subnet)) if ip_range: args.extend(("--ip-range", ip_range)) if gateway: args.extend(("--gateway", gateway)) args.append(net_name) return args async def assert_cnt_nets(compose, cnt): """ create missing networks """ net = cnt.get("network_mode", None) if net and not net.startswith("bridge"): return proj_name = compose.project_name nets = compose.networks default_net = compose.default_net cnt_nets = cnt.get("networks", None) if cnt_nets and is_dict(cnt_nets): cnt_nets = list(cnt_nets.keys()) cnt_nets = norm_as_list(cnt_nets or default_net) for net in cnt_nets: net_desc = nets[net] or {} is_ext = net_desc.get("external", None) ext_desc = is_ext if is_dict(is_ext) else {} default_net_name = net if is_ext else f"{proj_name}_{net}" net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name try: await compose.podman.output([], "network", ["exists", net_name]) except subprocess.CalledProcessError as e: if is_ext: raise RuntimeError(f"External network [{net_name}] does not exists") from e args = get_network_create_args(net_desc, proj_name, net_name) await compose.podman.output([], "network", args) await compose.podman.output([], "network", ["exists", net_name]) def get_net_args(compose, cnt): service_name = cnt["service_name"] net_args = [] is_bridge = False mac_address = cnt.get("mac_address", None) net = cnt.get("network_mode", None) if net: if net == "none": is_bridge = False elif net == "host": net_args.append(f"--network={net}") elif net.startswith("slirp4netns"): # Note: podman-specific network mode net_args.append(f"--network={net}") elif net == "private": # Note: podman-specific network mode net_args.append("--network=private") elif net.startswith("pasta"): # Note: podman-specific network mode net_args.append(f"--network={net}") elif net.startswith("ns:"): # Note: podman-specific network mode net_args.append(f"--network={net}") elif net.startswith("service:"): other_srv = net.split(":", 1)[1].strip() other_cnt = compose.container_names_by_service[other_srv][0] net_args.append(f"--network=container:{other_cnt}") elif net.startswith("container:"): other_cnt = net.split(":", 1)[1].strip() net_args.append(f"--network=container:{other_cnt}") elif net.startswith("bridge"): is_bridge = True else: log.fatal("unknown network_mode [%s]", net) sys.exit(1) else: is_bridge = True proj_name = compose.project_name default_net = compose.default_net nets = compose.networks cnt_nets = cnt.get("networks", None) aliases = [service_name] # NOTE: from podman manpage: # NOTE: A container will only have access to aliases on the first network # that it joins. This is a limitation that will be removed in a later # release. ip = None ip6 = None ip_assignments = 0 if cnt.get("_aliases", None): aliases.extend(cnt.get("_aliases", None)) if cnt_nets and is_dict(cnt_nets): prioritized_cnt_nets = [] # cnt_nets is {net_key: net_value, ...} for net_key, net_value in cnt_nets.items(): net_value = net_value or {} aliases.extend(norm_as_list(net_value.get("aliases", None))) if net_value.get("ipv4_address", None) is not None: ip_assignments = ip_assignments + 1 if net_value.get("ipv6_address", None) is not None: ip_assignments = ip_assignments + 1 if not ip: ip = net_value.get("ipv4_address", None) if not ip6: ip6 = net_value.get("ipv6_address", None) net_priority = net_value.get("priority", 0) prioritized_cnt_nets.append(( net_priority, net_key, )) # sort dict by priority prioritized_cnt_nets.sort(reverse=True) cnt_nets = [net_key for _, net_key in prioritized_cnt_nets] cnt_nets = norm_as_list(cnt_nets or default_net) net_names = [] for net in cnt_nets: net_desc = nets[net] or {} is_ext = net_desc.get("external", None) ext_desc = is_ext if is_dict(is_ext) else {} default_net_name = net if is_ext else f"{proj_name}_{net}" net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name net_names.append(net_name) net_names_str = ",".join(net_names) # TODO: add support for per-interface aliases # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases # Even though podman accepts network-specific aliases (e.g., --network=bridge:alias=foo, # podman currently ignores this if a per-container network-alias is set; as pdoman-compose # always sets a network-alias to the container name, is currently doesn't make sense to # implement this. multiple_nets = cnt.get("networks", None) if multiple_nets and len(multiple_nets) > 1: # networks can be specified as a dict with config per network or as a plain list without # config. Support both cases by converting the plain list to a dict with empty config. if is_list(multiple_nets): multiple_nets = {net: {} for net in multiple_nets} else: multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()} # if a mac_address was specified on the container level, we need to check that it is not # specified on the network level as well if mac_address is not None: for net_config_ in multiple_nets.values(): network_mac = net_config_.get("x-podman.mac_address", None) if network_mac is not None: raise RuntimeError( f"conflicting mac addresses {mac_address} and {network_mac}:" "specifying mac_address on both container and network level " "is not supported" ) for net_, net_config_ in multiple_nets.items(): net_desc = nets[net_] or {} is_ext = net_desc.get("external", None) ext_desc = is_ext if is_dict(is_ext) else {} default_net_name = net_ if is_ext else f"{proj_name}_{net_}" net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name ipv4 = net_config_.get("ipv4_address", None) ipv6 = net_config_.get("ipv6_address", None) # custom extension; not supported by docker-compose v3 mac = net_config_.get("x-podman.mac_address", None) # if a mac_address was specified on the container level, apply it to the first network # This works for Python > 3.6, because dict insert ordering is preserved, so we are # sure that the first network we encounter here is also the first one specified by # the user if mac is None and mac_address is not None: mac = mac_address mac_address = None net_options = [] if ipv4: net_options.append(f"ip={ipv4}") if ipv6: net_options.append(f"ip={ipv6}") if mac: net_options.append(f"mac={mac}") if net_options: net_args.append(f"--network={net_name}:" + ",".join(net_options)) else: net_args.append(f"--network={net_name}") else: if is_bridge: if net_names_str: net_args.append(f"--network={net_names_str}") else: net_args.append("--network=bridge") if ip: net_args.append(f"--ip={ip}") if ip6: net_args.append(f"--ip6={ip6}") if mac_address: net_args.append(f"--mac-address={mac_address}") if is_bridge: for alias in aliases: net_args.extend([f"--network-alias={alias}"]) return net_args async def container_to_args(compose, cnt, detached=True): # TODO: double check -e , --add-host, -v, --read-only dirname = compose.dirname pod = cnt.get("pod", None) or "" name = cnt["name"] podman_args = [f"--name={name}"] if detached: podman_args.append("-d") if pod: podman_args.append(f"--pod={pod}") deps = [] for dep_srv in cnt.get("_deps", None) or []: deps.extend(compose.container_names_by_service.get(dep_srv, None) or []) if deps: deps_csv = ",".join(deps) podman_args.append(f"--requires={deps_csv}") sec = norm_as_list(cnt.get("security_opt", None)) for sec_item in sec: podman_args.extend(["--security-opt", sec_item]) ann = norm_as_list(cnt.get("annotations", None)) for a in ann: podman_args.extend(["--annotation", a]) if cnt.get("read_only", None): podman_args.append("--read-only") if cnt.get("http_proxy", None) is False: podman_args.append("--http-proxy=false") for i in cnt.get("labels", []): podman_args.extend(["--label", i]) for c in cnt.get("cap_add", []): podman_args.extend(["--cap-add", c]) for c in cnt.get("cap_drop", []): podman_args.extend(["--cap-drop", c]) for item in cnt.get("group_add", []): podman_args.extend(["--group-add", item]) for item in cnt.get("devices", []): podman_args.extend(["--device", item]) for item in norm_as_list(cnt.get("dns", None)): podman_args.extend(["--dns", item]) for item in norm_as_list(cnt.get("dns_opt", None)): podman_args.extend(["--dns-opt", item]) for item in norm_as_list(cnt.get("dns_search", None)): podman_args.extend(["--dns-search", item]) env_file = cnt.get("env_file", []) if is_str(env_file) or is_dict(env_file): env_file = [env_file] for i in env_file: if is_str(i): i = {"path": i} path = i["path"] required = i.get("required", True) i = os.path.realpath(os.path.join(dirname, path)) if not os.path.exists(i): if not required: continue raise ValueError("Env file at {} does not exist".format(i)) dotenv_dict = {} dotenv_dict = dotenv_to_dict(i) env = norm_as_list(dotenv_dict) for e in env: podman_args.extend(["-e", e]) env = norm_as_list(cnt.get("environment", {})) for e in env: podman_args.extend(["-e", e]) tmpfs_ls = cnt.get("tmpfs", []) if is_str(tmpfs_ls): tmpfs_ls = [tmpfs_ls] for i in tmpfs_ls: podman_args.extend(["--tmpfs", i]) for volume in cnt.get("volumes", []): podman_args.extend(await get_mount_args(compose, cnt, volume)) await assert_cnt_nets(compose, cnt) podman_args.extend(get_net_args(compose, cnt)) log_config = cnt.get("logging", None) if log_config is not None: podman_args.append(f'--log-driver={log_config.get("driver", "k8s-file")}') log_opts = log_config.get("options") or {} podman_args += [f"--log-opt={name}={value}" for name, value in log_opts.items()] for secret in cnt.get("secrets", []): podman_args.extend(get_secret_args(compose, cnt, secret)) for i in cnt.get("extra_hosts", []): podman_args.extend(["--add-host", i]) for i in cnt.get("expose", []): podman_args.extend(["--expose", i]) if cnt.get("publishall", None): podman_args.append("-P") ports = cnt.get("ports", None) or [] if isinstance(ports, str): ports = [ports] for port in ports: if isinstance(port, dict): port = port_dict_to_str(port) elif not isinstance(port, str): raise TypeError("port should be either string or dict") podman_args.extend(["-p", port]) userns_mode = cnt.get("userns_mode", None) if userns_mode is not None: podman_args.extend(["--userns", userns_mode]) user = cnt.get("user", None) if user is not None: podman_args.extend(["-u", user]) if cnt.get("working_dir", None) is not None: podman_args.extend(["-w", cnt["working_dir"]]) if cnt.get("hostname", None): podman_args.extend(["--hostname", cnt["hostname"]]) if cnt.get("shm_size", None): podman_args.extend(["--shm-size", str(cnt["shm_size"])]) if cnt.get("stdin_open", None): podman_args.append("-i") if cnt.get("stop_signal", None): podman_args.extend(["--stop-signal", cnt["stop_signal"]]) sysctls = cnt.get("sysctls") if sysctls is not None: if isinstance(sysctls, dict): for sysctl, value in sysctls.items(): podman_args.extend(["--sysctl", "{}={}".format(sysctl, value)]) elif isinstance(sysctls, list): for i in sysctls: podman_args.extend(["--sysctl", i]) else: raise TypeError("sysctls should be either dict or list") if cnt.get("tty", None): podman_args.append("--tty") if cnt.get("privileged", None): podman_args.append("--privileged") if cnt.get("pid", None): podman_args.extend(["--pid", cnt["pid"]]) pull_policy = cnt.get("pull_policy", None) if pull_policy is not None and pull_policy != "build": podman_args.extend(["--pull", pull_policy]) if cnt.get("restart", None) is not None: podman_args.extend(["--restart", cnt["restart"]]) container_to_ulimit_args(cnt, podman_args) container_to_res_args(cnt, podman_args) # currently podman shipped by fedora does not package this if cnt.get("init", None): podman_args.append("--init") if cnt.get("init-path", None): podman_args.extend(["--init-path", cnt["init-path"]]) entrypoint = cnt.get("entrypoint", None) if entrypoint is not None: if is_str(entrypoint): entrypoint = shlex.split(entrypoint) podman_args.extend(["--entrypoint", json.dumps(entrypoint)]) platform = cnt.get("platform", None) if platform is not None: podman_args.extend(["--platform", platform]) if cnt.get("runtime", None): podman_args.extend(["--runtime", cnt["runtime"]]) # WIP: healthchecks are still work in progress healthcheck = cnt.get("healthcheck", None) or {} if not is_dict(healthcheck): raise ValueError("'healthcheck' must be a key-value mapping") healthcheck_disable = healthcheck.get("disable", False) healthcheck_test = healthcheck.get("test", None) if healthcheck_disable: healthcheck_test = ["NONE"] if healthcheck_test: # If it's a string, it's equivalent to specifying CMD-SHELL if is_str(healthcheck_test): # podman does not add shell to handle command with whitespace podman_args.extend([ "--healthcheck-command", "/bin/sh -c " + cmd_quote(healthcheck_test), ]) elif is_list(healthcheck_test): healthcheck_test = healthcheck_test.copy() # If it's a list, first item is either NONE, CMD or CMD-SHELL. healthcheck_type = healthcheck_test.pop(0) if healthcheck_type == "NONE": podman_args.append("--no-healthcheck") elif healthcheck_type == "CMD": cmd_q = "' '".join([cmd_quote(i) for i in healthcheck_test]) podman_args.extend(["--healthcheck-command", "/bin/sh -c " + cmd_q]) elif healthcheck_type == "CMD-SHELL": if len(healthcheck_test) != 1: raise ValueError("'CMD_SHELL' takes a single string after it") cmd_q = cmd_quote(healthcheck_test[0]) podman_args.extend(["--healthcheck-command", "/bin/sh -c " + cmd_q]) else: raise ValueError( f"unknown healthcheck test type [{healthcheck_type}],\ expecting NONE, CMD or CMD-SHELL." ) else: raise ValueError("'healthcheck.test' either a string or a list") # interval, timeout and start_period are specified as durations. if "interval" in healthcheck: podman_args.extend(["--healthcheck-interval", healthcheck["interval"]]) if "timeout" in healthcheck: podman_args.extend(["--healthcheck-timeout", healthcheck["timeout"]]) if "start_period" in healthcheck: podman_args.extend(["--healthcheck-start-period", healthcheck["start_period"]]) # convert other parameters to string if "retries" in healthcheck: podman_args.extend(["--healthcheck-retries", str(healthcheck["retries"])]) # handle podman extension if 'x-podman' in cnt: raise ValueError( 'Configuration under x-podman has been migrated to x-podman.uidmap and ' 'x-podman.gidmap fields' ) rootfs_mode = False for uidmap in cnt.get('x-podman.uidmaps', []): podman_args.extend(["--uidmap", uidmap]) for gidmap in cnt.get('x-podman.gidmaps', []): podman_args.extend(["--gidmap", gidmap]) rootfs = cnt.get('x-podman.rootfs', None) if rootfs is not None: rootfs_mode = True podman_args.extend(["--rootfs", rootfs]) log.warning("WARNING: x-podman.rootfs and image both specified, image field ignored") if not rootfs_mode: podman_args.append(cnt["image"]) # command, ..etc. command = cnt.get("command", None) if command is not None: if is_str(command): podman_args.extend(shlex.split(command)) else: podman_args.extend([str(i) for i in command]) return podman_args def rec_deps(services, service_name, start_point=None): """ return all dependencies of service_name recursively """ if not start_point: start_point = service_name deps = services[service_name]["_deps"] for dep_name in deps.copy(): # avoid A depens on A if dep_name == service_name: continue dep_srv = services.get(dep_name, None) if not dep_srv: continue # NOTE: avoid creating loops, A->B->A if start_point and start_point in dep_srv["_deps"]: continue new_deps = rec_deps(services, dep_name, start_point) deps.update(new_deps) return deps def flat_deps(services, with_extends=False): """ create dependencies "_deps" or update it recursively for all services """ for name, srv in services.items(): deps = set() srv["_deps"] = deps if with_extends: ext = srv.get("extends", {}).get("service", None) if ext: if ext != name: deps.add(ext) continue deps_ls = srv.get("depends_on", None) or [] if is_str(deps_ls): deps_ls = [deps_ls] elif is_dict(deps_ls): deps_ls = list(deps_ls.keys()) deps.update(deps_ls) # parse link to get service name and remove alias links_ls = srv.get("links", None) or [] if not is_list(links_ls): links_ls = [links_ls] deps.update([(c.split(":")[0] if ":" in c else c) for c in links_ls]) for c in links_ls: if ":" in c: dep_name, dep_alias = c.split(":") if "_aliases" not in services[dep_name]: services[dep_name]["_aliases"] = set() services[dep_name]["_aliases"].add(dep_alias) for name, srv in services.items(): rec_deps(services, name) async def wait_with_timeout(coro, timeout): """ Asynchronously waits for the given coroutine to complete with a timeout. Args: coro: The coroutine to wait for. timeout (int or float): The maximum number of seconds to wait for. Raises: TimeoutError: If the coroutine does not complete within the specified timeout. """ try: return await asyncio.wait_for(coro, timeout) except asyncio.TimeoutError as exc: raise TimeoutError from exc ################### # podman and compose classes ################### class Podman: def __init__( self, compose, podman_path="podman", dry_run=False, semaphore: asyncio.Semaphore = asyncio.Semaphore(sys.maxsize), ): self.compose = compose self.podman_path = podman_path self.dry_run = dry_run self.semaphore = semaphore async def output(self, podman_args, cmd="", cmd_args=None): async with self.semaphore: cmd_args = cmd_args or [] xargs = self.compose.get_podman_args(cmd) if cmd else [] cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args log.info(str(cmd_ls)) p = await asyncio.create_subprocess_exec( *cmd_ls, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout_data, stderr_data = await p.communicate() if p.returncode == 0: return stdout_data raise subprocess.CalledProcessError(p.returncode, " ".join(cmd_ls), stderr_data) def exec( self, podman_args, cmd="", cmd_args=None, ): cmd_args = list(map(str, cmd_args or [])) xargs = self.compose.get_podman_args(cmd) if cmd else [] cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args log.info(" ".join([str(i) for i in cmd_ls])) os.execlp(self.podman_path, *cmd_ls) async def run( # pylint: disable=dangerous-default-value self, podman_args, cmd="", cmd_args=None, log_formatter=None, *, # Intentionally mutable default argument to hold references to tasks task_reference=set(), ) -> int: async with self.semaphore: cmd_args = list(map(str, cmd_args or [])) xargs = self.compose.get_podman_args(cmd) if cmd else [] cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args log.info(" ".join([str(i) for i in cmd_ls])) if self.dry_run: return None if log_formatter is not None: async def format_out(stdout): while True: line = await stdout.readline() if line: print(log_formatter, line.decode('utf-8'), end='') if stdout.at_eof(): break p = await asyncio.create_subprocess_exec( *cmd_ls, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) # pylint: disable=consider-using-with # This is hacky to make the tasks not get garbage collected # https://github.com/python/cpython/issues/91887 out_t = asyncio.create_task(format_out(p.stdout)) task_reference.add(out_t) out_t.add_done_callback(task_reference.discard) err_t = asyncio.create_task(format_out(p.stderr)) task_reference.add(err_t) err_t.add_done_callback(task_reference.discard) else: p = await asyncio.create_subprocess_exec(*cmd_ls) # pylint: disable=consider-using-with try: exit_code = await p.wait() except asyncio.CancelledError: log.info("Sending termination signal") p.terminate() try: exit_code = await wait_with_timeout(p.wait(), 10) except TimeoutError: log.warning("container did not shut down after 10 seconds, killing") p.kill() exit_code = await p.wait() log.info("exit code: %s", exit_code) return exit_code async def volume_ls(self, proj=None): if not proj: proj = self.compose.project_name output = ( await self.output( [], "volume", [ "ls", "--noheading", "--filter", f"label=io.podman.compose.project={proj}", "--format", "{{.Name}}", ], ) ).decode("utf-8") volumes = output.splitlines() return volumes def normalize_service(service, sub_dir=""): if "build" in service: build = service["build"] if is_str(build): service["build"] = {"context": build} if sub_dir and "build" in service: build = service["build"] context = build.get("context", None) or "" if context or sub_dir: if context.startswith("./"): context = context[2:] if sub_dir: context = os.path.join(sub_dir, context) context = context.rstrip("/") if not context: context = "." service["build"]["context"] = context if "build" in service and "additional_contexts" in service["build"]: if is_dict(build["additional_contexts"]): new_additional_contexts = [] for k, v in build["additional_contexts"].items(): new_additional_contexts.append(f"{k}={v}") build["additional_contexts"] = new_additional_contexts for key in ("command", "entrypoint"): if key in service: if is_str(service[key]): service[key] = shlex.split(service[key]) for key in ("env_file", "security_opt", "volumes"): if key not in service: continue if is_str(service[key]): service[key] = [service[key]] if "security_opt" in service: sec_ls = service["security_opt"] for ix, item in enumerate(sec_ls): if item in ("seccomp:unconfined", "apparmor:unconfined"): sec_ls[ix] = item.replace(":", "=") for key in ("environment", "labels"): if key not in service: continue service[key] = norm_as_dict(service[key]) if "extends" in service: extends = service["extends"] if is_str(extends): extends = {"service": extends} service["extends"] = extends if "depends_on" in service: deps = service["depends_on"] if is_str(deps): deps = [deps] if is_list(deps): deps_dict = {} for d in deps: deps_dict[d] = {'condition': 'service_started'} service["depends_on"] = deps_dict return service def normalize(compose): """ convert compose dict of some keys from string or dicts into arrays """ services = compose.get("services", None) or {} for service in services.values(): normalize_service(service) return compose def normalize_service_final(service: dict, project_dir: str) -> dict: if "build" in service: build = service["build"] context = build if is_str(build) else build.get("context", ".") context = os.path.normpath(os.path.join(project_dir, context)) dockerfile = ( "Dockerfile" if is_str(build) else service["build"].get("dockerfile", "Dockerfile") ) if not is_dict(service["build"]): service["build"] = {} service["build"]["dockerfile"] = dockerfile service["build"]["context"] = context return service def normalize_final(compose: dict, project_dir: str) -> dict: services = compose.get("services", None) or {} for service in services.values(): normalize_service_final(service, project_dir) return compose def clone(value): return value.copy() if is_list(value) or is_dict(value) else value def rec_merge_one(target, source): """ update target from source recursively """ done = set() for key, value in source.items(): if key in target: continue target[key] = clone(value) done.add(key) for key, value in target.items(): if key in done: continue if key not in source: continue value2 = source[key] if key in ("command", "entrypoint"): target[key] = clone(value2) continue if not isinstance(value2, type(value)): value_type = type(value) value2_type = type(value2) raise ValueError(f"can't merge value of [{key}] of type {value_type} and {value2_type}") if is_list(value2): if key == "volumes": # clean duplicate mount targets pts = {v.split(":", 2)[1] for v in value2 if ":" in v} del_ls = [ ix for (ix, v) in enumerate(value) if ":" in v and v.split(":", 2)[1] in pts ] for ix in reversed(del_ls): del value[ix] value.extend(value2) else: value.extend(value2) elif is_dict(value2): rec_merge_one(value, value2) else: target[key] = value2 return target def rec_merge(target, *sources): """ update target recursively from sources """ for source in sources: ret = rec_merge_one(target, source) return ret def resolve_extends(services, service_names, environ): for name in service_names: service = services[name] ext = service.get("extends", {}) if is_str(ext): ext = {"service": ext} from_service_name = ext.get("service", None) if not from_service_name: continue filename = ext.get("file", None) if filename: if filename.startswith("./"): filename = filename[2:] with open(filename, "r", encoding="utf-8") as f: content = yaml.safe_load(f) or {} if "services" in content: content = content["services"] subdirectory = os.path.dirname(filename) content = rec_subs(content, environ) from_service = content.get(from_service_name, {}) or {} normalize_service(from_service, subdirectory) else: from_service = services.get(from_service_name, {}).copy() del from_service["_deps"] try: del from_service["extends"] except KeyError: pass new_service = rec_merge({}, from_service, service) services[name] = new_service def dotenv_to_dict(dotenv_path): if not os.path.isfile(dotenv_path): return {} return dotenv_values(dotenv_path) COMPOSE_DEFAULT_LS = [ "compose.yaml", "compose.yml", "compose.override.yaml", "compose.override.yml", "podman-compose.yaml", "podman-compose.yml", "docker-compose.yml", "docker-compose.yaml", "docker-compose.override.yml", "docker-compose.override.yaml", "container-compose.yml", "container-compose.yaml", "container-compose.override.yml", "container-compose.override.yaml", ] class PodmanCompose: def __init__(self): self.podman = None self.podman_version = None self.environ = {} self.exit_code = None self.commands = {} self.global_args = None self.project_name = None self.dirname = None self.pods = None self.containers = None self.vols = None self.networks = {} self.default_net = "default" self.declared_secrets = None self.container_names_by_service = None self.container_by_name = None self.services = None self.all_services = set() self.prefer_volume_over_mount = True self.merged_yaml = None self.yaml_hash = "" self.console_colors = [ "\x1b[1;32m", "\x1b[1;33m", "\x1b[1;34m", "\x1b[1;35m", "\x1b[1;36m", ] def assert_services(self, services): if is_str(services): services = [services] given = set(services or []) missing = given - self.all_services if missing: missing_csv = ",".join(missing) log.warning("missing services [%s]", missing_csv) sys.exit(1) def get_podman_args(self, cmd): xargs = [] for args in self.global_args.podman_args: xargs.extend(shlex.split(args)) cmd_norm = cmd if cmd != "create" else "run" cmd_args = self.global_args.__dict__.get(f"podman_{cmd_norm}_args", None) or [] for args in cmd_args: xargs.extend(shlex.split(args)) return xargs async def run(self): log.info("podman-compose version: %s", __version__) args = self._parse_args() podman_path = args.podman_path if podman_path != "podman": if os.path.isfile(podman_path) and os.access(podman_path, os.X_OK): podman_path = os.path.realpath(podman_path) else: # this also works if podman hasn't been installed now if args.dry_run is False: log.fatal("Binary %s has not been found.", podman_path) sys.exit(1) self.podman = Podman(self, podman_path, args.dry_run, asyncio.Semaphore(args.parallel)) if not args.dry_run: # just to make sure podman is running try: self.podman_version = (await self.podman.output(["--version"], "", [])).decode( "utf-8" ).strip() or "" self.podman_version = (self.podman_version.split() or [""])[-1] except subprocess.CalledProcessError: self.podman_version = None if not self.podman_version: log.fatal("it seems that you do not have `podman` installed") sys.exit(1) log.info("using podman version: %s", self.podman_version) cmd_name = args.command compose_required = cmd_name != "version" and ( cmd_name != "systemd" or args.action != "create-unit" ) if compose_required: self._parse_compose_file() cmd = self.commands[cmd_name] retcode = await cmd(self, args) if isinstance(retcode, int): sys.exit(retcode) def resolve_in_pod(self, compose): if self.global_args.in_pod_bool is None: extension_dict = compose.get("x-podman", None) if extension_dict is not None: in_pod_value = extension_dict.get("in_pod", None) if in_pod_value is not None: self.global_args.in_pod_bool = in_pod_value else: self.global_args.in_pod_bool = True # otherwise use `in_pod` value provided by command line return self.global_args.in_pod_bool def _parse_compose_file(self): args = self.global_args # cmd = args.command dirname = os.environ.get("COMPOSE_PROJECT_DIR", None) if dirname and os.path.isdir(dirname): os.chdir(dirname) pathsep = os.environ.get("COMPOSE_PATH_SEPARATOR", None) or os.pathsep if not args.file: default_str = os.environ.get("COMPOSE_FILE", None) if default_str: default_ls = default_str.split(pathsep) else: default_ls = COMPOSE_DEFAULT_LS args.file = list(filter(os.path.exists, default_ls)) files = args.file if not files: log.fatal( "no compose.yaml, docker-compose.yml or container-compose.yml file found, " "pass files with -f" ) sys.exit(-1) ex = map(lambda x: x == '-' or os.path.exists(x), files) missing = [fn0 for ex0, fn0 in zip(ex, files) if not ex0] if missing: log.fatal("missing files: %s", missing) sys.exit(1) # make absolute relative_files = files files = list(map(os.path.realpath, files)) filename = files[0] project_name = args.project_name # no_ansi = args.no_ansi # no_cleanup = args.no_cleanup # dry_run = args.dry_run # host_env = None dirname = os.path.realpath(os.path.dirname(filename)) dir_basename = os.path.basename(dirname) self.dirname = dirname # env-file is relative to the CWD dotenv_dict = {} if args.env_file: # Load .env from the Compose file's directory to preserve # behavior prior to 1.1.0 and to match with Docker Compose (v2). if ".env" == args.env_file: project_dotenv_file = os.path.realpath(os.path.join(dirname, ".env")) if os.path.exists(project_dotenv_file): dotenv_dict.update(dotenv_to_dict(project_dotenv_file)) dotenv_path = os.path.realpath(args.env_file) dotenv_dict.update(dotenv_to_dict(dotenv_path)) # TODO: remove next line os.chdir(dirname) os.environ.update({ key: value for key, value in dotenv_dict.items() if key.startswith("PODMAN_") }) self.environ = dotenv_dict self.environ.update(dict(os.environ)) # see: https://docs.docker.com/compose/reference/envvars/ # see: https://docs.docker.com/compose/env-file/ self.environ.update({ "COMPOSE_PROJECT_DIR": dirname, "COMPOSE_FILE": pathsep.join(relative_files), "COMPOSE_PATH_SEPARATOR": pathsep, }) compose = {} # Iterate over files primitively to allow appending to files in-loop files_iter = iter(files) while True: try: filename = next(files_iter) except StopIteration: break if filename.strip().split('/')[-1] == '-': content = yaml.safe_load(sys.stdin) else: with open(filename, "r", encoding="utf-8") as f: content = yaml.safe_load(f) # log(filename, json.dumps(content, indent = 2)) if not isinstance(content, dict): sys.stderr.write( "Compose file does not contain a top level object: %s\n" % filename ) sys.exit(1) content = normalize(content) # log(filename, json.dumps(content, indent = 2)) content = rec_subs(content, self.environ) rec_merge(compose, content) # If `include` is used, append included files to files include = compose.get("include", None) if include: files.extend(include) # As compose obj is updated and tested with every loop, not deleting `include` # from it, results in it being tested again and again, original values for # `include` be appended to `files`, and, included files be processed for ever. # Solution is to remove 'include' key from compose obj. This doesn't break # having `include` present and correctly processed in included files del compose["include"] resolved_services = self._resolve_profiles(compose.get("services", {}), set(args.profile)) compose["services"] = resolved_services if not getattr(args, "no_normalize", None): compose = normalize_final(compose, self.dirname) self.merged_yaml = yaml.safe_dump(compose) merged_json_b = json.dumps(compose, separators=(",", ":")).encode("utf-8") self.yaml_hash = hashlib.sha256(merged_json_b).hexdigest() compose["_dirname"] = dirname # debug mode if len(files) > 1: log.debug(" ** merged:\n%s", json.dumps(compose, indent=2)) # ver = compose.get('version', None) if not project_name: project_name = compose.get("name", None) if project_name is None: # More strict then actually needed for simplicity: # podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* project_name = ( self.environ.get("COMPOSE_PROJECT_NAME", None) or dir_basename.lower() ) project_name = norm_re.sub("", project_name) if not project_name: raise RuntimeError(f"Project name [{dir_basename}] normalized to empty") self.project_name = project_name self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name}) services = compose.get("services", None) if services is None: services = {} log.warning("WARNING: No services defined") # include services with no profile defined or the selected profiles services = self._resolve_profiles(services, set(args.profile)) # NOTE: maybe add "extends.service" to _deps at this stage flat_deps(services, with_extends=True) service_names = sorted([(len(srv["_deps"]), name) for name, srv in services.items()]) service_names = [name for _, name in service_names] resolve_extends(services, service_names, self.environ) flat_deps(services) service_names = sorted([(len(srv["_deps"]), name) for name, srv in services.items()]) service_names = [name for _, name in service_names] nets = compose.get("networks", None) or {} if not nets: nets["default"] = None self.networks = nets if len(self.networks) == 1: self.default_net = list(nets.keys())[0] elif "default" in nets: self.default_net = "default" else: self.default_net = None default_net = self.default_net allnets = set() for name, srv in services.items(): srv_nets = srv.get("networks", None) or default_net srv_nets = list(srv_nets.keys()) if is_dict(srv_nets) else norm_as_list(srv_nets) allnets.update(srv_nets) given_nets = set(nets.keys()) missing_nets = allnets - given_nets unused_nets = given_nets - allnets - set(["default"]) if len(unused_nets): unused_nets_str = ",".join(unused_nets) log.warning("WARNING: unused networks: %s", unused_nets_str) if len(missing_nets): missing_nets_str = ",".join(missing_nets) raise RuntimeError(f"missing networks: {missing_nets_str}") # volumes: [...] self.vols = compose.get("volumes", {}) podman_compose_labels = [ "io.podman.compose.config-hash=" + self.yaml_hash, "io.podman.compose.project=" + project_name, "io.podman.compose.version=" + __version__, f"PODMAN_SYSTEMD_UNIT=podman-compose@{project_name}.service", "com.docker.compose.project=" + project_name, "com.docker.compose.project.working_dir=" + dirname, "com.docker.compose.project.config_files=" + ",".join(relative_files), ] # other top-levels: # networks: {driver: ...} # configs: {...} self.declared_secrets = compose.get("secrets", {}) given_containers = [] container_names_by_service = {} self.services = services for service_name, service_desc in services.items(): replicas = try_int(service_desc.get("deploy", {}).get("replicas", "1")) container_names_by_service[service_name] = [] for num in range(1, replicas + 1): name0 = f"{project_name}_{service_name}_{num}" if num == 1: name = service_desc.get("container_name", name0) else: name = name0 container_names_by_service[service_name].append(name) # log(service_name,service_desc) cnt = { "name": name, "num": num, "service_name": service_name, **service_desc, } x_podman = service_desc.get("x-podman", None) rootfs_mode = x_podman is not None and x_podman.get("rootfs", None) is not None if "image" not in cnt and not rootfs_mode: cnt["image"] = f"{project_name}_{service_name}" labels = norm_as_list(cnt.get("labels", None)) cnt["ports"] = norm_ports(cnt.get("ports", None)) labels.extend(podman_compose_labels) labels.extend([ f"com.docker.compose.container-number={num}", "com.docker.compose.service=" + service_name, ]) cnt["labels"] = labels cnt["_service"] = service_name cnt["_project"] = project_name given_containers.append(cnt) volumes = cnt.get("volumes", None) or [] for volume in volumes: mnt_dict = get_mnt_dict(self, cnt, volume) if ( mnt_dict.get("type", None) == "volume" and mnt_dict["source"] and mnt_dict["source"] not in self.vols ): vol_name = mnt_dict["source"] raise RuntimeError(f"volume [{vol_name}] not defined in top level") self.container_names_by_service = container_names_by_service self.all_services = set(container_names_by_service.keys()) container_by_name = {c["name"]: c for c in given_containers} # log("deps:", [(c["name"], c["_deps"]) for c in given_containers]) given_containers = list(container_by_name.values()) given_containers.sort(key=lambda c: len(c.get("_deps", None) or [])) # log("sorted:", [c["name"] for c in given_containers]) args.in_pod_bool = self.resolve_in_pod(compose) pods, containers = transform(args, project_name, given_containers) self.pods = pods self.containers = containers self.container_by_name = {c["name"]: c for c in containers} def _resolve_profiles(self, defined_services, requested_profiles=None): """ Returns a service dictionary (key = service name, value = service config) compatible with the requested_profiles list. The returned service dictionary contains all services which do not include/reference a profile in addition to services that match the requested_profiles. :param defined_services: The service dictionary :param requested_profiles: The profiles requested using the --profile arg. """ if requested_profiles is None: requested_profiles = set() services = {} for name, config in defined_services.items(): service_profiles = set(config.get("profiles", [])) if not service_profiles or requested_profiles.intersection(service_profiles): services[name] = config return services def _parse_args(self): parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) self._init_global_parser(parser) subparsers = parser.add_subparsers(title="command", dest="command") subparser = subparsers.add_parser("help", help="show help") for cmd_name, cmd in self.commands.items(): subparser = subparsers.add_parser(cmd_name, help=cmd.desc) # pylint: disable=protected-access for cmd_parser in cmd._parse_args: # pylint: disable=protected-access cmd_parser(subparser) self.global_args = parser.parse_args() if self.global_args.in_pod is not None and self.global_args.in_pod.lower() not in ( '', 'true', '1', 'false', '0', ): raise ValueError( f'Invalid --in-pod value: \'{self.global_args.in_pod}\'. ' 'It must be set to either of: empty value, true, 1, false, 0' ) if self.global_args.in_pod == '' or self.global_args.in_pod is None: self.global_args.in_pod_bool = None else: self.global_args.in_pod_bool = self.global_args.in_pod.lower() in ('true', '1') if self.global_args.version: self.global_args.command = "version" if not self.global_args.command or self.global_args.command == "help": parser.print_help() sys.exit(-1) logging.basicConfig(level=("DEBUG" if self.global_args.verbose else "WARN")) return self.global_args @staticmethod def _init_global_parser(parser): parser.add_argument("-v", "--version", help="show version", action="store_true") parser.add_argument( "--in-pod", help="pod creation", metavar="in_pod", type=str, default=None, ) parser.add_argument( "--pod-args", help="custom arguments to be passed to `podman pod`", metavar="pod_args", type=str, default="--infra=false --share=", ) parser.add_argument( "--env-file", help="Specify an alternate environment file", metavar="env_file", type=str, default=".env", ) parser.add_argument( "-f", "--file", help="Specify an compose file (default: docker-compose.yml) or '-' to read from stdin.", metavar="file", action="append", default=[], ) parser.add_argument( "--profile", help="Specify a profile to enable", metavar="profile", action="append", default=[], ) parser.add_argument( "-p", "--project-name", help="Specify an alternate project name (default: directory name)", type=str, default=None, ) parser.add_argument( "--podman-path", help="Specify an alternate path to podman (default: use location in $PATH variable)", type=str, default="podman", ) parser.add_argument( "--podman-args", help="custom global arguments to be passed to `podman`", metavar="args", action="append", default=[], ) for podman_cmd in PODMAN_CMDS: parser.add_argument( f"--podman-{podman_cmd}-args", help=f"custom arguments to be passed to `podman {podman_cmd}`", metavar="args", action="append", default=[], ) parser.add_argument( "--no-ansi", help="Do not print ANSI control characters", action="store_true", ) parser.add_argument( "--no-cleanup", help="Do not stop and remove existing pod & containers", action="store_true", ) parser.add_argument( "--dry-run", help="No action; perform a simulation of commands", action="store_true", ) parser.add_argument( "--parallel", type=int, default=os.environ.get("COMPOSE_PARALLEL_LIMIT", sys.maxsize) ) parser.add_argument( "--verbose", help="Print debugging output", action="store_true", ) podman_compose = PodmanCompose() ################### # decorators to add commands and parse options ################### class PodmanComposeError(Exception): pass class cmd_run: # pylint: disable=invalid-name,too-few-public-methods def __init__(self, compose, cmd_name, cmd_desc=None): self.compose = compose self.cmd_name = cmd_name self.cmd_desc = cmd_desc def __call__(self, func): def wrapped(*args, **kw): return func(*args, **kw) if not asyncio.iscoroutinefunction(func): raise PodmanComposeError("Command must be async") wrapped._compose = self.compose # Trim extra indentation at start of multiline docstrings. wrapped.desc = self.cmd_desc or re.sub(r"^\s+", "", func.__doc__) wrapped._parse_args = [] self.compose.commands[self.cmd_name] = wrapped return wrapped class cmd_parse: # pylint: disable=invalid-name,too-few-public-methods def __init__(self, compose, cmd_names): self.compose = compose self.cmd_names = cmd_names if is_list(cmd_names) else [cmd_names] def __call__(self, func): def wrapped(*args, **kw): return func(*args, **kw) for cmd_name in self.cmd_names: self.compose.commands[cmd_name]._parse_args.append(wrapped) return wrapped ################### # actual commands ################### @cmd_run(podman_compose, "version", "show version") async def compose_version(compose, args): if getattr(args, "short", False): print(__version__) return if getattr(args, "format", "pretty") == "json": res = {"version": __version__} print(json.dumps(res)) return print("podman-compose version", __version__) await compose.podman.run(["--version"], "", []) def is_local(container: dict) -> bool: """Test if a container is local, i.e. if it is * prefixed with localhost/ * has a build section and is not prefixed """ return ( "/" not in container["image"] if "build" in container else container["image"].startswith("localhost/") ) @cmd_run(podman_compose, "wait", "wait running containers to stop") async def compose_wait(compose, args): # pylint: disable=unused-argument containers = [cnt["name"] for cnt in compose.containers] cmd_args = ["--"] cmd_args.extend(containers) await compose.podman.exec([], "wait", cmd_args) @cmd_run(podman_compose, "systemd") async def compose_systemd(compose, args): """ create systemd unit file and register its compose stacks When first installed type `sudo podman-compose systemd -a create-unit` later you can add a compose stack by running `podman-compose systemd -a register` then you can start/stop your stack with `systemctl --user start podman-compose@` """ stacks_dir = ".config/containers/compose/projects" if args.action == "register": proj_name = compose.project_name fn = os.path.expanduser(f"~/{stacks_dir}/{proj_name}.env") os.makedirs(os.path.dirname(fn), exist_ok=True) log.debug("writing [%s]: ...", fn) with open(fn, "w", encoding="utf-8") as f: for k, v in compose.environ.items(): if k.startswith("COMPOSE_") or k.startswith("PODMAN_"): f.write(f"{k}={v}\n") log.debug("writing [%s]: done.", fn) log.info("\n\ncreating the pod without starting it: ...\n\n") username = getpass.getuser() print( f""" you can use systemd commands like enable, start, stop, status, cat all without `sudo` like this: \t\tsystemctl --user enable --now 'podman-compose@{proj_name}' \t\tsystemctl --user status 'podman-compose@{proj_name}' \t\tjournalctl --user -xeu 'podman-compose@{proj_name}' and for that to work outside a session you might need to run the following command *once* \t\tsudo loginctl enable-linger '{username}' you can use podman commands like: \t\tpodman pod ps \t\tpodman pod stats 'pod_{proj_name}' \t\tpodman pod logs --tail=10 -f 'pod_{proj_name}' """ ) elif args.action in ("list", "ls"): ls = glob.glob(os.path.expanduser(f"~/{stacks_dir}/*.env")) for i in ls: print(os.path.basename(i[:-4])) elif args.action == "create-unit": fn = "/etc/systemd/user/podman-compose@.service" out = f"""\ # {fn} [Unit] Description=%i rootless pod (podman-compose) [Service] Type=simple EnvironmentFile=%h/{stacks_dir}/%i.env ExecStartPre=-{script} up --no-start ExecStartPre=/usr/bin/podman pod start pod_%i ExecStart={script} wait ExecStop=/usr/bin/podman pod stop pod_%i [Install] WantedBy=default.target """ if os.access(os.path.dirname(fn), os.W_OK): log.debug("writing [%s]: ...", fn) with open(fn, "w", encoding="utf-8") as f: f.write(out) log.debug("writing [%s]: done.", fn) print( """ while in your project type `podman-compose systemd -a register` """ ) else: print(out) log.warning("Could not write to [%s], use 'sudo'", fn) @cmd_run(podman_compose, "pull", "pull stack images") async def compose_pull(compose, args): img_containers = [cnt for cnt in compose.containers if "image" in cnt] if args.services: services = set(args.services) img_containers = [cnt for cnt in img_containers if cnt["_service"] in services] images = {cnt["image"] for cnt in img_containers} if not args.force_local: local_images = {cnt["image"] for cnt in img_containers if is_local(cnt)} images -= local_images await asyncio.gather(*[compose.podman.run([], "pull", [image]) for image in images]) @cmd_run(podman_compose, "push", "push stack images") async def compose_push(compose, args): services = set(args.services) for cnt in compose.containers: if "build" not in cnt: continue if services and cnt["_service"] not in services: continue await compose.podman.run([], "push", [cnt["image"]]) async def build_one(compose, args, cnt): if "build" not in cnt: return None if getattr(args, "if_not_exists", None): try: img_id = await compose.podman.output( [], "inspect", ["-t", "image", "-f", "{{.Id}}", cnt["image"]] ) except subprocess.CalledProcessError: img_id = None if img_id: return None build_desc = cnt["build"] if not hasattr(build_desc, "items"): build_desc = {"context": build_desc} ctx = build_desc.get("context", ".") dockerfile = build_desc.get("dockerfile", None) if dockerfile: dockerfile = os.path.join(ctx, dockerfile) else: dockerfile_alts = [ "Containerfile", "ContainerFile", "containerfile", "Dockerfile", "DockerFile", "dockerfile", ] for dockerfile in dockerfile_alts: dockerfile = os.path.join(ctx, dockerfile) if os.path.exists(dockerfile): break if not os.path.exists(dockerfile): raise OSError("Dockerfile not found in " + ctx) build_args = ["-f", dockerfile, "-t", cnt["image"]] for secret in build_desc.get("secrets", []): build_args.extend(get_secret_args(compose, cnt, secret, podman_is_building=True)) for tag in build_desc.get("tags", []): build_args.extend(["-t", tag]) for additional_ctx in build_desc.get("additional_contexts", {}): build_args.extend([f"--build-context={additional_ctx}"]) if "target" in build_desc: build_args.extend(["--target", build_desc["target"]]) container_to_ulimit_build_args(cnt, build_args) if getattr(args, "no_cache", None): build_args.append("--no-cache") if getattr(args, "pull_always", None): build_args.append("--pull-always") elif getattr(args, "pull", None): build_args.append("--pull") env = dict(cnt.get("environment", {})) for name, value in env.items(): build_args += ["--env", f"{name}" if value is None else f"{name}={value}"] args_list = norm_as_list(build_desc.get("args", {})) for build_arg in args_list + args.build_arg: build_args.extend(( "--build-arg", build_arg, )) build_args.append(ctx) status = await compose.podman.run([], "build", build_args) return status @cmd_run(podman_compose, "build", "build stack images") async def compose_build(compose, args): tasks = [] if args.services: container_names_by_service = compose.container_names_by_service compose.assert_services(args.services) for service in args.services: cnt = compose.container_by_name[container_names_by_service[service][0]] tasks.append(asyncio.create_task(build_one(compose, args, cnt))) else: for cnt in compose.containers: tasks.append(asyncio.create_task(build_one(compose, args, cnt))) status = 0 for t in asyncio.as_completed(tasks): s = await t if s is not None: status = s return status async def create_pods(compose, args): # pylint: disable=unused-argument for pod in compose.pods: podman_args = [ "create", "--name=" + pod["name"], ] if args.pod_args: podman_args.extend(shlex.split(args.pod_args)) # if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"): # podman_args.append("--infra-name={}_infra".format(pod["name"])) ports = pod.get("ports", None) or [] if isinstance(ports, str): ports = [ports] for i in ports: podman_args.extend(["-p", str(i)]) await compose.podman.run([], "pod", podman_args) def get_excluded(compose, args): excluded = set() if args.services: excluded = set(compose.services) for service in args.services: excluded -= compose.services[service]["_deps"] excluded.discard(service) log.debug("** excluding: %s", excluded) return excluded @cmd_run(podman_compose, "up", "Create and start the entire stack or some of its services") async def compose_up(compose: PodmanCompose, args): proj_name = compose.project_name excluded = get_excluded(compose, args) if not args.no_build: # `podman build` does not cache, so don't always build build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__) if await compose.commands["build"](compose, build_args) != 0: log.error("Build command failed") hashes = ( ( await compose.podman.output( [], "ps", [ "--filter", f"label=io.podman.compose.project={proj_name}", "-a", "--format", '{{ index .Labels "io.podman.compose.config-hash"}}', ], ) ) .decode("utf-8") .splitlines() ) diff_hashes = [i for i in hashes if i and i != compose.yaml_hash] if args.force_recreate or len(diff_hashes): log.info("recreating: ...") down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) await compose.commands["down"](compose, down_args) log.info("recreating: done\n\n") # args.no_recreate disables check for changes (which is not implemented) podman_command = "run" if args.detach and not args.no_start else "create" await create_pods(compose, args) for cnt in compose.containers: if cnt["_service"] in excluded: log.debug("** skipping: %s", cnt["name"]) continue podman_args = await container_to_args(compose, cnt, detached=args.detach) subproc = await compose.podman.run([], podman_command, podman_args) if podman_command == "run" and subproc is not None: await compose.podman.run([], "start", [cnt["name"]]) if args.no_start or args.detach or args.dry_run: return # TODO: handle already existing # TODO: if error creating do not enter loop # TODO: colors if sys.stdout.isatty() exit_code_from = args.__dict__.get("exit_code_from", None) if exit_code_from: args.abort_on_container_exit = True max_service_length = 0 for cnt in compose.containers: curr_length = len(cnt["_service"]) max_service_length = curr_length if curr_length > max_service_length else max_service_length tasks = set() loop = asyncio.get_event_loop() loop.add_signal_handler(signal.SIGINT, lambda: [t.cancel("User exit") for t in tasks]) for i, cnt in enumerate(compose.containers): # Add colored service prefix to output by piping output through sed color_idx = i % len(compose.console_colors) color = compose.console_colors[color_idx] space_suffix = " " * (max_service_length - len(cnt["_service"]) + 1) log_formatter = "{}[{}]{}|\x1b[0m".format(color, cnt["_service"], space_suffix) if cnt["_service"] in excluded: log.debug("** skipping: %s", cnt["name"]) continue tasks.add( asyncio.create_task( compose.podman.run([], "start", ["-a", cnt["name"]], log_formatter=log_formatter), name=cnt["_service"], ) ) def _task_cancelled(task: Task) -> bool: if task.cancelled(): return True # Task.cancelling() is new in python 3.11 if sys.version_info >= (3, 11) and task.cancelling(): return True return False exit_code = 0 exiting = False while tasks: done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) if args.abort_on_container_exit: if not exiting: # If 2 containers exit at the exact same time, the cancellation of the other ones # cause the status to overwrite. Sleeping for 1 seems to fix this and make it match # docker-compose await asyncio.sleep(1) for t in tasks: if not _task_cancelled(t): t.cancel() t: Task exiting = True for t in done: if t.get_name() == exit_code_from: exit_code = t.result() return exit_code def get_volume_names(compose, cnt): proj_name = compose.project_name basedir = compose.dirname srv_name = cnt["_service"] ls = [] for volume in cnt.get("volumes", []): if is_str(volume): volume = parse_short_mount(volume, basedir) volume = fix_mount_dict(compose, volume, proj_name, srv_name) mount_type = volume["type"] if mount_type != "volume": continue volume_name = (volume.get("_vol", None) or {}).get("name", None) ls.append(volume_name) return ls @cmd_run(podman_compose, "down", "tear down entire stack") async def compose_down(compose, args): excluded = get_excluded(compose, args) podman_args = [] timeout_global = getattr(args, "timeout", None) containers = list(reversed(compose.containers)) down_tasks = [] for cnt in containers: if cnt["_service"] in excluded: continue podman_stop_args = [*podman_args] timeout = timeout_global if timeout is None: timeout_str = cnt.get("stop_grace_period", None) or STOP_GRACE_PERIOD timeout = str_to_seconds(timeout_str) if timeout is not None: podman_stop_args.extend(["-t", str(timeout)]) down_tasks.append( asyncio.create_task( compose.podman.run([], "stop", [*podman_stop_args, cnt["name"]]), name=cnt["name"] ) ) await asyncio.gather(*down_tasks) for cnt in containers: if cnt["_service"] in excluded: continue await compose.podman.run([], "rm", [cnt["name"]]) if args.remove_orphans: names = ( ( await compose.podman.output( [], "ps", [ "--filter", f"label=io.podman.compose.project={compose.project_name}", "-a", "--format", "{{ .Names }}", ], ) ) .decode("utf-8") .splitlines() ) for name in names: await compose.podman.run([], "stop", [*podman_args, name]) for name in names: await compose.podman.run([], "rm", [name]) if args.volumes: vol_names_to_keep = set() for cnt in containers: if cnt["_service"] not in excluded: continue vol_names_to_keep.update(get_volume_names(compose, cnt)) log.debug("keep %s", vol_names_to_keep) for volume_name in await compose.podman.volume_ls(): if volume_name in vol_names_to_keep: continue await compose.podman.run([], "volume", ["rm", volume_name]) if excluded: return for pod in compose.pods: await compose.podman.run([], "pod", ["rm", pod["name"]]) @cmd_run(podman_compose, "ps", "show status of containers") async def compose_ps(compose, args): proj_name = compose.project_name ps_args = ["-a", "--filter", f"label=io.podman.compose.project={proj_name}"] if args.quiet is True: ps_args.extend(["--format", "{{.ID}}"]) elif args.format: ps_args.extend(["--format", args.format]) await compose.podman.run( [], "ps", ps_args, ) @cmd_run( podman_compose, "run", "create a container similar to a service to run a one-off command", ) async def compose_run(compose, args): await create_pods(compose, args) compose.assert_services(args.service) container_names = compose.container_names_by_service[args.service] container_name = container_names[0] cnt = dict(compose.container_by_name[container_name]) deps = cnt["_deps"] if deps and not args.no_deps: up_args = argparse.Namespace( **dict( args.__dict__, detach=True, services=deps, # defaults no_build=False, build=None, force_recreate=False, no_start=False, no_cache=False, build_arg=[], parallel=1, remove_orphans=True, ) ) await compose.commands["up"](compose, up_args) build_args = argparse.Namespace( services=[args.service], if_not_exists=(not args.build), build_arg=[], **args.__dict__ ) await compose.commands["build"](compose, build_args) compose_run_update_container_from_args(compose, cnt, args) # run podman podman_args = await container_to_args(compose, cnt, args.detach) if not args.detach: podman_args.insert(1, "-i") if args.rm: podman_args.insert(1, "--rm") p = await compose.podman.run([], "run", podman_args) sys.exit(p) def compose_run_update_container_from_args(compose, cnt, args): # adjust one-off container options name0 = "{}_{}_tmp{}".format(compose.project_name, args.service, random.randrange(0, 65536)) cnt["name"] = args.name or name0 if args.entrypoint: cnt["entrypoint"] = args.entrypoint if args.user: cnt["user"] = args.user if args.workdir: cnt["working_dir"] = args.workdir env = dict(cnt.get("environment", {})) if args.env: additional_env_vars = dict(map(lambda each: each.split("=", maxsplit=1), args.env)) env.update(additional_env_vars) cnt["environment"] = env if not args.service_ports: for k in ("expose", "publishall", "ports"): try: del cnt[k] except KeyError: pass if args.publish: ports = cnt.get("ports", []) ports.extend(norm_ports(args.publish)) cnt["ports"] = ports if args.volume: # TODO: handle volumes volumes = clone(cnt.get("volumes", None) or []) volumes.extend(args.volume) cnt["volumes"] = volumes cnt["tty"] = not args.T if args.cnt_command is not None and len(args.cnt_command) > 0: cnt["command"] = args.cnt_command # can't restart and --rm if args.rm and "restart" in cnt: del cnt["restart"] @cmd_run(podman_compose, "exec", "execute a command in a running container") async def compose_exec(compose, args): compose.assert_services(args.service) container_names = compose.container_names_by_service[args.service] container_name = container_names[args.index - 1] cnt = compose.container_by_name[container_name] podman_args = compose_exec_args(cnt, container_name, args) p = await compose.podman.run([], "exec", podman_args) sys.exit(p) def compose_exec_args(cnt, container_name, args): podman_args = ["--interactive"] if args.privileged: podman_args += ["--privileged"] if args.user: podman_args += ["--user", args.user] if args.workdir: podman_args += ["--workdir", args.workdir] if not args.T: podman_args += ["--tty"] env = dict(cnt.get("environment", {})) if args.env: additional_env_vars = dict( map(lambda each: each.split("=", maxsplit=1) if "=" in each else (each, None), args.env) ) env.update(additional_env_vars) for name, value in env.items(): podman_args += ["--env", f"{name}" if value is None else f"{name}={value}"] podman_args += [container_name] if args.cnt_command is not None and len(args.cnt_command) > 0: podman_args += args.cnt_command return podman_args async def transfer_service_status(compose, args, action): # TODO: handle dependencies, handle creations container_names_by_service = compose.container_names_by_service if not args.services: args.services = container_names_by_service.keys() compose.assert_services(args.services) targets = [] for service in args.services: if service not in container_names_by_service: raise ValueError("unknown service: " + service) targets.extend(container_names_by_service[service]) if action in ["stop", "restart"]: targets = list(reversed(targets)) timeout_global = getattr(args, "timeout", None) tasks = [] for target in targets: podman_args = [] if action != "start": timeout = timeout_global if timeout is None: timeout_str = ( compose.container_by_name[target].get("stop_grace_period", None) or STOP_GRACE_PERIOD ) timeout = str_to_seconds(timeout_str) if timeout is not None: podman_args.extend(["-t", str(timeout)]) tasks.append(asyncio.create_task(compose.podman.run([], action, podman_args + [target]))) await asyncio.gather(*tasks) @cmd_run(podman_compose, "start", "start specific services") async def compose_start(compose, args): await transfer_service_status(compose, args, "start") @cmd_run(podman_compose, "stop", "stop specific services") async def compose_stop(compose, args): await transfer_service_status(compose, args, "stop") @cmd_run(podman_compose, "restart", "restart specific services") async def compose_restart(compose, args): await transfer_service_status(compose, args, "restart") @cmd_run(podman_compose, "logs", "show logs from services") async def compose_logs(compose, args): container_names_by_service = compose.container_names_by_service if not args.services and not args.latest: args.services = container_names_by_service.keys() compose.assert_services(args.services) targets = [] for service in args.services: targets.extend(container_names_by_service[service]) podman_args = [] if args.follow: podman_args.append("-f") if args.latest: podman_args.append("-l") if args.names: podman_args.append("-n") if args.since: podman_args.extend(["--since", args.since]) # the default value is to print all logs which is in podman = 0 and not # needed to be passed if args.tail and args.tail != "all": podman_args.extend(["--tail", args.tail]) if args.timestamps: podman_args.append("-t") if args.until: podman_args.extend(["--until", args.until]) for target in targets: podman_args.append(target) await compose.podman.run([], "logs", podman_args) @cmd_run(podman_compose, "config", "displays the compose file") async def compose_config(compose, args): if args.services: for service in compose.services: print(service) return print(compose.merged_yaml) @cmd_run(podman_compose, "port", "Prints the public port for a port binding.") async def compose_port(compose, args): # TODO - deal with pod index compose.assert_services(args.service) containers = compose.container_names_by_service[args.service] container_ports = list( itertools.chain(*(compose.container_by_name[c]["ports"] for c in containers)) ) def _published_target(port_string): published, target = port_string.split(":")[-2:] return int(published), int(target) select_udp = args.protocol == "udp" published, target = None, None for p in container_ports: is_udp = p[-4:] == "/udp" if select_udp and is_udp: published, target = _published_target(p[-4:]) if not select_udp and not is_udp: published, target = _published_target(p) if target == args.private_port: print(published) return @cmd_run(podman_compose, "pause", "Pause all running containers") async def compose_pause(compose, args): container_names_by_service = compose.container_names_by_service if not args.services: args.services = container_names_by_service.keys() targets = [] for service in args.services: targets.extend(container_names_by_service[service]) await compose.podman.run([], "pause", targets) @cmd_run(podman_compose, "unpause", "Unpause all running containers") async def compose_unpause(compose, args): container_names_by_service = compose.container_names_by_service if not args.services: args.services = container_names_by_service.keys() targets = [] for service in args.services: targets.extend(container_names_by_service[service]) await compose.podman.run([], "unpause", targets) @cmd_run(podman_compose, "kill", "Kill one or more running containers with a specific signal") async def compose_kill(compose, args): # to ensure that the user did not execute the command by mistake if not args.services and not args.all: log.fatal( "Error: you must provide at least one service name or use (--all) to kill all services" ) sys.exit() container_names_by_service = compose.container_names_by_service podman_args = [] if args.signal: podman_args.extend(["--signal", args.signal]) if args.all is True: services = container_names_by_service.keys() targets = [] for service in services: targets.extend(container_names_by_service[service]) for target in targets: podman_args.append(target) await compose.podman.run([], "kill", podman_args) elif args.services: targets = [] for service in args.services: targets.extend(container_names_by_service[service]) for target in targets: podman_args.append(target) await compose.podman.run([], "kill", podman_args) @cmd_run( podman_compose, "stats", "Display percentage of CPU, memory, network I/O, block I/O and PIDs for services.", ) async def compose_stats(compose, args): container_names_by_service = compose.container_names_by_service if not args.services: args.services = container_names_by_service.keys() targets = [] podman_args = [] if args.interval: podman_args.extend(["--interval", args.interval]) if args.format: podman_args.extend(["--format", args.format]) if args.no_reset: podman_args.append("--no-reset") if args.no_stream: podman_args.append("--no-stream") for service in args.services: targets.extend(container_names_by_service[service]) for target in targets: podman_args.append(target) try: await compose.podman.run([], "stats", podman_args) except KeyboardInterrupt: pass @cmd_run(podman_compose, "images", "List images used by the created containers") async def compose_images(compose, args): img_containers = [cnt for cnt in compose.containers if "image" in cnt] data = [] if args.quiet is True: for img in img_containers: name = img["name"] output = await compose.podman.output([], "images", ["--quiet", img["image"]]) data.append(output.decode("utf-8").split()) else: data.append(["CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE", ""]) for img in img_containers: name = img["name"] output = await compose.podman.output( [], "images", [ "--format", "table " + name + " {{.Repository}} {{.Tag}} {{.ID}} {{.Size}}", "-n", img["image"], ], ) data.append(output.decode("utf-8").split()) # Determine the maximum length of each column column_widths = [max(map(len, column)) for column in zip(*data)] # Print each row for row in data: # Format each cell using the maximum column width formatted_row = [cell.ljust(width) for cell, width in zip(row, column_widths)] formatted_row[-2:] = ["".join(formatted_row[-2:]).strip()] print("\t".join(formatted_row)) ################### # command arguments parsing ################### @cmd_parse(podman_compose, "version") def compose_version_parse(parser): parser.add_argument( "-f", "--format", choices=["pretty", "json"], default="pretty", help="Format the output", ) parser.add_argument( "--short", action="store_true", help="Shows only Podman Compose's version number", ) @cmd_parse(podman_compose, "up") def compose_up_parse(parser): parser.add_argument( "-d", "--detach", action="store_true", help="Detached mode: Run container in the background, print new container name. \ Incompatible with --abort-on-container-exit.", ) parser.add_argument("--no-color", action="store_true", help="Produce monochrome output.") parser.add_argument( "--quiet-pull", action="store_true", help="Pull without printing progress information.", ) parser.add_argument("--no-deps", action="store_true", help="Don't start linked services.") parser.add_argument( "--force-recreate", action="store_true", help="Recreate containers even if their configuration and image haven't changed.", ) parser.add_argument( "--always-recreate-deps", action="store_true", help="Recreate dependent containers. Incompatible with --no-recreate.", ) parser.add_argument( "--no-recreate", action="store_true", help="If containers already exist, don't recreate them. Incompatible with --force-recreate " "and -V.", ) parser.add_argument( "--no-build", action="store_true", help="Don't build an image, even if it's missing.", ) parser.add_argument( "--no-start", action="store_true", help="Don't start the services after creating them.", ) parser.add_argument( "--build", action="store_true", help="Build images before starting containers." ) parser.add_argument( "--abort-on-container-exit", action="store_true", help="Stops all containers if any container was stopped. Incompatible with -d.", ) parser.add_argument( "-t", "--timeout", type=int, default=None, help="Use this timeout in seconds for container shutdown when attached or when containers " "are already running. (default: 10)", ) parser.add_argument( "-V", "--renew-anon-volumes", action="store_true", help="Recreate anonymous volumes instead of retrieving data from the previous containers.", ) parser.add_argument( "--remove-orphans", action="store_true", help="Remove containers for services not defined in the Compose file.", ) parser.add_argument( "--scale", metavar="SERVICE=NUM", action="append", help="Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if " "present.", ) parser.add_argument( "--exit-code-from", metavar="SERVICE", type=str, default=None, help="Return the exit code of the selected service container. " "Implies --abort-on-container-exit.", ) @cmd_parse(podman_compose, "down") def compose_down_parse(parser): parser.add_argument( "-v", "--volumes", action="store_true", default=False, help="Remove named volumes declared in the `volumes` section of the Compose file and " "anonymous volumes attached to containers.", ) parser.add_argument( "--remove-orphans", action="store_true", help="Remove containers for services not defined in the Compose file.", ) @cmd_parse(podman_compose, "run") def compose_run_parse(parser): parser.add_argument( "--build", action="store_true", help="Build images before starting containers." ) parser.add_argument( "-d", "--detach", action="store_true", help="Detached mode: Run container in the background, print new container name.", ) parser.add_argument("--name", type=str, default=None, help="Assign a name to the container") parser.add_argument( "--entrypoint", type=str, default=None, help="Override the entrypoint of the image.", ) parser.add_argument( "-e", "--env", metavar="KEY=VAL", action="append", help="Set an environment variable (can be used multiple times)", ) parser.add_argument( "-l", "--label", metavar="KEY=VAL", action="append", help="Add or override a label (can be used multiple times)", ) parser.add_argument( "-u", "--user", type=str, default=None, help="Run as specified username or uid" ) parser.add_argument("--no-deps", action="store_true", help="Don't start linked services") parser.add_argument( "--rm", action="store_true", help="Remove container after run. Ignored in detached mode.", ) parser.add_argument( "-p", "--publish", action="append", help="Publish a container's port(s) to the host (can be used multiple times)", ) parser.add_argument( "--service-ports", action="store_true", help="Run command with the service's ports enabled and mapped to the host.", ) parser.add_argument( "-v", "--volume", action="append", help="Bind mount a volume (can be used multiple times)", ) parser.add_argument( "-T", action="store_true", help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.", ) parser.add_argument( "-w", "--workdir", type=str, default=None, help="Working directory inside the container", ) parser.add_argument("service", metavar="service", nargs=None, help="service name") parser.add_argument( "cnt_command", metavar="command", nargs=argparse.REMAINDER, help="command and its arguments", ) @cmd_parse(podman_compose, "exec") def compose_exec_parse(parser): parser.add_argument( "-d", "--detach", action="store_true", help="Detached mode: Run container in the background, print new container name.", ) parser.add_argument( "--privileged", action="store_true", default=False, help="Give the process extended Linux capabilities inside the container", ) parser.add_argument( "-u", "--user", type=str, default=None, help="Run as specified username or uid" ) parser.add_argument( "-T", action="store_true", help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.", ) parser.add_argument( "--index", type=int, default=1, help="Index of the container if there are multiple instances of a service", ) parser.add_argument( "-e", "--env", metavar="KEY=VAL", action="append", help="Set an environment variable (can be used multiple times)", ) parser.add_argument( "-w", "--workdir", type=str, default=None, help="Working directory inside the container", ) parser.add_argument("service", metavar="service", nargs=None, help="service name") parser.add_argument( "cnt_command", metavar="command", nargs=argparse.REMAINDER, help="command and its arguments", ) @cmd_parse(podman_compose, ["down", "stop", "restart"]) def compose_parse_timeout(parser): parser.add_argument( "-t", "--timeout", help="Specify a shutdown timeout in seconds. ", type=int, default=None, ) @cmd_parse(podman_compose, ["logs"]) def compose_logs_parse(parser): parser.add_argument( "-f", "--follow", action="store_true", help="Follow log output. The default is false", ) parser.add_argument( "-l", "--latest", action="store_true", help="Act on the latest container podman is aware of", ) parser.add_argument( "-n", "--names", action="store_true", help="Output the container name in the log", ) parser.add_argument("--since", help="Show logs since TIMESTAMP", type=str, default=None) parser.add_argument("-t", "--timestamps", action="store_true", help="Show timestamps.") parser.add_argument( "--tail", help="Number of lines to show from the end of the logs for each " "container.", type=str, default="all", ) parser.add_argument("--until", help="Show logs until TIMESTAMP", type=str, default=None) parser.add_argument( "services", metavar="services", nargs="*", default=None, help="service names" ) @cmd_parse(podman_compose, "systemd") def compose_systemd_parse(parser): parser.add_argument( "-a", "--action", choices=["register", "create-unit", "list", "ls"], default="register", help="create systemd unit file or register compose stack to it", ) @cmd_parse(podman_compose, "pull") def compose_pull_parse(parser): parser.add_argument( "--force-local", action="store_true", default=False, help="Also pull unprefixed images for services which have a build section", ) parser.add_argument("services", metavar="services", nargs="*", help="services to pull") @cmd_parse(podman_compose, "push") def compose_push_parse(parser): parser.add_argument( "--ignore-push-failures", action="store_true", help="Push what it can and ignores images with push failures. (not implemented)", ) parser.add_argument("services", metavar="services", nargs="*", help="services to push") @cmd_parse(podman_compose, "ps") def compose_ps_parse(parser): parser.add_argument("-q", "--quiet", help="Only display container IDs", action="store_true") @cmd_parse(podman_compose, ["build", "up"]) def compose_build_up_parse(parser): parser.add_argument( "--pull", help="attempt to pull a newer version of the image", action="store_true", ) parser.add_argument( "--pull-always", help="attempt to pull a newer version of the image, Raise an error even if the image is " "present locally.", action="store_true", ) parser.add_argument( "--build-arg", metavar="key=val", action="append", default=[], help="Set build-time variables for services.", ) parser.add_argument( "--no-cache", help="Do not use cache when building the image.", action="store_true", ) @cmd_parse(podman_compose, ["build", "up", "down", "start", "stop", "restart"]) def compose_build_parse(parser): parser.add_argument( "services", metavar="services", nargs="*", default=None, help="affected services", ) @cmd_parse(podman_compose, "config") def compose_config_parse(parser): parser.add_argument( "--no-normalize", help="Don't normalize compose model.", action="store_true" ) parser.add_argument( "--services", help="Print the service names, one per line.", action="store_true" ) @cmd_parse(podman_compose, "port") def compose_port_parse(parser): parser.add_argument( "--index", type=int, default=1, help="index of the container if there are multiple instances of a service", ) parser.add_argument( "--protocol", choices=["tcp", "udp"], default="tcp", help="tcp or udp", ) parser.add_argument("service", metavar="service", nargs=None, help="service name") parser.add_argument( "private_port", metavar="private_port", nargs=None, type=int, help="private port", ) @cmd_parse(podman_compose, ["pause", "unpause"]) def compose_pause_unpause_parse(parser): parser.add_argument( "services", metavar="services", nargs="*", default=None, help="service names" ) @cmd_parse(podman_compose, ["kill"]) def compose_kill_parse(parser): parser.add_argument( "services", metavar="services", nargs="*", default=None, help="service names" ) parser.add_argument( "-s", "--signal", type=str, help="Signal to send to the container (default 'KILL')", ) parser.add_argument( "-a", "--all", help="Signal all running containers", action="store_true", ) @cmd_parse(podman_compose, "images") def compose_images_parse(parser): parser.add_argument("-q", "--quiet", help="Only display images IDs", action="store_true") @cmd_parse(podman_compose, ["stats"]) def compose_stats_parse(parser): parser.add_argument( "services", metavar="services", nargs="*", default=None, help="service names" ) parser.add_argument( "-i", "--interval", type=int, help="Time in seconds between stats reports (default 5)", ) parser.add_argument( "--no-reset", help="Disable resetting the screen between intervals", action="store_true", ) parser.add_argument( "--no-stream", help="Disable streaming stats and only pull the first result", action="store_true", ) @cmd_parse(podman_compose, ["ps", "stats"]) def compose_format_parse(parser): parser.add_argument( "-f", "--format", type=str, help="Pretty-print container statistics to JSON or using a Go template", ) async def async_main(): await podman_compose.run() def main(): asyncio.run(async_main()) if __name__ == "__main__": main() podman-compose-1.2.0/pyproject.toml000066400000000000000000000003711463674324000173610ustar00rootroot00000000000000[tool.ruff] line-length = 100 target-version = "py38" [tool.ruff.lint] select = ["W", "E", "F", "I"] ignore = [ ] [tool.ruff.lint.isort] force-single-line = true [tool.ruff.format] preview = true # needed for quote-style quote-style = "preserve" podman-compose-1.2.0/pytests/000077500000000000000000000000001463674324000161575ustar00rootroot00000000000000podman-compose-1.2.0/pytests/__init__.py000066400000000000000000000000001463674324000202560ustar00rootroot00000000000000podman-compose-1.2.0/pytests/test_can_merge_build.py000066400000000000000000000155111463674324000226720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 from __future__ import annotations import argparse import copy import os import unittest import yaml from parameterized import parameterized from podman_compose import PodmanCompose class TestCanMergeBuild(unittest.TestCase): @parameterized.expand([ ({}, {}, {}), ({}, {"test": "test"}, {"test": "test"}), ({"test": "test"}, {}, {"test": "test"}), ({"test": "test-1"}, {"test": "test-2"}, {"test": "test-2"}), ({}, {"build": "."}, {"build": {"context": "."}}), ({"build": "."}, {}, {"build": {"context": "."}}), ({"build": "./dir-1"}, {"build": "./dir-2"}, {"build": {"context": "./dir-2"}}), ({}, {"build": {"context": "./dir-1"}}, {"build": {"context": "./dir-1"}}), ({"build": {"context": "./dir-1"}}, {}, {"build": {"context": "./dir-1"}}), ( {"build": {"context": "./dir-1"}}, {"build": {"context": "./dir-2"}}, {"build": {"context": "./dir-2"}}, ), ( {}, {"build": {"dockerfile": "dockerfile-1"}}, {"build": {"dockerfile": "dockerfile-1"}}, ), ( {"build": {"dockerfile": "dockerfile-1"}}, {}, {"build": {"dockerfile": "dockerfile-1"}}, ), ( {"build": {"dockerfile": "./dockerfile-1"}}, {"build": {"dockerfile": "./dockerfile-2"}}, {"build": {"dockerfile": "./dockerfile-2"}}, ), ( {"build": {"dockerfile": "./dockerfile-1"}}, {"build": {"context": "./dir-2"}}, {"build": {"dockerfile": "./dockerfile-1", "context": "./dir-2"}}, ), ( {"build": {"dockerfile": "./dockerfile-1", "context": "./dir-1"}}, {"build": {"dockerfile": "./dockerfile-2", "context": "./dir-2"}}, {"build": {"dockerfile": "./dockerfile-2", "context": "./dir-2"}}, ), ( {"build": {"dockerfile": "./dockerfile-1"}}, {"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}}, {"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}}, ), ( {"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}}, {"build": {"dockerfile": "./dockerfile-1"}}, {"build": {"dockerfile": "./dockerfile-1", "args": ["ENV1=1"]}}, ), ( {"build": {"dockerfile": "./dockerfile-2", "args": ["ENV1=1"]}}, {"build": {"dockerfile": "./dockerfile-1", "args": ["ENV2=2"]}}, {"build": {"dockerfile": "./dockerfile-1", "args": ["ENV1=1", "ENV2=2"]}}, ), ]) def test_parse_compose_file_when_multiple_composes(self, input, override, expected): compose_test_1 = {"services": {"test-service": input}} compose_test_2 = {"services": {"test-service": override}} dump_yaml(compose_test_1, "test-compose-1.yaml") dump_yaml(compose_test_2, "test-compose-2.yaml") podman_compose = PodmanCompose() set_args(podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"]) podman_compose._parse_compose_file() # pylint: disable=protected-access actual_compose = {} if podman_compose.services: podman_compose.services["test-service"].pop("_deps") actual_compose = podman_compose.services["test-service"] self.assertEqual(actual_compose, expected) # $$$ is a placeholder for either command or entrypoint @parameterized.expand([ ({}, {"$$$": []}, {"$$$": []}), ({"$$$": []}, {}, {"$$$": []}), ({"$$$": []}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), ({"$$$": "sh-2"}, {"$$$": []}, {"$$$": []}), ({}, {"$$$": "sh"}, {"$$$": ["sh"]}), ({"$$$": "sh"}, {}, {"$$$": ["sh"]}), ({"$$$": "sh-1"}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), ({"$$$": ["sh-1"]}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), ({"$$$": "sh-1"}, {"$$$": ["sh-2"]}, {"$$$": ["sh-2"]}), ({"$$$": "sh-1"}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}), ({"$$$": ["sh-1"]}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}), ({"$$$": ["sh-1", "sh-2"]}, {"$$$": ["sh-3", "sh-4"]}, {"$$$": ["sh-3", "sh-4"]}), ({}, {"$$$": ["sh-3", "sh 4"]}, {"$$$": ["sh-3", "sh 4"]}), ({"$$$": "sleep infinity"}, {"$$$": "sh"}, {"$$$": ["sh"]}), ({"$$$": "sh"}, {"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}), ( {}, {"$$$": "bash -c 'sleep infinity'"}, {"$$$": ["bash", "-c", "sleep infinity"]}, ), ]) def test_parse_compose_file_when_multiple_composes_keys_command_entrypoint( self, base_template, override_template, expected_template ): for key in ['command', 'entrypoint']: base, override, expected = template_to_expression( base_template, override_template, expected_template, key ) compose_test_1 = {"services": {"test-service": base}} compose_test_2 = {"services": {"test-service": override}} dump_yaml(compose_test_1, "test-compose-1.yaml") dump_yaml(compose_test_2, "test-compose-2.yaml") podman_compose = PodmanCompose() set_args(podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"]) podman_compose._parse_compose_file() # pylint: disable=protected-access actual = {} if podman_compose.services: podman_compose.services["test-service"].pop("_deps") actual = podman_compose.services["test-service"] self.assertEqual(actual, expected) def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None: podman_compose.global_args = argparse.Namespace() podman_compose.global_args.file = file_names podman_compose.global_args.project_name = None podman_compose.global_args.env_file = None podman_compose.global_args.profile = [] podman_compose.global_args.in_pod_bool = True podman_compose.global_args.no_normalize = True def dump_yaml(compose: dict, name: str) -> None: with open(name, "w", encoding="utf-8") as outfile: yaml.safe_dump(compose, outfile, default_flow_style=False) def template_to_expression(base, override, expected, key): base_copy = copy.deepcopy(base) override_copy = copy.deepcopy(override) expected_copy = copy.deepcopy(expected) expected_copy[key] = expected_copy.pop("$$$") if "$$$" in base: base_copy[key] = base_copy.pop("$$$") if "$$$" in override: override_copy[key] = override_copy.pop("$$$") return base_copy, override_copy, expected_copy def test_clean_test_yamls() -> None: test_files = ["test-compose-1.yaml", "test-compose-2.yaml"] for file in test_files: if os.path.exists(file): os.remove(file) podman-compose-1.2.0/pytests/test_compose_exec_args.py000066400000000000000000000021371463674324000232600ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import argparse import unittest from podman_compose import compose_exec_args class TestComposeExecArgs(unittest.TestCase): def test_minimal(self): cnt = get_minimal_container() args = get_minimal_args() result = compose_exec_args(cnt, "container_name", args) expected = ["--interactive", "--tty", "container_name"] self.assertEqual(result, expected) def test_additional_env_value_equals(self): cnt = get_minimal_container() args = get_minimal_args() args.env = ["key=valuepart1=valuepart2"] result = compose_exec_args(cnt, "container_name", args) expected = [ "--interactive", "--tty", "--env", "key=valuepart1=valuepart2", "container_name", ] self.assertEqual(result, expected) def get_minimal_container(): return {} def get_minimal_args(): return argparse.Namespace( T=None, cnt_command=None, env=None, privileged=None, user=None, workdir=None, ) podman-compose-1.2.0/pytests/test_compose_run_update_container_from_args.py000066400000000000000000000036251463674324000275720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import argparse import unittest from podman_compose import PodmanCompose from podman_compose import compose_run_update_container_from_args class TestComposeRunUpdateContainerFromArgs(unittest.TestCase): def test_minimal(self): cnt = get_minimal_container() compose = get_minimal_compose() args = get_minimal_args() compose_run_update_container_from_args(compose, cnt, args) expected_cnt = {"name": "default_name", "tty": True} self.assertEqual(cnt, expected_cnt) def test_additional_env_value_equals(self): cnt = get_minimal_container() compose = get_minimal_compose() args = get_minimal_args() args.env = ["key=valuepart1=valuepart2"] compose_run_update_container_from_args(compose, cnt, args) expected_cnt = { "environment": { "key": "valuepart1=valuepart2", }, "name": "default_name", "tty": True, } self.assertEqual(cnt, expected_cnt) def test_publish_ports(self): cnt = get_minimal_container() compose = get_minimal_compose() args = get_minimal_args() args.publish = ["1111", "2222:2222"] compose_run_update_container_from_args(compose, cnt, args) expected_cnt = { "name": "default_name", "ports": ["1111", "2222:2222"], "tty": True, } self.assertEqual(cnt, expected_cnt) def get_minimal_container(): return {} def get_minimal_compose(): return PodmanCompose() def get_minimal_args(): return argparse.Namespace( T=None, cnt_command=None, entrypoint=None, env=None, name="default_name", rm=None, service=None, publish=None, service_ports=None, user=None, volume=None, workdir=None, ) podman-compose-1.2.0/pytests/test_container_to_args.py000066400000000000000000000374011463674324000232750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from os import path from unittest import mock from parameterized import parameterized from podman_compose import container_to_args def create_compose_mock(project_name="test_project_name"): compose = mock.Mock() compose.project_name = project_name compose.dirname = "test_dirname" compose.container_names_by_service.get = mock.Mock(return_value=None) compose.prefer_volume_over_mount = False compose.default_net = None compose.networks = {} return compose def get_minimal_container(): return { "name": "project_name_service_name1", "service_name": "service_name", "image": "busybox", } class TestContainerToArgs(unittest.IsolatedAsyncioTestCase): async def test_minimal(self): c = create_compose_mock() cnt = get_minimal_container() args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_runtime(self): c = create_compose_mock() cnt = get_minimal_container() cnt["runtime"] = "runsc" args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--runtime", "runsc", "busybox", ], ) async def test_sysctl_list(self): c = create_compose_mock() cnt = get_minimal_container() cnt["sysctls"] = [ "net.core.somaxconn=1024", "net.ipv4.tcp_syncookies=0", ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--sysctl", "net.core.somaxconn=1024", "--sysctl", "net.ipv4.tcp_syncookies=0", "busybox", ], ) async def test_sysctl_map(self): c = create_compose_mock() cnt = get_minimal_container() cnt["sysctls"] = { "net.core.somaxconn": 1024, "net.ipv4.tcp_syncookies": 0, } args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--sysctl", "net.core.somaxconn=1024", "--sysctl", "net.ipv4.tcp_syncookies=0", "busybox", ], ) async def test_sysctl_wrong_type(self): c = create_compose_mock() cnt = get_minimal_container() # check whether wrong types are correctly rejected for wrong_type in [True, 0, 0.0, "wrong", ()]: with self.assertRaises(TypeError): cnt["sysctls"] = wrong_type await container_to_args(c, cnt) async def test_pid(self): c = create_compose_mock() cnt = get_minimal_container() cnt["pid"] = "host" args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--pid", "host", "busybox", ], ) async def test_http_proxy(self): c = create_compose_mock() cnt = get_minimal_container() cnt["http_proxy"] = False args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--http-proxy=false", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_uidmaps_extension_old_path(self): c = create_compose_mock() cnt = get_minimal_container() cnt['x-podman'] = {'uidmaps': ['1000:1000:1']} with self.assertRaises(ValueError): await container_to_args(c, cnt) async def test_uidmaps_extension(self): c = create_compose_mock() cnt = get_minimal_container() cnt['x-podman.uidmaps'] = ['1000:1000:1', '1001:1001:2'] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", '--uidmap', '1000:1000:1', '--uidmap', '1001:1001:2', "busybox", ], ) async def test_gidmaps_extension(self): c = create_compose_mock() cnt = get_minimal_container() cnt['x-podman.gidmaps'] = ['1000:1000:1', '1001:1001:2'] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", '--gidmap', '1000:1000:1', '--gidmap', '1001:1001:2', "busybox", ], ) async def test_rootfs_extension(self): c = create_compose_mock() cnt = get_minimal_container() del cnt["image"] cnt["x-podman.rootfs"] = "/path/to/rootfs" args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--rootfs", "/path/to/rootfs", ], ) async def test_env_file_str(self): c = create_compose_mock() cnt = get_minimal_container() env_file = path.realpath('tests/env-file-tests/env-files/project-1.env') cnt['env_file'] = env_file args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "-e", "ZZVAR1=podman-rocks-123", "-e", "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_env_file_str_not_exists(self): c = create_compose_mock() cnt = get_minimal_container() cnt['env_file'] = 'notexists' with self.assertRaises(ValueError): await container_to_args(c, cnt) async def test_env_file_str_array_one_path(self): c = create_compose_mock() cnt = get_minimal_container() env_file = path.realpath('tests/env-file-tests/env-files/project-1.env') cnt['env_file'] = [env_file] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "-e", "ZZVAR1=podman-rocks-123", "-e", "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_env_file_str_array_two_paths(self): c = create_compose_mock() cnt = get_minimal_container() env_file = path.realpath('tests/env-file-tests/env-files/project-1.env') env_file_2 = path.realpath('tests/env-file-tests/env-files/project-2.env') cnt['env_file'] = [env_file, env_file_2] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "-e", "ZZVAR1=podman-rocks-123", "-e", "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", "-e", "ZZVAR1=podman-rocks-223", "-e", "ZZVAR2=podman-rocks-224", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_env_file_obj_required(self): c = create_compose_mock() cnt = get_minimal_container() env_file = path.realpath('tests/env-file-tests/env-files/project-1.env') cnt['env_file'] = {'path': env_file, 'required': True} args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "-e", "ZZVAR1=podman-rocks-123", "-e", "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_env_file_obj_required_non_existent_path(self): c = create_compose_mock() cnt = get_minimal_container() cnt['env_file'] = {'path': 'not-exists', 'required': True} with self.assertRaises(ValueError): await container_to_args(c, cnt) async def test_env_file_obj_optional(self): c = create_compose_mock() cnt = get_minimal_container() cnt['env_file'] = {'path': 'not-exists', 'required': False} args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "busybox", ], ) async def test_gpu_count_all(self): c = create_compose_mock() cnt = get_minimal_container() cnt["command"] = ["nvidia-smi"] cnt["deploy"] = {"resources": {"reservations": {"devices": [{}]}}} cnt["deploy"]["resources"]["reservations"]["devices"][0] = { "driver": "nvidia", "count": "all", "capabilities": ["gpu"], } args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--device", "nvidia.com/gpu=all", "--security-opt=label=disable", "busybox", "nvidia-smi", ], ) async def test_gpu_count_specific(self): c = create_compose_mock() cnt = get_minimal_container() cnt["command"] = ["nvidia-smi"] cnt["deploy"] = { "resources": { "reservations": { "devices": [ { "driver": "nvidia", "count": 2, "capabilities": ["gpu"], } ] } } } args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--device", "nvidia.com/gpu=0", "--device", "nvidia.com/gpu=1", "--security-opt=label=disable", "busybox", "nvidia-smi", ], ) async def test_gpu_device_ids_all(self): c = create_compose_mock() cnt = get_minimal_container() cnt["command"] = ["nvidia-smi"] cnt["deploy"] = { "resources": { "reservations": { "devices": [ { "driver": "nvidia", "device_ids": "all", "capabilities": ["gpu"], } ] } } } args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--device", "nvidia.com/gpu=all", "--security-opt=label=disable", "busybox", "nvidia-smi", ], ) async def test_gpu_device_ids_specific(self): c = create_compose_mock() cnt = get_minimal_container() cnt["command"] = ["nvidia-smi"] cnt["deploy"] = { "resources": { "reservations": { "devices": [ { "driver": "nvidia", "device_ids": [1, 3], "capabilities": ["gpu"], } ] } } } args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--device", "nvidia.com/gpu=1", "--device", "nvidia.com/gpu=3", "--security-opt=label=disable", "busybox", "nvidia-smi", ], ) @parameterized.expand([ (False, "z", ["--mount", "type=bind,source=./foo,destination=/mnt,z"]), (False, "Z", ["--mount", "type=bind,source=./foo,destination=/mnt,Z"]), (True, "z", ["-v", "./foo:/mnt:z"]), (True, "Z", ["-v", "./foo:/mnt:Z"]), ]) async def test_selinux_volume(self, prefer_volume, selinux_type, expected_additional_args): c = create_compose_mock() c.prefer_volume_over_mount = prefer_volume cnt = get_minimal_container() # This is supposed to happen during `_parse_compose_file` # but that is probably getting skipped during testing cnt["_service"] = cnt["service_name"] cnt["volumes"] = [ { "type": "bind", "source": "./foo", "target": "/mnt", "bind": { "selinux": selinux_type, }, } ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", *expected_additional_args, "--network=bridge", "--network-alias=service_name", "busybox", ], ) podman-compose-1.2.0/pytests/test_container_to_args_secrets.py000066400000000000000000000051431463674324000250230ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from podman_compose import container_to_args from .test_container_to_args import create_compose_mock from .test_container_to_args import get_minimal_container class TestContainerToArgsSecrets(unittest.IsolatedAsyncioTestCase): async def test_pass_secret_as_env_variable(self): c = create_compose_mock() c.declared_secrets = { "my_secret": {"external": "true"} # must have external or name value } cnt = get_minimal_container() cnt["secrets"] = [ { "source": "my_secret", "target": "ENV_SECRET", "type": "env", }, ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--secret", "my_secret,type=env,target=ENV_SECRET", "busybox", ], ) async def test_secret_as_env_external_true_has_no_name(self): c = create_compose_mock() c.declared_secrets = { "my_secret": { "name": "my_secret", # must have external or name value } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ { "source": "my_secret", "target": "ENV_SECRET", "type": "env", } ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge", "--network-alias=service_name", "--secret", "my_secret,type=env,target=ENV_SECRET", "busybox", ], ) async def test_pass_secret_as_env_variable_no_external(self): c = create_compose_mock() c.declared_secrets = { "my_secret": {} # must have external or name value } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ { "source": "my_secret", "target": "ENV_SECRET", "type": "env", } ] with self.assertRaises(ValueError) as context: await container_to_args(c, cnt) self.assertIn('ERROR: unparsable secret: ', str(context.exception)) podman-compose-1.2.0/pytests/test_get_net_args.py000066400000000000000000000241761463674324000222430ustar00rootroot00000000000000import unittest from parameterized import parameterized from podman_compose import get_net_args from .test_container_to_args import create_compose_mock PROJECT_NAME = "test_project_name" SERVICE_NAME = "service_name" CONTAINER_NAME = f"{PROJECT_NAME}_{SERVICE_NAME}_1" def get_networked_compose(num_networks=1): compose = create_compose_mock(PROJECT_NAME) for network in range(num_networks): compose.networks[f"net{network}"] = { "driver": "bridge", "ipam": { "config": [ {"subnet": f"192.168.{network}.0/24"}, {"subnet": f"fd00:{network}::/64"}, ] }, "enable_ipv6": True, } return compose def get_minimal_container(): return { "name": CONTAINER_NAME, "service_name": SERVICE_NAME, "image": "busybox", } class TestGetNetArgs(unittest.TestCase): def test_minimal(self): compose = get_networked_compose() container = get_minimal_container() expected_args = [ "--network=bridge", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_one_net(self): compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {}} expected_args = [ f"--network={PROJECT_NAME}_net0", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_alias(self): compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {}} container["_aliases"] = ["alias1", "alias2"] expected_args = [ f"--network={PROJECT_NAME}_net0", f"--network-alias={SERVICE_NAME}", "--network-alias=alias1", "--network-alias=alias2", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_one_ipv4(self): ip = "192.168.0.42" compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {"ipv4_address": ip}} expected_args = [ f"--network={PROJECT_NAME}_net0", f"--ip={ip}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertEqual(expected_args, args) def test_one_ipv6(self): ipv6_address = "fd00:0::42" compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {"ipv6_address": ipv6_address}} expected_args = [ f"--network={PROJECT_NAME}_net0", f"--ip6={ipv6_address}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_one_mac(self): mac = "00:11:22:33:44:55" compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {}} container["mac_address"] = mac expected_args = [ f"--network={PROJECT_NAME}_net0", f"--mac-address={mac}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_one_mac_two_nets(self): mac = "00:11:22:33:44:55" compose = get_networked_compose(num_networks=6) container = get_minimal_container() container["networks"] = {"net0": {}, "net1": {}} container["mac_address"] = mac expected_args = [ f"--network={PROJECT_NAME}_net0:mac={mac}", f"--network={PROJECT_NAME}_net1", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_two_nets_as_dict(self): compose = get_networked_compose(num_networks=2) container = get_minimal_container() container["networks"] = {"net0": {}, "net1": {}} expected_args = [ f"--network={PROJECT_NAME}_net0", f"--network={PROJECT_NAME}_net1", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_two_nets_as_list(self): compose = get_networked_compose(num_networks=2) container = get_minimal_container() container["networks"] = ["net0", "net1"] expected_args = [ f"--network={PROJECT_NAME}_net0", f"--network={PROJECT_NAME}_net1", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_two_ipv4(self): ip0 = "192.168.0.42" ip1 = "192.168.1.42" compose = get_networked_compose(num_networks=2) container = get_minimal_container() container["networks"] = {"net0": {"ipv4_address": ip0}, "net1": {"ipv4_address": ip1}} expected_args = [ f"--network={PROJECT_NAME}_net0:ip={ip0}", f"--network={PROJECT_NAME}_net1:ip={ip1}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_two_ipv6(self): ip0 = "fd00:0::42" ip1 = "fd00:1::42" compose = get_networked_compose(num_networks=2) container = get_minimal_container() container["networks"] = {"net0": {"ipv6_address": ip0}, "net1": {"ipv6_address": ip1}} expected_args = [ f"--network={PROJECT_NAME}_net0:ip={ip0}", f"--network={PROJECT_NAME}_net1:ip={ip1}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) # custom extension; not supported by docker-compose def test_two_mac(self): mac0 = "00:00:00:00:00:01" mac1 = "00:00:00:00:00:02" compose = get_networked_compose(num_networks=2) container = get_minimal_container() container["networks"] = { "net0": {"x-podman.mac_address": mac0}, "net1": {"x-podman.mac_address": mac1}, } expected_args = [ f"--network={PROJECT_NAME}_net0:mac={mac0}", f"--network={PROJECT_NAME}_net1:mac={mac1}", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_mixed_mac(self): ip4_0 = "192.168.0.42" ip4_1 = "192.168.1.42" ip4_2 = "192.168.2.42" mac_0 = "00:00:00:00:00:01" mac_1 = "00:00:00:00:00:02" compose = get_networked_compose(num_networks=3) container = get_minimal_container() container["networks"] = { "net0": {"ipv4_address": ip4_0}, "net1": {"ipv4_address": ip4_1, "x-podman.mac_address": mac_0}, "net2": {"ipv4_address": ip4_2}, } container["mac_address"] = mac_1 expected_exception = ( r"specifying mac_address on both container and network level " r"is not supported" ) self.assertRaisesRegex(RuntimeError, expected_exception, get_net_args, compose, container) def test_mixed_config(self): ip4_0 = "192.168.0.42" ip4_1 = "192.168.1.42" ip6_0 = "fd00:0::42" ip6_2 = "fd00:2::42" mac = "00:11:22:33:44:55" compose = get_networked_compose(num_networks=4) container = get_minimal_container() container["networks"] = { "net0": {"ipv4_address": ip4_0, "ipv6_address": ip6_0}, "net1": {"ipv4_address": ip4_1}, "net2": {"ipv6_address": ip6_2}, "net3": {}, } container["mac_address"] = mac expected_args = [ f"--network={PROJECT_NAME}_net0:ip={ip4_0},ip={ip6_0},mac={mac}", f"--network={PROJECT_NAME}_net1:ip={ip4_1}", f"--network={PROJECT_NAME}_net2:ip={ip6_2}", f"--network={PROJECT_NAME}_net3", f"--network-alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @parameterized.expand([ ("bridge", ["--network=bridge", f"--network-alias={SERVICE_NAME}"]), ("host", ["--network=host"]), ("none", []), ("slirp4netns", ["--network=slirp4netns"]), ("slirp4netns:cidr=10.42.0.0/24", ["--network=slirp4netns:cidr=10.42.0.0/24"]), ("private", ["--network=private"]), ("pasta", ["--network=pasta"]), ("pasta:--ipv4-only,-a,10.0.2.0", ["--network=pasta:--ipv4-only,-a,10.0.2.0"]), ("ns:my_namespace", ["--network=ns:my_namespace"]), ("container:my_container", ["--network=container:my_container"]), ]) def test_network_modes(self, network_mode, expected_args): compose = get_networked_compose() container = get_minimal_container() container["network_mode"] = network_mode args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_network_mode_invalid(self): compose = get_networked_compose() container = get_minimal_container() container["network_mode"] = "invalid_mode" with self.assertRaises(SystemExit): get_net_args(compose, container) def test_network__mode_service(self): compose = get_networked_compose() compose.container_names_by_service = { "service_1": ["container_1"], "service_2": ["container_2"], } container = get_minimal_container() container["network_mode"] = "service:service_2" expected_args = ["--network=container:container_2"] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) podman-compose-1.2.0/pytests/test_get_network_create_args.py000066400000000000000000000136651463674324000244720ustar00rootroot00000000000000import unittest from podman_compose import get_network_create_args class TestGetNetworkCreateArgs(unittest.TestCase): def test_minimal(self): net_desc = { "labels": [], "internal": False, "driver": None, "driver_opts": {}, "ipam": {"config": []}, "enable_ipv6": False, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) def test_ipv6(self): net_desc = { "labels": [], "internal": False, "driver": None, "driver_opts": {}, "ipam": {"config": []}, "enable_ipv6": True, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", "--ipv6", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) def test_bridge(self): net_desc = { "labels": [], "internal": False, "driver": "bridge", "driver_opts": {"opt1": "value1", "opt2": "value2"}, "ipam": {"config": []}, "enable_ipv6": False, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", "--driver", "bridge", "--opt", "opt1=value1", "--opt", "opt2=value2", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) def test_ipam_driver_default(self): net_desc = { "labels": [], "internal": False, "driver": None, "driver_opts": {}, "ipam": { "driver": "default", "config": [ { "subnet": "192.168.0.0/24", "ip_range": "192.168.0.2/24", "gateway": "192.168.0.1", } ], }, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", "--subnet", "192.168.0.0/24", "--ip-range", "192.168.0.2/24", "--gateway", "192.168.0.1", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) def test_ipam_driver(self): net_desc = { "labels": [], "internal": False, "driver": None, "driver_opts": {}, "ipam": { "driver": "someipamdriver", "config": [ { "subnet": "192.168.0.0/24", "ip_range": "192.168.0.2/24", "gateway": "192.168.0.1", } ], }, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", "--ipam-driver", "someipamdriver", "--subnet", "192.168.0.0/24", "--ip-range", "192.168.0.2/24", "--gateway", "192.168.0.1", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) def test_complete(self): net_desc = { "labels": ["label1", "label2"], "internal": True, "driver": "bridge", "driver_opts": {"opt1": "value1", "opt2": "value2"}, "ipam": { "driver": "someipamdriver", "config": [ { "subnet": "192.168.0.0/24", "ip_range": "192.168.0.2/24", "gateway": "192.168.0.1", } ], }, "enable_ipv6": True, } proj_name = "test_project" net_name = "test_network" expected_args = [ "create", "--label", f"io.podman.compose.project={proj_name}", "--label", f"com.docker.compose.project={proj_name}", "--label", "label1", "--label", "label2", "--internal", "--driver", "bridge", "--opt", "opt1=value1", "--opt", "opt2=value2", "--ipam-driver", "someipamdriver", "--ipv6", "--subnet", "192.168.0.0/24", "--ip-range", "192.168.0.2/24", "--gateway", "192.168.0.1", net_name, ] args = get_network_create_args(net_desc, proj_name, net_name) self.assertEqual(args, expected_args) podman-compose-1.2.0/pytests/test_normalize_depends_on.py000066400000000000000000000025111463674324000237650ustar00rootroot00000000000000import copy from podman_compose import normalize_service test_cases_simple = [ ( {"depends_on": "my_service"}, {"depends_on": {"my_service": {"condition": "service_started"}}}, ), ( {"depends_on": ["my_service"]}, {"depends_on": {"my_service": {"condition": "service_started"}}}, ), ( {"depends_on": ["my_service1", "my_service2"]}, { "depends_on": { "my_service1": {"condition": "service_started"}, "my_service2": {"condition": "service_started"}, }, }, ), ( {"depends_on": {"my_service": {"condition": "service_started"}}}, {"depends_on": {"my_service": {"condition": "service_started"}}}, ), ( {"depends_on": {"my_service": {"condition": "service_healthy"}}}, {"depends_on": {"my_service": {"condition": "service_healthy"}}}, ), ] def test_normalize_service_simple(): for test_case, expected in copy.deepcopy(test_cases_simple): test_original = copy.deepcopy(test_case) test_case = normalize_service(test_case) test_result = expected == test_case if not test_result: print("test: ", test_original) print("expected: ", expected) print("actual: ", test_case) assert test_result podman-compose-1.2.0/pytests/test_normalize_final_build.py000066400000000000000000000215111463674324000241200ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 # pylint: disable=protected-access from __future__ import annotations import argparse import os import unittest import yaml from parameterized import parameterized from podman_compose import PodmanCompose from podman_compose import normalize_final from podman_compose import normalize_service_final cwd = os.path.abspath(".") class TestNormalizeFinalBuild(unittest.TestCase): cases_simple_normalization = [ ({"image": "test-image"}, {"image": "test-image"}), ( {"build": "."}, { "build": {"context": cwd, "dockerfile": "Dockerfile"}, }, ), ( {"build": "../relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "../relative")), "dockerfile": "Dockerfile", }, }, ), ( {"build": "./relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "./relative")), "dockerfile": "Dockerfile", }, }, ), ( {"build": "/workspace/absolute"}, { "build": { "context": "/workspace/absolute", "dockerfile": "Dockerfile", }, }, ), ( { "build": { "dockerfile": "Dockerfile", }, }, { "build": { "context": cwd, "dockerfile": "Dockerfile", }, }, ), ( { "build": { "context": ".", }, }, { "build": { "context": cwd, "dockerfile": "Dockerfile", }, }, ), ( { "build": {"context": "../", "dockerfile": "test-dockerfile"}, }, { "build": { "context": os.path.normpath(os.path.join(cwd, "../")), "dockerfile": "test-dockerfile", }, }, ), ( { "build": {"context": ".", "dockerfile": "./dev/test-dockerfile"}, }, { "build": { "context": cwd, "dockerfile": "./dev/test-dockerfile", }, }, ), ] @parameterized.expand(cases_simple_normalization) def test_normalize_service_final_returns_absolute_path_in_context(self, input, expected): # Tests that [service.build] is normalized after merges project_dir = cwd self.assertEqual(normalize_service_final(input, project_dir), expected) @parameterized.expand(cases_simple_normalization) def test_normalize_returns_absolute_path_in_context(self, input, expected): project_dir = cwd compose_test = {"services": {"test-service": input}} compose_expected = {"services": {"test-service": expected}} self.assertEqual(normalize_final(compose_test, project_dir), compose_expected) @parameterized.expand(cases_simple_normalization) def test_parse_compose_file_when_single_compose(self, input, expected): compose_test = {"services": {"test-service": input}} dump_yaml(compose_test, "test-compose.yaml") podman_compose = PodmanCompose() set_args(podman_compose, ["test-compose.yaml"], no_normalize=None) podman_compose._parse_compose_file() actual_compose = {} if podman_compose.services: podman_compose.services["test-service"].pop("_deps") actual_compose = podman_compose.services["test-service"] self.assertEqual(actual_compose, expected) @parameterized.expand([ ( {}, {"build": "."}, {"build": {"context": cwd, "dockerfile": "Dockerfile"}}, ), ( {"build": "."}, {}, {"build": {"context": cwd, "dockerfile": "Dockerfile"}}, ), ( {"build": "/workspace/absolute"}, {"build": "./relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "./relative")), "dockerfile": "Dockerfile", } }, ), ( {"build": "./relative"}, {"build": "/workspace/absolute"}, {"build": {"context": "/workspace/absolute", "dockerfile": "Dockerfile"}}, ), ( {"build": "./relative"}, {"build": "/workspace/absolute"}, {"build": {"context": "/workspace/absolute", "dockerfile": "Dockerfile"}}, ), ( {"build": {"dockerfile": "test-dockerfile"}}, {}, {"build": {"context": cwd, "dockerfile": "test-dockerfile"}}, ), ( {}, {"build": {"dockerfile": "test-dockerfile"}}, {"build": {"context": cwd, "dockerfile": "test-dockerfile"}}, ), ( {}, {"build": {"dockerfile": "test-dockerfile"}}, {"build": {"context": cwd, "dockerfile": "test-dockerfile"}}, ), ( {"build": {"dockerfile": "test-dockerfile-1"}}, {"build": {"dockerfile": "test-dockerfile-2"}}, {"build": {"context": cwd, "dockerfile": "test-dockerfile-2"}}, ), ( {"build": "/workspace/absolute"}, {"build": {"dockerfile": "test-dockerfile"}}, {"build": {"context": "/workspace/absolute", "dockerfile": "test-dockerfile"}}, ), ( {"build": {"dockerfile": "test-dockerfile"}}, {"build": "/workspace/absolute"}, {"build": {"context": "/workspace/absolute", "dockerfile": "test-dockerfile"}}, ), ( {"build": {"dockerfile": "./test-dockerfile-1"}}, {"build": {"dockerfile": "./test-dockerfile-2", "args": ["ENV1=1"]}}, { "build": { "context": cwd, "dockerfile": "./test-dockerfile-2", "args": ["ENV1=1"], } }, ), ( {"build": {"dockerfile": "./test-dockerfile-1", "args": ["ENV1=1"]}}, {"build": {"dockerfile": "./test-dockerfile-2"}}, { "build": { "context": cwd, "dockerfile": "./test-dockerfile-2", "args": ["ENV1=1"], } }, ), ( {"build": {"dockerfile": "./test-dockerfile-1", "args": ["ENV1=1"]}}, {"build": {"dockerfile": "./test-dockerfile-2", "args": ["ENV2=2"]}}, { "build": { "context": cwd, "dockerfile": "./test-dockerfile-2", "args": ["ENV1=1", "ENV2=2"], } }, ), ]) def test_parse_when_multiple_composes(self, input, override, expected): compose_test_1 = {"services": {"test-service": input}} compose_test_2 = {"services": {"test-service": override}} dump_yaml(compose_test_1, "test-compose-1.yaml") dump_yaml(compose_test_2, "test-compose-2.yaml") podman_compose = PodmanCompose() set_args( podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"], no_normalize=None, ) podman_compose._parse_compose_file() actual_compose = {} if podman_compose.services: podman_compose.services["test-service"].pop("_deps") actual_compose = podman_compose.services["test-service"] self.assertEqual(actual_compose, expected) def set_args(podman_compose: PodmanCompose, file_names: list[str], no_normalize: bool) -> None: podman_compose.global_args = argparse.Namespace() podman_compose.global_args.file = file_names podman_compose.global_args.project_name = None podman_compose.global_args.env_file = None podman_compose.global_args.profile = [] podman_compose.global_args.in_pod_bool = True podman_compose.global_args.no_normalize = no_normalize def dump_yaml(compose: dict, name: str) -> None: # Path(Path.cwd()/"subdirectory").mkdir(parents=True, exist_ok=True) with open(name, "w", encoding="utf-8") as outfile: yaml.safe_dump(compose, outfile, default_flow_style=False) def test_clean_test_yamls() -> None: test_files = ["test-compose-1.yaml", "test-compose-2.yaml", "test-compose.yaml"] for file in test_files: if os.path.exists(file): os.remove(file) podman-compose-1.2.0/pytests/test_normalize_service.py000066400000000000000000000052021463674324000233070ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from parameterized import parameterized from podman_compose import normalize_service class TestNormalizeService(unittest.TestCase): @parameterized.expand([ ({"test": "test"}, {"test": "test"}), ({"build": "."}, {"build": {"context": "."}}), ({"build": "./dir-1"}, {"build": {"context": "./dir-1"}}), ({"build": {"context": "./dir-1"}}, {"build": {"context": "./dir-1"}}), ( {"build": {"dockerfile": "dockerfile-1"}}, {"build": {"dockerfile": "dockerfile-1"}}, ), ( {"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}}, {"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}}, ), ( {"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}}, {"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}}, ), ( {"build": {"additional_contexts": {"ctx": "../ctx", "ctx2": "../ctx2"}}}, {"build": {"additional_contexts": ["ctx=../ctx", "ctx2=../ctx2"]}}, ), ]) def test_simple(self, input, expected): self.assertEqual(normalize_service(input), expected) @parameterized.expand([ ({"test": "test"}, {"test": "test"}), ({"build": "."}, {"build": {"context": "./sub_dir/."}}), ({"build": "./dir-1"}, {"build": {"context": "./sub_dir/dir-1"}}), ({"build": {"context": "./dir-1"}}, {"build": {"context": "./sub_dir/dir-1"}}), ( {"build": {"dockerfile": "dockerfile-1"}}, {"build": {"context": "./sub_dir", "dockerfile": "dockerfile-1"}}, ), ( {"build": {"context": "./dir-1", "dockerfile": "dockerfile-1"}}, {"build": {"context": "./sub_dir/dir-1", "dockerfile": "dockerfile-1"}}, ), ]) def test_normalize_service_with_sub_dir(self, input, expected): self.assertEqual(normalize_service(input, sub_dir="./sub_dir"), expected) @parameterized.expand([ ([], []), (["sh"], ["sh"]), (["sh", "-c", "date"], ["sh", "-c", "date"]), ("sh", ["sh"]), ("sleep infinity", ["sleep", "infinity"]), ( "bash -c 'sleep infinity'", ["bash", "-c", "sleep infinity"], ), ]) def test_command_like(self, input, expected): for key in ['command', 'entrypoint']: input_service = {} input_service[key] = input expected_service = {} expected_service[key] = expected self.assertEqual(normalize_service(input_service), expected_service) podman-compose-1.2.0/pytests/test_volumes.py000066400000000000000000000010301463674324000212540ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 # pylint: disable=redefined-outer-name import unittest from podman_compose import parse_short_mount class ParseShortMountTests(unittest.TestCase): def test_multi_propagation(self): self.assertEqual( parse_short_mount("/foo/bar:/baz:U,Z", "/"), { "type": "bind", "source": "/foo/bar", "target": "/baz", "bind": { "propagation": "U,Z", }, }, ) podman-compose-1.2.0/requirements.txt000066400000000000000000000003511463674324000177270ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pyyaml python-dotenv podman-compose-1.2.0/scripts/000077500000000000000000000000001463674324000161335ustar00rootroot00000000000000podman-compose-1.2.0/scripts/clean_up.sh000077500000000000000000000003641463674324000202630ustar00rootroot00000000000000#!/usr/bin/env bash find . -name "*.pyc" -delete find . -name "__pycache__" -delete find . -name "*.orig" -delete rm -rf .cache/ rm -rf build/ rm -rf builddocs/ rm -rf dist/ rm -rf deb_dist/ rm src/podman_compose.egg-info -rf rm builddocs.zip podman-compose-1.2.0/scripts/make_release.sh000077500000000000000000000002221463674324000211030ustar00rootroot00000000000000#!/usr/bin/env bash ./scripts/uninstall.sh ./scripts/clean_up.sh python3 setup.py register python3 setup.py sdist bdist_wheel twine upload dist/* podman-compose-1.2.0/scripts/uninstall.sh000077500000000000000000000001131463674324000204760ustar00rootroot00000000000000#!/usr/bin/env bash pip3 uninstall podman-compose -y ./scripts/clean_up.sh podman-compose-1.2.0/setup.cfg000066400000000000000000000002261463674324000162650ustar00rootroot00000000000000[bdist_wheel] universal = 1 [metadata] version = attr: podman_compose.__version__ [flake8] # The GitHub editor is 127 chars wide max-line-length=127podman-compose-1.2.0/setup.py000066400000000000000000000031521463674324000161570ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os from setuptools import setup try: README = open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8").read() except: # noqa: E722 # pylint: disable=bare-except README = "" setup( name="podman-compose", description="A script to run docker-compose.yml using podman", long_description=README, long_description_content_type="text/markdown", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Intended Audience :: Developers", "Operating System :: OS Independent", "Development Status :: 3 - Alpha", "Topic :: Software Development :: Build Tools", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", ], keywords="podman, podman-compose", author="Muayyad Alsadi", author_email="alsadi@gmail.com", url="https://github.com/containers/podman-compose", py_modules=["podman_compose"], entry_points={"console_scripts": ["podman-compose = podman_compose:main"]}, include_package_data=True, license="GPL-2.0-only", install_requires=[ "pyyaml", "python-dotenv", ], extras_require={"devel": ["ruff", "pre-commit", "coverage", "parameterized"]}, # test_suite='tests', # tests_require=[ # 'coverage', # 'tox', # ] ) podman-compose-1.2.0/test-requirements.txt000066400000000000000000000013111463674324000207010ustar00rootroot00000000000000-e . coverage==7.4.3 parameterized==0.9.0 pytest==8.0.2 tox==4.13.0 ruff==0.3.1 pylint==3.1.0 # The packages below are transitive dependencies of the packages above and are included here # to make testing reproducible. # To refresh, create a new virtualenv and do: # pip install -r requirements.txt -r test-requirements.txt # pip freeze > test-requirements.txt # and edit test-requirements.txt to add this comment astroid==3.1.0 cachetools==5.3.3 chardet==5.2.0 colorama==0.4.6 dill==0.3.8 distlib==0.3.8 filelock==3.13.1 iniconfig==2.0.0 isort==5.13.2 mccabe==0.7.0 packaging==23.2 platformdirs==4.2.0 pluggy==1.4.0 pyproject-api==1.6.1 python-dotenv==1.0.1 PyYAML==6.0.1 tomlkit==0.12.4 virtualenv==20.25.1 podman-compose-1.2.0/tests/000077500000000000000000000000001463674324000156065ustar00rootroot00000000000000podman-compose-1.2.0/tests/__init__.py000066400000000000000000000004011463674324000177120ustar00rootroot00000000000000import os import subprocess def create_base_test_image(): subprocess.check_call( ['podman', 'build', '-t', 'nopush/podman-compose-test', '.'], cwd=os.path.join(os.path.dirname(__file__), "base_image"), ) create_base_test_image() podman-compose-1.2.0/tests/additional_contexts/000077500000000000000000000000001463674324000216455ustar00rootroot00000000000000podman-compose-1.2.0/tests/additional_contexts/README.md000066400000000000000000000003121463674324000231200ustar00rootroot00000000000000# Test podman-compose with build.additional_contexts ``` podman-compose build podman-compose up podman-compose down ``` expected output would be ``` [dict] | Data for dict [list] | Data for list ``` podman-compose-1.2.0/tests/additional_contexts/data_for_dict/000077500000000000000000000000001463674324000244275ustar00rootroot00000000000000podman-compose-1.2.0/tests/additional_contexts/data_for_dict/data.txt000066400000000000000000000000161463674324000260760ustar00rootroot00000000000000Data for dict podman-compose-1.2.0/tests/additional_contexts/data_for_list/000077500000000000000000000000001463674324000244575ustar00rootroot00000000000000podman-compose-1.2.0/tests/additional_contexts/data_for_list/data.txt000066400000000000000000000000161463674324000261260ustar00rootroot00000000000000Data for list podman-compose-1.2.0/tests/additional_contexts/project/000077500000000000000000000000001463674324000233135ustar00rootroot00000000000000podman-compose-1.2.0/tests/additional_contexts/project/Dockerfile000066400000000000000000000001371463674324000253060ustar00rootroot00000000000000FROM busybox COPY --from=data data.txt /data/data.txt CMD ["busybox", "cat", "/data/data.txt"] podman-compose-1.2.0/tests/additional_contexts/project/docker-compose.yml000066400000000000000000000003261463674324000267510ustar00rootroot00000000000000version: "3.7" services: dict: build: context: . additional_contexts: data: ../data_for_dict list: build: context: . additional_contexts: - data=../data_for_list podman-compose-1.2.0/tests/base_image/000077500000000000000000000000001463674324000176625ustar00rootroot00000000000000podman-compose-1.2.0/tests/base_image/Dockerfile000066400000000000000000000002201463674324000216460ustar00rootroot00000000000000FROM docker.io/library/debian:bookworm-slim RUN apt-get update \ && apt-get install -y \ dumb-init \ busybox \ wget podman-compose-1.2.0/tests/build/000077500000000000000000000000001463674324000167055ustar00rootroot00000000000000podman-compose-1.2.0/tests/build/README.md000066400000000000000000000007541463674324000201720ustar00rootroot00000000000000# Test podman-compose with build ``` podman-compose build podman-compose up -d curl http://localhost:8080/index.txt curl http://localhost:8000/index.txt podman inspect my-busybox-httpd2 podman-compose down ``` expected output would be something like ``` 2019-09-03T15:16:38+0000 ALT buildno=2 port 8000 2019-09-03T15:16:38+0000 { ... } ``` as you can see we were able to override buildno to be 2 instead of 1, and httpd_port to 8000. NOTE: build labels are not passed to `podman build` podman-compose-1.2.0/tests/build/context/000077500000000000000000000000001463674324000203715ustar00rootroot00000000000000podman-compose-1.2.0/tests/build/context/Dockerfile000066400000000000000000000002271463674324000223640ustar00rootroot00000000000000FROM busybox RUN mkdir -p /var/www/html/ && date -Iseconds > /var/www/html/index.txt CMD ["busybox", "httpd", "-f", "-p", "80", "-h", "/var/www/html"] podman-compose-1.2.0/tests/build/context/Dockerfile-alt000066400000000000000000000004661463674324000231470ustar00rootroot00000000000000FROM busybox ARG buildno=1 ARG httpd_port=80 ARG other_variable=not_set ENV httpd_port ${httpd_port} ENV other_variable ${other_variable} RUN mkdir -p /var/www/html/ && \ echo "ALT buildno=$buildno port=$httpd_port `date -Iseconds`" > /var/www/html/index.txt CMD httpd -f -p "$httpd_port" -h /var/www/html podman-compose-1.2.0/tests/build/docker-compose.yml000066400000000000000000000010131463674324000223350ustar00rootroot00000000000000version: "3" services: web1: build: ./context image: my-busybox-httpd ports: - 8080:80 web2: build: context: ./context dockerfile: Dockerfile-alt labels: mykey: myval args: buildno: 2 httpd_port: 8000 image: my-busybox-httpd2 ports: - 8000:8000 test_build_arg_argument: build: context: ./context dockerfile: Dockerfile-alt image: my-busybox-httpd2 command: env podman-compose-1.2.0/tests/build_fail/000077500000000000000000000000001463674324000177005ustar00rootroot00000000000000podman-compose-1.2.0/tests/build_fail/README.md000066400000000000000000000006631463674324000211640ustar00rootroot00000000000000# Test podman-compose with build (fail scenario) ```shell podman-compose build || echo $? ``` expected output would be something like ``` STEP 1/3: FROM busybox STEP 2/3: RUN this_command_does_not_exist /bin/sh: this_command_does_not_exist: not found Error: building at STEP "RUN this_command_does_not_exist": while running runtime: exit status 127 exit code: 127 ``` Expected `podman-compose` exit code: ```shell echo $? 127 ``` podman-compose-1.2.0/tests/build_fail/context/000077500000000000000000000000001463674324000213645ustar00rootroot00000000000000podman-compose-1.2.0/tests/build_fail/context/Dockerfile000066400000000000000000000000701463674324000233530ustar00rootroot00000000000000FROM busybox RUN this_command_does_not_exist CMD ["sh"] podman-compose-1.2.0/tests/build_fail/docker-compose.yml000066400000000000000000000001241463674324000233320ustar00rootroot00000000000000version: "3" services: test: build: ./context image: build-fail-img podman-compose-1.2.0/tests/build_secrets/000077500000000000000000000000001463674324000204355ustar00rootroot00000000000000podman-compose-1.2.0/tests/build_secrets/Dockerfile000066400000000000000000000004341463674324000224300ustar00rootroot00000000000000FROM busybox RUN --mount=type=secret,required=true,id=build_secret \ ls -l /run/secrets/ && cat /run/secrets/build_secret RUN --mount=type=secret,required=true,id=build_secret,target=/tmp/secret \ ls -l /run/secrets/ /tmp/ && cat /tmp/secret CMD [ 'echo', 'nothing here' ] podman-compose-1.2.0/tests/build_secrets/docker-compose.yaml000066400000000000000000000010661463674324000242360ustar00rootroot00000000000000version: "3.8" services: test: image: test secrets: - run_secret # implicitly mount to /run/secrets/run_secret - source: run_secret target: /tmp/run_secret2 # explicit mount point build: context: . secrets: - build_secret # can be mounted in Dockerfile with "RUN --mount=type=secret,id=build_secret" - source: build_secret target: build_secret2 # rename to build_secret2 secrets: build_secret: file: ./my_secret run_secret: file: ./my_secret podman-compose-1.2.0/tests/build_secrets/docker-compose.yaml.invalid000066400000000000000000000006701463674324000256630ustar00rootroot00000000000000version: "3.8" services: test: image: test build: context: . secrets: # invalid target argument # # According to https://github.com/compose-spec/compose-spec/blob/master/build.md, target is # supposed to be the "name of a *file* to be mounted in /run/secrets/". Not a path. - source: build_secret target: /build_secret secrets: build_secret: file: ./my_secret podman-compose-1.2.0/tests/build_secrets/my_secret000066400000000000000000000000361463674324000223510ustar00rootroot00000000000000important-secret-is-important podman-compose-1.2.0/tests/deps/000077500000000000000000000000001463674324000165415ustar00rootroot00000000000000podman-compose-1.2.0/tests/deps/README.md000066400000000000000000000001241463674324000200150ustar00rootroot00000000000000 ``` podman-compose run --rm sleep /bin/sh -c 'wget -O - http://web:8000/hosts' ``` podman-compose-1.2.0/tests/deps/docker-compose.yaml000066400000000000000000000011341463674324000223360ustar00rootroot00000000000000version: "3.7" services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp sleep: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"] depends_on: - "web" tmpfs: - /run - /tmp sleep2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"] depends_on: - sleep tmpfs: - /run - /tmp podman-compose-1.2.0/tests/env-file-tests/000077500000000000000000000000001463674324000204535ustar00rootroot00000000000000podman-compose-1.2.0/tests/env-file-tests/.env000066400000000000000000000001611463674324000212420ustar00rootroot00000000000000ZZVAR1='This value is overwritten by env-file-tests/.env' ZZVAR3='This value is loaded from env-file-tests/.env' podman-compose-1.2.0/tests/env-file-tests/.gitignore000066400000000000000000000002361463674324000224440ustar00rootroot00000000000000# This overrides the repository root .gitignore (ignoring all .env). # The .env files in this directory are important for the test cases. !.env !project/.env podman-compose-1.2.0/tests/env-file-tests/README.md000066400000000000000000000020571463674324000217360ustar00rootroot00000000000000running the following commands should always give podman-rocks-123 ``` podman-compose -f project/container-compose.yaml --env-file env-files/project-1.env up ``` ``` podman-compose -f $(pwd)/project/container-compose.yaml --env-file $(pwd)/env-files/project-1.env up ``` ``` podman-compose -f $(pwd)/project/container-compose.env-file-flat.yaml up ``` ``` podman-compose -f $(pwd)/project/container-compose.env-file-obj.yaml up ``` ``` podman-compose -f $(pwd)/project/container-compose.env-file-obj-optional.yaml up ``` based on environment variable precedent this command should give podman-rocks-321 ``` ZZVAR1=podman-rocks-321 podman-compose -f $(pwd)/project/container-compose.yaml --env-file $(pwd)/env-files/project-1.env up ``` _The below test should print three environment variables_ ``` podman-compose -f $(pwd)/project/container-compose.load-.env-in-project.yaml run --rm app ZZVAR1=This value is overwritten by env-file-tests/.env ZZVAR2=This value is loaded from .env in project/ directory ZZVAR3=This value is loaded from env-file-tests/.env ``` podman-compose-1.2.0/tests/env-file-tests/env-files/000077500000000000000000000000001463674324000223435ustar00rootroot00000000000000podman-compose-1.2.0/tests/env-file-tests/env-files/project-1.env000066400000000000000000000001101463674324000246510ustar00rootroot00000000000000ZZVAR1=podman-rocks-123 ZZVAR2=podman-rocks-124 ZZVAR3=podman-rocks-125 podman-compose-1.2.0/tests/env-file-tests/env-files/project-2.env000066400000000000000000000000601463674324000246560ustar00rootroot00000000000000ZZVAR1=podman-rocks-223 ZZVAR2=podman-rocks-224 podman-compose-1.2.0/tests/env-file-tests/project/000077500000000000000000000000001463674324000221215ustar00rootroot00000000000000podman-compose-1.2.0/tests/env-file-tests/project/.env000066400000000000000000000001661463674324000227150ustar00rootroot00000000000000ZZVAR1='This value is loaded but should be overwritten' ZZVAR2='This value is loaded from .env in project/ directory' podman-compose-1.2.0/tests/env-file-tests/project/container-compose.env-file-flat.yaml000066400000000000000000000002651463674324000310650ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp env_file: - ../env-files/project-1.env podman-compose-1.2.0/tests/env-file-tests/project/container-compose.env-file-obj-optional.yaml000066400000000000000000000003741463674324000325350ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp env_file: - path: ../env-files/project-1.env - path: ../env-files/project-2.env required: false podman-compose-1.2.0/tests/env-file-tests/project/container-compose.env-file-obj.yaml000066400000000000000000000002731463674324000307100ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp env_file: - path: ../env-files/project-1.env podman-compose-1.2.0/tests/env-file-tests/project/container-compose.load-.env-in-project.yaml000066400000000000000000000003271463674324000322660ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp environment: ZZVAR1: $ZZVAR1 ZZVAR2: $ZZVAR2 ZZVAR3: $ZZVAR3 podman-compose-1.2.0/tests/env-file-tests/project/container-compose.yaml000066400000000000000000000002531463674324000264320ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp environment: ZZVAR1: $ZZVAR1 podman-compose-1.2.0/tests/env-tests/000077500000000000000000000000001463674324000175365ustar00rootroot00000000000000podman-compose-1.2.0/tests/env-tests/README.md000066400000000000000000000001611463674324000210130ustar00rootroot00000000000000running the following command should give myval2 ``` podman_compose run -l monkey -e ZZVAR1=myval2 env-test ``` podman-compose-1.2.0/tests/env-tests/container-compose.yml000066400000000000000000000002031463674324000237010ustar00rootroot00000000000000version: '3' services: env-test: image: busybox command: sh -c "export | grep ZZ" environment: - ZZVAR1=myval1 podman-compose-1.2.0/tests/exit-from/000077500000000000000000000000001463674324000175205ustar00rootroot00000000000000podman-compose-1.2.0/tests/exit-from/README.md000066400000000000000000000003651463674324000210030ustar00rootroot00000000000000We have service named sh1 that exits with code 1 and sh2 that exists with code 2 ``` podman-compose up --exit-code-from=sh1 echo $? ``` the above should give 1. ``` podman-compose up --exit-code-from=sh2 echo $? ``` the above should give 2. podman-compose-1.2.0/tests/exit-from/docker-compose.yaml000066400000000000000000000010301463674324000233100ustar00rootroot00000000000000version: "3" services: too_long: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600; exit 0"] tmpfs: - /run - /tmp sh1: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"] tmpfs: - /run - /tmp sh2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 2"] tmpfs: - /run - /tmp podman-compose-1.2.0/tests/extends/000077500000000000000000000000001463674324000172605ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends/docker-compose.yaml000066400000000000000000000011511463674324000230540ustar00rootroot00000000000000version: "3" services: echo: image: busybox command: ["/bin/busybox", "echo", "Zero"] ports: - '1234:1234' environment: - FOO=original - BAR=original # volumes: # - ./original:/foo # - ./original:/bar echo1: extends: service: echo command: ["/bin/busybox", "echo", "One"] ports: - '12345:12345' # volumes: # - ./local:/bar # - ./local:/baz env1: extends: service: echo command: ["/bin/busybox", "env"] environment: - BAR=local - BAZ=local podman-compose-1.2.0/tests/extends_w_empty_service/000077500000000000000000000000001463674324000225445ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_empty_service/common-services.yml000066400000000000000000000001621463674324000263770ustar00rootroot00000000000000services: webapp_default: webapp_special: image: nopush/podman-compose-test volumes: - "/data" podman-compose-1.2.0/tests/extends_w_empty_service/docker-compose.yml000066400000000000000000000003021463674324000261740ustar00rootroot00000000000000version: "3" services: web: image: nopush/podman-compose-test extends: file: common-services.yml service: webapp_default environment: - DEBUG=1 cpu_shares: 5 podman-compose-1.2.0/tests/extends_w_file/000077500000000000000000000000001463674324000206055ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_file/common-services.yml000066400000000000000000000001101463674324000244310ustar00rootroot00000000000000webapp: build: . ports: - "8000:8000" volumes: - "/data" podman-compose-1.2.0/tests/extends_w_file/docker-compose.yml000066400000000000000000000003131463674324000242370ustar00rootroot00000000000000version: "3" services: web: extends: file: common-services.yml service: webapp environment: - DEBUG=1 cpu_shares: 5 important_web: extends: web cpu_shares: 10 podman-compose-1.2.0/tests/extends_w_file_subdir/000077500000000000000000000000001463674324000221555ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_file_subdir/docker-compose.yml000066400000000000000000000002041463674324000256060ustar00rootroot00000000000000version: "3" services: web: extends: file: sub/docker-compose.yml service: webapp environment: - DEBUG=1podman-compose-1.2.0/tests/extends_w_file_subdir/sub/000077500000000000000000000000001463674324000227465ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_file_subdir/sub/docker-compose.yml000066400000000000000000000003101463674324000263750ustar00rootroot00000000000000version: "3" services: webapp: build: context: docker/example dockerfile: Dockerfile image: localhost/subdir_test:me ports: - "8000:8000" volumes: - "/data" podman-compose-1.2.0/tests/extends_w_file_subdir/sub/docker/000077500000000000000000000000001463674324000242155ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_file_subdir/sub/docker/example/000077500000000000000000000000001463674324000256505ustar00rootroot00000000000000podman-compose-1.2.0/tests/extends_w_file_subdir/sub/docker/example/Dockerfile000066400000000000000000000000501463674324000276350ustar00rootroot00000000000000FROM nopush/podman-compose-test as base podman-compose-1.2.0/tests/in_pod/000077500000000000000000000000001463674324000170565ustar00rootroot00000000000000podman-compose-1.2.0/tests/in_pod/custom_x-podman_false/000077500000000000000000000000001463674324000233455ustar00rootroot00000000000000podman-compose-1.2.0/tests/in_pod/custom_x-podman_false/docker-compose.yml000066400000000000000000000003321463674324000270000ustar00rootroot00000000000000version: "3" services: cont: image: nopush/podman-compose-test userns_mode: keep-id:uid=1000 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] x-podman: in_pod: false podman-compose-1.2.0/tests/in_pod/custom_x-podman_not_exists/000077500000000000000000000000001463674324000244525ustar00rootroot00000000000000podman-compose-1.2.0/tests/in_pod/custom_x-podman_not_exists/docker-compose.yml000066400000000000000000000002751463674324000301130ustar00rootroot00000000000000version: "3" services: cont: image: nopush/podman-compose-test userns_mode: keep-id:uid=1000 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] podman-compose-1.2.0/tests/in_pod/custom_x-podman_true/000077500000000000000000000000001463674324000232325ustar00rootroot00000000000000podman-compose-1.2.0/tests/in_pod/custom_x-podman_true/docker-compose.yml000066400000000000000000000003311463674324000266640ustar00rootroot00000000000000version: "3" services: cont: image: nopush/podman-compose-test userns_mode: keep-id:uid=1000 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] x-podman: in_pod: true podman-compose-1.2.0/tests/include/000077500000000000000000000000001463674324000172315ustar00rootroot00000000000000podman-compose-1.2.0/tests/include/docker-compose.base.yaml000066400000000000000000000002331463674324000237360ustar00rootroot00000000000000version: '3.6' services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"] podman-compose-1.2.0/tests/include/docker-compose.extend.yaml000066400000000000000000000002331463674324000243130ustar00rootroot00000000000000version: '3.6' services: web2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8004"] podman-compose-1.2.0/tests/include/docker-compose.yaml000066400000000000000000000001251463674324000230250ustar00rootroot00000000000000version: '3.6' include: - docker-compose.base.yaml - docker-compose.extend.yaml podman-compose-1.2.0/tests/interpolation/000077500000000000000000000000001463674324000204755ustar00rootroot00000000000000podman-compose-1.2.0/tests/interpolation/.env000066400000000000000000000000621463674324000212640ustar00rootroot00000000000000DOT_ENV_VARIABLE=This value is from the .env file podman-compose-1.2.0/tests/interpolation/docker-compose-colon-question-error.yml000066400000000000000000000003521463674324000302360ustar00rootroot00000000000000version: "3.7" services: variables: image: busybox command: ["/bin/busybox", "sh", "-c", "export | grep EXAMPLE"] environment: EXAMPLE_COLON_QUESTION_ERROR: ${NOT_A_VARIABLE:?Missing variable} podman-compose-1.2.0/tests/interpolation/docker-compose-question-error.yml000066400000000000000000000003431463674324000271260ustar00rootroot00000000000000version: "3.7" services: variables: image: busybox command: ["/bin/busybox", "sh", "-c", "export | grep EXAMPLE"] environment: EXAMPLE_QUESTION_ERROR: ${NOT_A_VARIABLE?Missing variable} podman-compose-1.2.0/tests/interpolation/docker-compose.yml000066400000000000000000000010241463674324000241270ustar00rootroot00000000000000version: "3.7" services: variables: image: busybox command: ["/bin/busybox", "sh", "-c", "export | grep EXAMPLE"] environment: EXAMPLE_VARIABLE: "Host user: $USER" EXAMPLE_BRACES: "Host user: ${USER}" EXAMPLE_COLON_DASH_DEFAULT: ${NOT_A_VARIABLE:-My default} EXAMPLE_DASH_DEFAULT: ${NOT_A_VARIABLE-My other default} EXAMPLE_DOT_ENV: $DOT_ENV_VARIABLE EXAMPLE_LITERAL: This is a $$literal EXAMPLE_EMPTY: $NOT_A_VARIABLE podman-compose-1.2.0/tests/ipam_default/000077500000000000000000000000001463674324000202405ustar00rootroot00000000000000podman-compose-1.2.0/tests/ipam_default/docker-compose.yaml000066400000000000000000000004041463674324000240340ustar00rootroot00000000000000version: '3' # --ipam-driver must not be pass when driver is "default" networks: ipam_test_default: ipam: driver: default config: - subnet: 172.19.0.0/24 services: testipam: image: busybox command: ["echo", "ipamtest"] podman-compose-1.2.0/tests/multicompose/000077500000000000000000000000001463674324000203265ustar00rootroot00000000000000podman-compose-1.2.0/tests/multicompose/README.md000066400000000000000000000011331463674324000216030ustar00rootroot00000000000000# Multiple compose files to make sure we get results similar to ``` docker-compose -f d1/docker-compose.yml -f d2/docker-compose.yml up -d docker exec -ti d1_web1_1 sh -c 'set' docker exec -ti d1_web2_1 sh -c 'set' curl http://${d1_web1_1}:8001/index.txt curl http://${d1_web1_1}:8002/index.txt ``` we need to verify - project base directory and project name is `d1` - `var12='d1/12.env'` which means `enf_file` was appended not replaced (which means that we normalize to array before merge) - `var2='d1/2.env'` which means that paths inside `d2/docker-compose.yml` directory are relative to `d1` podman-compose-1.2.0/tests/multicompose/d1/000077500000000000000000000000001463674324000206325ustar00rootroot00000000000000podman-compose-1.2.0/tests/multicompose/d1/1.env000066400000000000000000000000161463674324000215010ustar00rootroot00000000000000var1=d1/1.env podman-compose-1.2.0/tests/multicompose/d1/12.env000066400000000000000000000000201463674324000215560ustar00rootroot00000000000000var12=d1/12.env podman-compose-1.2.0/tests/multicompose/d1/2.env000066400000000000000000000000161463674324000215020ustar00rootroot00000000000000var2=d1/2.env podman-compose-1.2.0/tests/multicompose/d1/docker-compose.yml000066400000000000000000000003661463674324000242740ustar00rootroot00000000000000version: '3' services: web1: image: busybox command: busybox httpd -h /var/www/html/ -f -p 8001 volumes: - ./1.env:/var/www/html/index.txt:z env_file: ./1.env labels: l1: v1 environment: - mykey1=myval1 podman-compose-1.2.0/tests/multicompose/d2/000077500000000000000000000000001463674324000206335ustar00rootroot00000000000000podman-compose-1.2.0/tests/multicompose/d2/12.env000066400000000000000000000000201463674324000215570ustar00rootroot00000000000000var12=d2/12.env podman-compose-1.2.0/tests/multicompose/d2/2.env000066400000000000000000000000161463674324000215030ustar00rootroot00000000000000var2=d2/2.env podman-compose-1.2.0/tests/multicompose/d2/docker-compose.yml000066400000000000000000000005221463674324000242670ustar00rootroot00000000000000version: '3' services: web1: image: busybox env_file: ./12.env labels: - l1=v2 - l2=v2 environment: mykey1: myval2 mykey2: myval2 web2: image: busybox command: busybox httpd -h /var/www/html/ -f -p 8002 volumes: - ./2.env:/var/www/html/index.txt:z env_file: ./2.env podman-compose-1.2.0/tests/nethost/000077500000000000000000000000001463674324000172725ustar00rootroot00000000000000podman-compose-1.2.0/tests/nethost/docker-compose.yaml000066400000000000000000000001601463674324000230650ustar00rootroot00000000000000version: '3' services: web: image: busybox command: httpd -f -p 8123 -h /etc/ network_mode: host podman-compose-1.2.0/tests/netprio/000077500000000000000000000000001463674324000172665ustar00rootroot00000000000000podman-compose-1.2.0/tests/netprio/docker-compose.yaml000066400000000000000000000004441463674324000230660ustar00rootroot00000000000000--- # https://github.com/compose-spec/compose-spec/blob/master/spec.md#priority services: app: image: busybox command: top networks: app_net_1: app_net_2: priority: 1000 app_net_3: priority: 100 networks: app_net_1: app_net_2: app_net_3: podman-compose-1.2.0/tests/nets_test1/000077500000000000000000000000001463674324000176775ustar00rootroot00000000000000podman-compose-1.2.0/tests/nets_test1/docker-compose.yml000066400000000000000000000010601463674324000233310ustar00rootroot00000000000000version: "3" services: web1: image: busybox hostname: web1 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html ports: - 8001:8001 volumes: - ./test1.txt:/var/www/html/index.txt:ro,z web2: image: busybox hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html ports: - 8002:8001 volumes: - ./test2.txt:/var/www/html/index.txt:ro,z podman-compose-1.2.0/tests/nets_test1/test1.txt000066400000000000000000000000061463674324000214740ustar00rootroot00000000000000test1 podman-compose-1.2.0/tests/nets_test1/test2.txt000066400000000000000000000000061463674324000214750ustar00rootroot00000000000000test2 podman-compose-1.2.0/tests/nets_test2/000077500000000000000000000000001463674324000177005ustar00rootroot00000000000000podman-compose-1.2.0/tests/nets_test2/docker-compose.yml000066400000000000000000000011071463674324000233340ustar00rootroot00000000000000version: "3" networks: mystack: services: web1: image: busybox hostname: web1 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html ports: - 8001:8001 volumes: - ./test1.txt:/var/www/html/index.txt:ro,z web2: image: busybox hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html ports: - 8002:8001 volumes: - ./test2.txt:/var/www/html/index.txt:ro,z podman-compose-1.2.0/tests/nets_test2/test1.txt000066400000000000000000000000061463674324000214750ustar00rootroot00000000000000test1 podman-compose-1.2.0/tests/nets_test2/test2.txt000066400000000000000000000000061463674324000214760ustar00rootroot00000000000000test2 podman-compose-1.2.0/tests/nets_test3/000077500000000000000000000000001463674324000177015ustar00rootroot00000000000000podman-compose-1.2.0/tests/nets_test3/docker-compose.yml000066400000000000000000000020611463674324000233350ustar00rootroot00000000000000version: "3" networks: net1: net2: services: web1: image: busybox #container_name: web1 hostname: web1 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: - net1 ports: - 8001:8001 volumes: - ./test1.txt:/var/www/html/index.txt:ro,z web2: image: busybox #container_name: web2 hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: - net1 - net2 ports: - 8002:8001 volumes: - ./test2.txt:/var/www/html/index.txt:ro,z web3: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: net1: aliases: - alias11 - alias12 net2: aliases: - alias21 volumes: - ./test2.txt:/var/www/html/index.txt:ro,z podman-compose-1.2.0/tests/nets_test3/test1.txt000066400000000000000000000000061463674324000214760ustar00rootroot00000000000000test1 podman-compose-1.2.0/tests/nets_test3/test2.txt000066400000000000000000000000061463674324000214770ustar00rootroot00000000000000test2 podman-compose-1.2.0/tests/nets_test_ip/000077500000000000000000000000001463674324000203065ustar00rootroot00000000000000podman-compose-1.2.0/tests/nets_test_ip/docker-compose.yml000066400000000000000000000034631463674324000237510ustar00rootroot00000000000000version: "3" networks: shared-network: driver: bridge ipam: config: - subnet: "172.19.1.0/24" internal-network: driver: bridge ipam: config: - subnet: "172.19.2.0/24" services: web1: image: busybox hostname: web1 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: shared-network: ipv4_address: "172.19.1.10" x-podman.mac_address: "02:01:01:00:01:01" internal-network: ipv4_address: "172.19.2.10" x-podman.mac_address: "02:01:01:00:02:01" volumes: - ./test1.txt:/var/www/html/index.txt:ro,z web2: image: busybox hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html mac_address: "02:01:01:00:02:02" networks: internal-network: ipv4_address: "172.19.2.11" volumes: - ./test2.txt:/var/www/html/index.txt:ro,z web3: image: busybox hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: internal-network: volumes: - ./test3.txt:/var/www/html/index.txt:ro,z web4: image: busybox hostname: web2 command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html networks: internal-network: shared-network: ipv4_address: "172.19.1.13" volumes: - ./test4.txt:/var/www/html/index.txt:ro,z podman-compose-1.2.0/tests/nets_test_ip/test1.txt000066400000000000000000000000061463674324000221030ustar00rootroot00000000000000test1 podman-compose-1.2.0/tests/nets_test_ip/test2.txt000066400000000000000000000000061463674324000221040ustar00rootroot00000000000000test2 podman-compose-1.2.0/tests/nets_test_ip/test3.txt000066400000000000000000000000061463674324000221050ustar00rootroot00000000000000test3 podman-compose-1.2.0/tests/nets_test_ip/test4.txt000066400000000000000000000000061463674324000221060ustar00rootroot00000000000000test4 podman-compose-1.2.0/tests/no_services/000077500000000000000000000000001463674324000201255ustar00rootroot00000000000000podman-compose-1.2.0/tests/no_services/docker-compose.yaml000066400000000000000000000001661463674324000237260ustar00rootroot00000000000000version: '3' networks: shared-network: driver: bridge ipam: config: - subnet: 172.19.0.0/24 podman-compose-1.2.0/tests/pid/000077500000000000000000000000001463674324000163625ustar00rootroot00000000000000podman-compose-1.2.0/tests/pid/docker-compose.yml000066400000000000000000000001341463674324000220150ustar00rootroot00000000000000version: "3" services: serv: image: busybox pid: host command: sh -c "ps all" podman-compose-1.2.0/tests/ports/000077500000000000000000000000001463674324000167555ustar00rootroot00000000000000podman-compose-1.2.0/tests/ports/docker-compose.yml000066400000000000000000000017201463674324000224120ustar00rootroot00000000000000version: "3" services: web1: image: nopush/podman-compose-test hostname: web1 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html ports: - 8001:8001 volumes: - ./test1.txt:/var/www/html/index.txt:ro,z web2: image: nopush/podman-compose-test hostname: web2 command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"] working_dir: /var/www/html ports: - 8002:8002 - target: 8003 host_ip: 127.0.0.1 published: 8003 protocol: udp - target: 8004 host_ip: 127.0.0.1 published: 8004 protocol: tcp - target: 8005 published: 8005 - target: 8006 protocol: udp - target: 8007 host_ip: 127.0.0.1 volumes: - ./test2.txt:/var/www/html/index.txt:ro,z podman-compose-1.2.0/tests/ports/test1.txt000066400000000000000000000000061463674324000205520ustar00rootroot00000000000000test1 podman-compose-1.2.0/tests/ports/test2.txt000066400000000000000000000000061463674324000205530ustar00rootroot00000000000000test2 podman-compose-1.2.0/tests/profile/000077500000000000000000000000001463674324000172465ustar00rootroot00000000000000podman-compose-1.2.0/tests/profile/docker-compose.yml000066400000000000000000000012261463674324000227040ustar00rootroot00000000000000version: "3" services: default-service: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp service-1: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp profiles: - profile-1 service-2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp profiles: - profile-2 podman-compose-1.2.0/tests/seccomp/000077500000000000000000000000001463674324000172375ustar00rootroot00000000000000podman-compose-1.2.0/tests/seccomp/docker-compose.yml000066400000000000000000000003531463674324000226750ustar00rootroot00000000000000version: "3" services: web1: image: busybox command: httpd -f -p 80 -h /var/www/html volumes: - ./docker-compose.yml:/var/www/html/index.html ports: - "8080:80" security_opt: - seccomp:unconfined podman-compose-1.2.0/tests/secrets/000077500000000000000000000000001463674324000172565ustar00rootroot00000000000000podman-compose-1.2.0/tests/secrets/bad_external_name/000077500000000000000000000000001463674324000227065ustar00rootroot00000000000000podman-compose-1.2.0/tests/secrets/bad_external_name/docker-compose.yaml000066400000000000000000000004061463674324000265040ustar00rootroot00000000000000version: "3.8" services: test: image: busybox command: - cat - /run/secrets/new_secret tmpfs: - /run - /tmp secrets: - new_secret secrets: new_secret: external: true name: my_secret podman-compose-1.2.0/tests/secrets/bad_external_target/000077500000000000000000000000001463674324000232545ustar00rootroot00000000000000podman-compose-1.2.0/tests/secrets/bad_external_target/docker-compose.yaml000066400000000000000000000004261463674324000270540ustar00rootroot00000000000000version: "3.8" services: test: image: busybox command: - cat - /run/secrets/my_secret_2 tmpfs: - /run - /tmp secrets: - source: my_secret target: new_secret secrets: my_secret: external: true podman-compose-1.2.0/tests/secrets/docker-compose.yaml000066400000000000000000000021121463674324000230500ustar00rootroot00000000000000--- # echo "sec" | podman secret create my_secret - # echo "sec2" | podman secret create my_secret_2 - # echo "sec3" | podman secret create my_secret_3 - version: "3.8" services: test: image: busybox command: - /tmp/print_secrets.sh tmpfs: - /run - /tmp volumes: - ./print_secrets.sh:/tmp/print_secrets.sh:z secrets: - my_secret - my_secret_2 - source: my_secret_3 target: my_secret_3 uid: '103' gid: '103' mode: 400 - file_secret - source: file_secret target: custom_name - source: file_secret target: /etc/custom_location - source: file_secret target: unused_params_warning uid: '103' gid: '103' mode: 400 - source: my_secret target: ENV_SECRET type: env secrets: my_secret: external: true my_secret_2: external: true name: my_secret_2 my_secret_3: external: true name: my_secret_3 file_secret: file: ./my_secret podman-compose-1.2.0/tests/secrets/my_secret000066400000000000000000000000361463674324000211720ustar00rootroot00000000000000important-secret-is-important podman-compose-1.2.0/tests/secrets/print_secrets.sh000077500000000000000000000001731463674324000225020ustar00rootroot00000000000000#!/bin/sh ls -la /run/secrets/* ls -la /etc/custom_location cat /run/secrets/* cat /etc/custom_location env | grep SECRET podman-compose-1.2.0/tests/selinux/000077500000000000000000000000001463674324000172755ustar00rootroot00000000000000podman-compose-1.2.0/tests/selinux/docker-compose.yml000066400000000000000000000004161463674324000227330ustar00rootroot00000000000000version: "3" services: web1: image: busybox command: httpd -f -p 80 -h /var/www/html volumes: - type: bind source: ./docker-compose.yml target: /var/www/html/index.html bind: selinux: z ports: - "8080:80" podman-compose-1.2.0/tests/short/000077500000000000000000000000001463674324000167455ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/data/000077500000000000000000000000001463674324000176565ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/data/redis/000077500000000000000000000000001463674324000207645ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/data/redis/.keep000066400000000000000000000000001463674324000216770ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/data/web/000077500000000000000000000000001463674324000204335ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/data/web/.keep000066400000000000000000000000001463674324000213460ustar00rootroot00000000000000podman-compose-1.2.0/tests/short/docker-compose.yaml000066400000000000000000000022031463674324000225400ustar00rootroot00000000000000version: "3" services: redis: image: redis:alpine command: ["redis-server", "--appendonly yes", "--notify-keyspace-events", "Ex"] volumes: - ./data/redis:/data:z tmpfs: /run1 ports: - "6379" environment: - SECRET_KEY=aabbcc - ENV_IS_SET web: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"] working_dir: /var/www/html volumes: - /var/www/html tmpfs: - /run - /tmp web1: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] working_dir: /var/www/html volumes: - ./data/web:/var/www/html:ro,z web2: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"] working_dir: /var/www/html volumes: - ~/Downloads/www:/var/www/html:ro,z web3: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"] working_dir: /var/www/html volumes: - /var/www/html:/var/www/html:ro,z podman-compose-1.2.0/tests/test_podman_compose.py000066400000000000000000000056101463674324000222240ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from pathlib import Path from .test_utils import RunSubprocessMixin def base_path(): """Returns the base path for the project""" return Path(__file__).parent.parent def test_path(): """Returns the path to the tests directory""" return os.path.join(base_path(), "tests") def podman_compose_path(): """Returns the path to the podman compose script""" return os.path.join(base_path(), "podman_compose.py") class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin): def test_extends_w_file_subdir(self): """ Test that podman-compose can execute podman-compose -f up with extended File which includes a build context :return: """ main_path = Path(__file__).parent.parent command_up = [ "coverage", "run", str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), "up", "-d", ] command_check_container = [ "coverage", "run", str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), "ps", "--format", '{{.Image}}', ] self.run_subprocess_assert_returncode(command_up) # check container was created and exists out, _ = self.run_subprocess_assert_returncode(command_check_container) self.assertEqual(out, b'localhost/subdir_test:me\n') # cleanup test image(tags) self.run_subprocess_assert_returncode([ str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), "down", ]) self.run_subprocess_assert_returncode([ "podman", "rmi", "--force", "localhost/subdir_test:me", ]) # check container did not exists anymore out, _ = self.run_subprocess_assert_returncode(command_check_container) self.assertEqual(out, b'') def test_extends_w_empty_service(self): """ Test that podman-compose can execute podman-compose -f up with extended File which includes an empty service. (e.g. if the file is used as placeholder for more complex configurations.) """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "extends_w_empty_service", "docker-compose.yml")), "up", "-d", ] self.run_subprocess_assert_returncode(command_up) podman-compose-1.2.0/tests/test_podman_compose_additional_contexts.py000066400000000000000000000024401463674324000263410ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how additional contexts are passed to podman.""" import os import subprocess import unittest from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path def compose_yaml_path(): """ "Returns the path to the compose file used for this test module""" return os.path.join(test_path(), "additional_contexts", "project") class TestComposeBuildAdditionalContexts(unittest.TestCase): def test_build_additional_context(self): """podman build should receive additional contexts as --build-context See additional_context/project/docker-compose.yaml for context paths """ cmd = ( "coverage", "run", podman_compose_path(), "--dry-run", "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yml"), "build", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True, ) self.assertEqual(p.returncode, 0) self.assertIn("--build-context=data=../data_for_dict", p.stdout) self.assertIn("--build-context=data=../data_for_list", p.stdout) podman-compose-1.2.0/tests/test_podman_compose_build_secrets.py000066400000000000000000000056561463674324000251450ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how secrets in files are passed to podman.""" import os import subprocess import unittest from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path def compose_yaml_path(): """ "Returns the path to the compose file used for this test module""" return os.path.join(test_path(), "build_secrets") class TestComposeBuildSecrets(unittest.TestCase): def test_run_secret(self): """podman run should receive file secrets as --volume See build_secrets/docker-compose.yaml for secret names and mount points (aka targets) """ cmd = ( "coverage", "run", podman_compose_path(), "--dry-run", "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml"), "run", "test", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertEqual(p.returncode, 0) secret_path = os.path.join(compose_yaml_path(), "my_secret") self.assertIn(f"--volume {secret_path}:/run/secrets/run_secret:ro,rprivate,rbind", p.stdout) self.assertIn(f"--volume {secret_path}:/tmp/run_secret2:ro,rprivate,rbind", p.stdout) def test_build_secret(self): """podman build should receive secrets as --secret, so that they can be used inside the Dockerfile in "RUN --mount=type=secret ..." commands. """ cmd = ( "coverage", "run", podman_compose_path(), "--dry-run", "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml"), "build", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertEqual(p.returncode, 0) secret_path = os.path.join(compose_yaml_path(), "my_secret") self.assertIn(f"--secret id=build_secret,src={secret_path}", p.stdout) self.assertIn(f"--secret id=build_secret2,src={secret_path}", p.stdout) def test_invalid_build_secret(self): """build secrets in docker-compose file can only have a target argument without directory component """ cmd = ( "coverage", "run", podman_compose_path(), "--dry-run", "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml.invalid"), "build", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertNotEqual(p.returncode, 0) self.assertIn( 'ValueError: ERROR: Build secret "build_secret" has invalid target "/build_secret"', p.stdout, ) podman-compose-1.2.0/tests/test_podman_compose_build_ulimits.py000066400000000000000000000060341463674324000251520ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how ulimits are applied in podman-compose build.""" import os import subprocess import unittest from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path def compose_yaml_path(): """ "Returns the path to the compose file used for this test module""" return os.path.join(test_path(), "ulimit_build") class TestComposeBuildUlimits(unittest.TestCase): def test_build_ulimits_ulimit1(self): """podman build should receive and apply limits when building service ulimit1""" cmd = ( "coverage", "run", podman_compose_path(), "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml"), "build", "--no-cache", "ulimit1", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertEqual(p.returncode, 0) self.assertIn("--ulimit nofile=1001", p.stdout) self.assertIn("soft nofile limit: 1001", p.stdout) self.assertIn("hard nofile limit: 1001", p.stdout) def test_build_ulimits_ulimit2(self): """podman build should receive and apply limits when building service ulimit2""" cmd = ( "coverage", "run", podman_compose_path(), "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml"), "build", "--no-cache", "ulimit2", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertEqual(p.returncode, 0) self.assertIn("--ulimit nofile=1002", p.stdout) self.assertIn("--ulimit nproc=1002:2002", p.stdout) self.assertIn("soft process limit: 1002", p.stdout) self.assertIn("hard process limit: 2002", p.stdout) self.assertIn("soft nofile limit: 1002", p.stdout) self.assertIn("hard nofile limit: 1002", p.stdout) def test_build_ulimits_ulimit3(self): """podman build should receive and apply limits when building service ulimit3""" cmd = ( "coverage", "run", podman_compose_path(), "--verbose", "-f", os.path.join(compose_yaml_path(), "docker-compose.yaml"), "build", "--no-cache", "ulimit3", ) p = subprocess.run( cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True ) self.assertEqual(p.returncode, 0) self.assertIn("--ulimit nofile=1003", p.stdout) self.assertIn("--ulimit nproc=1003:2003", p.stdout) self.assertIn("soft process limit: 1003", p.stdout) self.assertIn("hard process limit: 2003", p.stdout) self.assertIn("soft nofile limit: 1003", p.stdout) self.assertIn("hard nofile limit: 1003", p.stdout) podman-compose-1.2.0/tests/test_podman_compose_config.py000066400000000000000000000052501463674324000235510ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """ test_podman_compose_config.py Tests the podman-compose config command which is used to return defined compose services. """ # pylint: disable=redefined-outer-name import os import unittest from parameterized import parameterized from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path from .test_utils import RunSubprocessMixin def profile_compose_file(): """ "Returns the path to the `profile` compose file used for this test module""" return os.path.join(test_path(), "profile", "docker-compose.yml") class TestComposeConfig(unittest.TestCase, RunSubprocessMixin): def test_config_no_profiles(self): """ Tests podman-compose config command without profile enablement. """ config_cmd = [ "coverage", "run", podman_compose_path(), "-f", profile_compose_file(), "config", ] out, _ = self.run_subprocess_assert_returncode(config_cmd) string_output = out.decode("utf-8") self.assertIn("default-service", string_output) self.assertNotIn("service-1", string_output) self.assertNotIn("service-2", string_output) @parameterized.expand( [ ( ["--profile", "profile-1", "config"], {"default-service": True, "service-1": True, "service-2": False}, ), ( ["--profile", "profile-2", "config"], {"default-service": True, "service-1": False, "service-2": True}, ), ( ["--profile", "profile-1", "--profile", "profile-2", "config"], {"default-service": True, "service-1": True, "service-2": True}, ), ], ) def test_config_profiles(self, profiles, expected_services): """ Tests podman-compose :param profiles: The enabled profiles for the parameterized test. :param expected_services: Dictionary used to model the expected "enabled" services in the profile. Key = service name, Value = True if the service is enabled, otherwise False. """ config_cmd = ["coverage", "run", podman_compose_path(), "-f", profile_compose_file()] config_cmd.extend(profiles) out, _ = self.run_subprocess_assert_returncode(config_cmd) actual_output = out.decode("utf-8") self.assertEqual(len(expected_services), 3) actual_services = {} for service, _ in expected_services.items(): actual_services[service] = service in actual_output self.assertEqual(expected_services, actual_services) podman-compose-1.2.0/tests/test_podman_compose_in_pod.py000066400000000000000000000376401463674324000235640ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from pathlib import Path from .test_utils import RunSubprocessMixin def base_path(): """Returns the base path for the project""" return Path(__file__).parent.parent def test_path(): """Returns the path to the tests directory""" return os.path.join(base_path(), "tests") def podman_compose_path(): """Returns the path to the podman compose script""" return os.path.join(base_path(), "podman_compose.py") # If a compose file has userns_mode set, setting in_pod to True, results in error. # Default in_pod setting is True, unless compose file provides otherwise. # Compose file provides custom in_pod option, which can be overridden by command line in_pod option. # Test all combinations of command line argument in_pod and compose file argument in_pod. class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin): # compose file provides x-podman in_pod=false def test_x_podman_in_pod_false_command_line_in_pod_not_exists(self): """ Test that podman-compose will not create a pod, when x-podman in_pod=false and command line does not provide this option """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "down", ] try: self.run_subprocess_assert_returncode(command_up) finally: self.run_subprocess_assert_returncode(down_cmd) command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] # throws an error, can not actually find this pod because it was not created self.run_subprocess_assert_returncode(command_rm_pod, expected_returncode=1) def test_x_podman_in_pod_false_command_line_in_pod_true(self): """ Test that podman-compose does not allow pod creating even with command line in_pod=True when --userns and --pod are set together: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=True", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] # should throw an error of not being able to find this pod (because it should not have # been created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) def test_x_podman_in_pod_false_command_line_in_pod_false(self): """ Test that podman-compose will not create a pod as command line sets in_pod=False """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=False", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "down", ] try: self.run_subprocess_assert_returncode(command_up) finally: self.run_subprocess_assert_returncode(down_cmd) command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] # can not actually find this pod because it was not created self.run_subprocess_assert_returncode(command_rm_pod, 1) def test_x_podman_in_pod_false_command_line_in_pod_empty_string(self): """ Test that podman-compose will not create a pod, when x-podman in_pod=false and command line command line in_pod="" """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") ), "down", ] try: self.run_subprocess_assert_returncode(command_up) finally: self.run_subprocess_assert_returncode(down_cmd) command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] # can not actually find this pod because it was not created self.run_subprocess_assert_returncode(command_rm_pod, 1) # compose file provides x-podman in_pod=true def test_x_podman_in_pod_true_command_line_in_pod_not_exists(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together even when x-podman in_pod=true: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] # should throw an error of not being able to find this pod (it should not have been # created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) def test_x_podman_in_pod_true_command_line_in_pod_true(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together even when x-podman in_pod=true and and command line in_pod=True: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=True", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] # should throw an error of not being able to find this pod (because it should not have # been created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) def test_x_podman_in_pod_true_command_line_in_pod_false(self): """ Test that podman-compose will not create a pod as command line sets in_pod=False """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=False", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") ), "down", ] try: self.run_subprocess_assert_returncode(command_up) finally: self.run_subprocess_assert_returncode(down_cmd) command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] # can not actually find this pod because it was not created self.run_subprocess_assert_returncode(command_rm_pod, 1) def test_x_podman_in_pod_true_command_line_in_pod_empty_string(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together even when x-podman in_pod=true and command line in_pod="": throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=", "-f", str( main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] # should throw an error of not being able to find this pod (because it should not have # been created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) # compose file does not provide x-podman in_pod def test_x_podman_in_pod_not_exists_command_line_in_pod_not_exists(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "-f", str( main_path.joinpath( "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" ) ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] # should throw an error of not being able to find this pod (it should not have been # created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) def test_x_podman_in_pod_not_exists_command_line_in_pod_true(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together even when x-podman in_pod=true: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container was not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=True", "-f", str( main_path.joinpath( "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" ) ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] # should throw an error of not being able to find this pod (because it should not have # been created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) def test_x_podman_in_pod_not_exists_command_line_in_pod_false(self): """ Test that podman-compose will not create a pod as command line sets in_pod=False """ main_path = Path(__file__).parent.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=False", "-f", str( main_path.joinpath( "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" ) ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", str( main_path.joinpath( "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" ) ), "down", ] try: self.run_subprocess_assert_returncode(command_up) finally: self.run_subprocess_assert_returncode(down_cmd) command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] # can not actually find this pod because it was not created self.run_subprocess_assert_returncode(command_rm_pod, 1) def test_x_podman_in_pod_not_exists_command_line_in_pod_empty_string(self): """ Test that podman-compose does not allow pod creating when --userns and --pod are set together: throws an error """ main_path = Path(__file__).parent.parent # FIXME: creates a pod anyway, although it should not # Container was not created, so command 'down' is not needed command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "--in-pod=", "-f", str( main_path.joinpath( "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" ) ), "up", "-d", ] try: out, err = self.run_subprocess_assert_returncode(command_up) self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) finally: command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] # should throw an error of not being able to find this pod (because it should not have # been created) and have expected_returncode=1 (see FIXME above) self.run_subprocess_assert_returncode(command_rm_pod) podman-compose-1.2.0/tests/test_podman_compose_include.py000066400000000000000000000041061463674324000237260ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from pathlib import Path from .test_utils import RunSubprocessMixin class TestPodmanComposeInclude(unittest.TestCase, RunSubprocessMixin): def test_podman_compose_include(self): """ Test that podman-compose can execute podman-compose -f up with include :return: """ main_path = Path(__file__).parent.parent command_up = [ "coverage", "run", str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "include", "docker-compose.yaml")), "up", "-d", ] command_check_container = [ "podman", "ps", "-a", "--filter", "label=io.podman.compose.project=include", "--format", '"{{.Image}}"', ] command_container_id = [ "podman", "ps", "-a", "--filter", "label=io.podman.compose.project=include", "--format", '"{{.ID}}"', ] command_down = ["podman", "rm", "--force"] self.run_subprocess_assert_returncode(command_up) out, _ = self.run_subprocess_assert_returncode(command_check_container) expected_output = b'"localhost/nopush/podman-compose-test:latest"\n' * 2 self.assertEqual(out, expected_output) # Get container ID to remove it out, _ = self.run_subprocess_assert_returncode(command_container_id) self.assertNotEqual(out, b"") container_ids = out.decode().strip().split("\n") container_ids = [container_id.replace('"', "") for container_id in container_ids] command_down.extend(container_ids) out, _ = self.run_subprocess_assert_returncode(command_down) # cleanup test image(tags) self.assertNotEqual(out, b"") # check container did not exists anymore out, _ = self.run_subprocess_assert_returncode(command_check_container) self.assertEqual(out, b"") podman-compose-1.2.0/tests/test_podman_compose_networks.py000066400000000000000000000064471463674324000241710ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """ test_podman_compose_networks.py Tests the podman networking parameters """ # pylint: disable=redefined-outer-name import os import unittest from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path from .test_utils import RunSubprocessMixin class TestPodmanComposeNetwork(RunSubprocessMixin, unittest.TestCase): @staticmethod def compose_file(): """Returns the path to the compose file used for this test module""" return os.path.join(test_path(), "nets_test_ip", "docker-compose.yml") def teardown(self): """ Ensures that the services within the "profile compose file" are removed between each test case. """ # run the test case yield down_cmd = [ "coverage", "run", podman_compose_path(), "-f", self.compose_file(), "kill", "-a", ] self.run_subprocess(down_cmd) def test_networks(self): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", self.compose_file(), "up", "-d", "--force-recreate", ] self.run_subprocess_assert_returncode(up_cmd) check_cmd = [ podman_compose_path(), "-f", self.compose_file(), "ps", "--format", '"{{.Names}}"', ] out, _ = self.run_subprocess_assert_returncode(check_cmd) self.assertIn(b"nets_test_ip_web1_1", out) self.assertIn(b"nets_test_ip_web2_1", out) expected_wget = { "172.19.1.10": "test1", "172.19.2.10": "test1", "172.19.2.11": "test2", "web3": "test3", "172.19.1.13": "test4", } for service in ("web1", "web2"): for ip, expect in expected_wget.items(): wget_cmd = [ podman_compose_path(), "-f", self.compose_file(), "exec", service, "wget", "-q", "-O-", f"http://{ip}:8001/index.txt", ] out, _ = self.run_subprocess_assert_returncode(wget_cmd) self.assertEqual(f"{expect}\r\n", out.decode('utf-8')) expected_macip = { "web1": { "eth0": ["172.19.1.10", "02:01:01:00:01:01"], "eth1": ["172.19.2.10", "02:01:01:00:02:01"], }, "web2": {"eth0": ["172.19.2.11", "02:01:01:00:02:02"]}, } for service, interfaces in expected_macip.items(): ip_cmd = [ podman_compose_path(), "-f", self.compose_file(), "exec", service, "ip", "addr", "show", ] out, _ = self.run_subprocess_assert_returncode(ip_cmd) for interface, values in interfaces.items(): ip, mac = values self.assertIn(f"ether {mac}", out.decode('utf-8')) self.assertIn(f"inet {ip}/", out.decode('utf-8')) podman-compose-1.2.0/tests/test_podman_compose_tests.py000066400000000000000000000122201463674324000234410ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """ test_podman_compose_up_down.py Tests the podman compose up and down commands used to create and remove services. """ # pylint: disable=redefined-outer-name import os import unittest from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path from .test_utils import RunSubprocessMixin class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin): def test_exit_from(self): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "exit-from", "docker-compose.yaml"), "up", ] self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh1"], 1) self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh2"], 2) def test_run(self): """ This will test depends_on as well """ run_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "deps", "docker-compose.yaml"), "run", "--rm", "sleep", "/bin/sh", "-c", "wget -q -O - http://web:8000/hosts", ] out, _ = self.run_subprocess_assert_returncode(run_cmd) self.assertIn(b'127.0.0.1\tlocalhost', out) # Run it again to make sure we can run it twice. I saw an issue where a second run, with # the container left up, would fail run_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "deps", "docker-compose.yaml"), "run", "--rm", "sleep", "/bin/sh", "-c", "wget -q -O - http://web:8000/hosts", ] out, _ = self.run_subprocess_assert_returncode(run_cmd) self.assertIn(b'127.0.0.1\tlocalhost', out) # This leaves a container running. Not sure it's intended, but it matches docker-compose down_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "deps", "docker-compose.yaml"), "down", ] self.run_subprocess_assert_returncode(down_cmd) def test_up_with_ports(self): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "ports", "docker-compose.yml"), "up", "-d", "--force-recreate", ] down_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "ports", "docker-compose.yml"), "down", "--volumes", ] try: self.run_subprocess_assert_returncode(up_cmd) finally: self.run_subprocess_assert_returncode(down_cmd) def test_down_with_vols(self): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "vol", "docker-compose.yaml"), "up", "-d", ] down_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "vol", "docker-compose.yaml"), "down", "--volumes", ] try: self.run_subprocess_assert_returncode(["podman", "volume", "create", "my-app-data"]) self.run_subprocess_assert_returncode([ "podman", "volume", "create", "actual-name-of-volume", ]) self.run_subprocess_assert_returncode(up_cmd) self.run_subprocess(["podman", "inspect", "volume", ""]) finally: out, _, return_code = self.run_subprocess(down_cmd) self.run_subprocess(["podman", "volume", "rm", "my-app-data"]) self.run_subprocess(["podman", "volume", "rm", "actual-name-of-volume"]) self.assertEqual(return_code, 0) def test_down_with_orphans(self): container_id, _ = self.run_subprocess_assert_returncode([ "podman", "run", "--rm", "-d", "nopush/podman-compose-test", "dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000", ]) down_cmd = [ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "ports", "docker-compose.yml"), "down", "--volumes", "--remove-orphans", ] self.run_subprocess_assert_returncode(down_cmd) self.run_subprocess_assert_returncode( [ "podman", "container", "exists", container_id.decode("utf-8"), ], 1, ) podman-compose-1.2.0/tests/test_podman_compose_up_down.py000066400000000000000000000050021463674324000237520ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """ test_podman_compose_up_down.py Tests the podman compose up and down commands used to create and remove services. """ # pylint: disable=redefined-outer-name import os import unittest from parameterized import parameterized from .test_podman_compose import podman_compose_path from .test_podman_compose import test_path from .test_utils import RunSubprocessMixin def profile_compose_file(): """ "Returns the path to the `profile` compose file used for this test module""" return os.path.join(test_path(), "profile", "docker-compose.yml") class TestUpDown(unittest.TestCase, RunSubprocessMixin): def tearDown(self): """ Ensures that the services within the "profile compose file" are removed between each test case. """ # run the test case down_cmd = [ "coverage", "run", podman_compose_path(), "--profile", "profile-1", "--profile", "profile-2", "-f", profile_compose_file(), "down", ] self.run_subprocess(down_cmd) @parameterized.expand( [ ( ["--profile", "profile-1", "up", "-d"], {"default-service": True, "service-1": True, "service-2": False}, ), ( ["--profile", "profile-2", "up", "-d"], {"default-service": True, "service-1": False, "service-2": True}, ), ( ["--profile", "profile-1", "--profile", "profile-2", "up", "-d"], {"default-service": True, "service-1": True, "service-2": True}, ), ], ) def test_up(self, profiles, expected_services): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", profile_compose_file(), ] up_cmd.extend(profiles) self.run_subprocess_assert_returncode(up_cmd) check_cmd = [ "podman", "container", "ps", "--format", '"{{.Names}}"', ] out, _ = self.run_subprocess_assert_returncode(check_cmd) self.assertEqual(len(expected_services), 3) actual_output = out.decode("utf-8") actual_services = {} for service, _ in expected_services.items(): actual_services[service] = service in actual_output self.assertEqual(expected_services, actual_services) podman-compose-1.2.0/tests/test_utils.py000066400000000000000000000022741463674324000203640ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import subprocess import time class RunSubprocessMixin: def is_debug_enabled(self): return "TESTS_DEBUG" in os.environ def run_subprocess(self, args): begin = time.time() if self.is_debug_enabled(): print("TEST_CALL", args) proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out, err = proc.communicate() if self.is_debug_enabled(): print("TEST_CALL completed", time.time() - begin) print("STDOUT:", out.decode('utf-8')) print("STDERR:", err.decode('utf-8')) return out, err, proc.returncode def run_subprocess_assert_returncode(self, args, expected_returncode=0): out, err, returncode = self.run_subprocess(args) decoded_out = out.decode('utf-8') decoded_err = err.decode('utf-8') self.assertEqual( returncode, expected_returncode, f"Invalid return code of process {returncode} != {expected_returncode}\n" f"stdout: {decoded_out}\nstderr: {decoded_err}\n", ) return out, err podman-compose-1.2.0/tests/testlogs/000077500000000000000000000000001463674324000174525ustar00rootroot00000000000000podman-compose-1.2.0/tests/testlogs/docker-compose.yml000066400000000000000000000004341463674324000231100ustar00rootroot00000000000000version: "3" services: loop1: image: busybox command: ["/bin/sh", "-c", "for i in `seq 1 10000`; do echo \"loop1: $$i\"; sleep 1; done"] loop2: image: busybox command: ["/bin/sh", "-c", "for i in `seq 1 10000`; do echo \"loop2: $$i\"; sleep 3; done"] podman-compose-1.2.0/tests/uidmaps/000077500000000000000000000000001463674324000172505ustar00rootroot00000000000000podman-compose-1.2.0/tests/uidmaps/docker-compose.yml000066400000000000000000000003721463674324000227070ustar00rootroot00000000000000version: "3.7" services: touch: image: busybox command: 'touch /mnt/test' volumes: - ./:/mnt user: 999:999 x-podman: uidmaps: - "0:1:1" - "999:0:1" gidmaps: - "0:1:1" - "999:0:1"podman-compose-1.2.0/tests/ulimit/000077500000000000000000000000001463674324000171115ustar00rootroot00000000000000podman-compose-1.2.0/tests/ulimit/Dockerfile000066400000000000000000000000561463674324000211040ustar00rootroot00000000000000FROM busybox COPY ./ulimit.sh /bin/ulimit.sh podman-compose-1.2.0/tests/ulimit/docker-compose.yaml000066400000000000000000000011311463674324000227030ustar00rootroot00000000000000version: "3" services: ulimit1: image: ulimit_test command: ["ulimit.sh" ] ulimits: nofile=1001 build: context: ./ dockerfile: Dockerfile ulimit2: image: ulimit_test command: ["ulimit.sh" ] ulimits: - nproc=1002:2002 - nofile=1002 build: context: ./ dockerfile: Dockerfile ulimit3: image: ulimit_test command: [ "ulimit.sh" ] ulimits: nofile: 1003 nproc: soft: 1003 hard: 2003 build: context: ./ dockerfile: Dockerfile podman-compose-1.2.0/tests/ulimit/ulimit.sh000077500000000000000000000002451463674324000207540ustar00rootroot00000000000000#!/bin/sh echo "soft process limit" ulimit -S -u echo "hard process limit" ulimit -H -u echo "soft nofile limit" ulimit -S -n echo "hard nofile limit" ulimit -H -n podman-compose-1.2.0/tests/ulimit_build/000077500000000000000000000000001463674324000202705ustar00rootroot00000000000000podman-compose-1.2.0/tests/ulimit_build/Dockerfile000066400000000000000000000001021463674324000222530ustar00rootroot00000000000000FROM busybox COPY ./ulimit.sh /bin/ulimit.sh RUN /bin/ulimit.sh podman-compose-1.2.0/tests/ulimit_build/docker-compose.yaml000066400000000000000000000007641463674324000240750ustar00rootroot00000000000000version: "3" services: ulimit1: image: ulimit_build_test build: context: ./ dockerfile: Dockerfile ulimits: nofile=1001 ulimit2: image: ulimit_build_test build: context: ./ dockerfile: Dockerfile ulimits: - nproc=1002:2002 - nofile=1002 ulimit3: image: ulimit_build_test build: context: ./ dockerfile: Dockerfile ulimits: nofile: 1003 nproc: soft: 1003 hard: 2003 podman-compose-1.2.0/tests/ulimit_build/ulimit.sh000077500000000000000000000002651463674324000221350ustar00rootroot00000000000000#!/bin/sh echo "soft process limit:" $(ulimit -S -u) echo "hard process limit:" $(ulimit -H -u) echo "soft nofile limit:" $(ulimit -S -n) echo "hard nofile limit:" $(ulimit -H -n) podman-compose-1.2.0/tests/vol/000077500000000000000000000000001463674324000164065ustar00rootroot00000000000000podman-compose-1.2.0/tests/vol/README.md000066400000000000000000000002231463674324000176620ustar00rootroot00000000000000# to test create the two external volumes ``` podman volume create my-app-data podman volume create actual-name-of-volume podman-compose up ``` podman-compose-1.2.0/tests/vol/docker-compose.yaml000066400000000000000000000024621463674324000222100ustar00rootroot00000000000000version: "3" services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8000"] working_dir: /var/www/html restart: always volumes: - /var/www/html tmpfs: - /run - /tmp web1: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8001"] restart: unless-stopped working_dir: /var/www/html volumes: - myvol1:/var/www/html:ro,z web2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8002"] working_dir: /var/www/html volumes: - myvol2:/var/www/html:ro web3: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8003"] working_dir: /var/www/html volumes: - myvol2:/var/www/html - data:/var/www/html_data - data2:/var/www/html_data2 - data3:/var/www/html_data3 volumes: myvol1: myvol2: labels: mylabel: myval data: name: my-app-data external: true data2: external: name: actual-name-of-volume data3: name: my-app-data3 podman-compose-1.2.0/tests/volumes_merge/000077500000000000000000000000001463674324000204575ustar00rootroot00000000000000podman-compose-1.2.0/tests/volumes_merge/docker-compose.override.yaml000066400000000000000000000003041463674324000260700ustar00rootroot00000000000000version: "3" services: web: volumes: - ./override.txt:/var/www/html/index.html:ro,z - ./override.txt:/var/www/html/index2.html:z - ./override.txt:/var/www/html/index3.html podman-compose-1.2.0/tests/volumes_merge/docker-compose.yaml000066400000000000000000000004761463674324000242640ustar00rootroot00000000000000version: "3" services: web: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/var/www/html", "-p", "8080"] ports: - 8080:8080 volumes: - ./index.txt:/var/www/html/index.html:ro,z - ./index.txt:/var/www/html/index2.html - ./index.txt:/var/www/html/index3.html:ro podman-compose-1.2.0/tests/volumes_merge/index.txt000066400000000000000000000000421463674324000223230ustar00rootroot00000000000000The file from docker-compose.yaml podman-compose-1.2.0/tests/volumes_merge/override.txt000066400000000000000000000000531463674324000230350ustar00rootroot00000000000000The file from docker-compose.override.yaml podman-compose-1.2.0/tests/yamlmagic/000077500000000000000000000000001463674324000175515ustar00rootroot00000000000000podman-compose-1.2.0/tests/yamlmagic/docker-compose.yml000066400000000000000000000007771463674324000232210ustar00rootroot00000000000000version: '3.6' x-deploy-base: &deploy-base restart_policy: delay: 2s x-common: &common network: host deploy: <<: *deploy-base networks: hostnet: {} networks: hostnet: external: true name: host volumes: node-red_data: services: node-red: <<: *common image: busybox command: busybox httpd -h /data -f -p 8080 deploy: <<: *deploy-base resources: limits: cpus: '0.5' memory: 32M volumes: - node-red_data:/data