pax_global_header00006660000000000000000000000064147372765610014534gustar00rootroot0000000000000052 comment=9cbc4c1dcd4afcfef28e9efdb2287aa1bbb7cfe9 podman-compose-1.3.0/000077500000000000000000000000001473727656100144565ustar00rootroot00000000000000podman-compose-1.3.0/.codespellignore000066400000000000000000000000111473727656100176250ustar00rootroot00000000000000assertIn podman-compose-1.3.0/.codespellrc000066400000000000000000000002031473727656100167510ustar00rootroot00000000000000[codespell] skip = .git,*.pdf,*.svg,requirements.txt,test-requirements.txt # poped - loved variable name ignore-words-list = poped podman-compose-1.3.0/.coveragerc000066400000000000000000000000251473727656100165740ustar00rootroot00000000000000[run] parallel=True podman-compose-1.3.0/.editorconfig000066400000000000000000000004141473727656100171320ustar00rootroot00000000000000root = 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.3.0/.github/000077500000000000000000000000001473727656100160165ustar00rootroot00000000000000podman-compose-1.3.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001473727656100202015ustar00rootroot00000000000000podman-compose-1.3.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000024541473727656100227000ustar00rootroot00000000000000--- 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) **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.3.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341473727656100237250ustar00rootroot00000000000000--- 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.3.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000006101473727656100216140ustar00rootroot00000000000000 ## 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.3.0/.github/dependabot.yml000066400000000000000000000001661473727656100206510ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" podman-compose-1.3.0/.github/workflows/000077500000000000000000000000001473727656100200535ustar00rootroot00000000000000podman-compose-1.3.0/.github/workflows/codespell.yml000066400000000000000000000005631473727656100225540ustar00rootroot00000000000000--- 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.3.0/.github/workflows/release.yml000066400000000000000000000011541473727656100222170ustar00rootroot00000000000000name: Build and Release on: push: tags: - 'v*.*.*' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Build binary run: | mkdir -p release/ docker build -t podman-compose-bin -v "$PWD/release:/result" . mv "$PWD/release/podman-compose" "$PWD/release/podman-compose-linux-x86" - name: Upload release asset uses: softprops/action-gh-release@v2 with: files: ./release/podman-compose-linux-x86 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} podman-compose-1.3.0/.github/workflows/static-checks.yml000066400000000000000000000011731473727656100233250ustar00rootroot00000000000000name: 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.3.0/.github/workflows/test.yml000066400000000000000000000022341473727656100215560ustar00rootroot00000000000000name: Tests on: push: pull_request: jobs: test: strategy: fail-fast: false matrix: python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] 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-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y podman python -m pip install --upgrade pip pip install -r requirements.txt pip install -r test-requirements.txt - name: Run integration tests run: | python -m unittest discover -v tests/integration env: TESTS_DEBUG: 1 - name: Run unit tests run: | coverage run --source podman_compose -m unittest discover tests/unit - name: Report coverage run: | coverage combine coverage report --format=markdown | tee -a $GITHUB_STEP_SUMMARY podman-compose-1.3.0/.gitignore000066400000000000000000000023521473727656100164500ustar00rootroot00000000000000# 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.3.0/.pre-commit-config.yaml000066400000000000000000000020051473727656100207340ustar00rootroot00000000000000repos: - 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.3.0/.pylintrc000066400000000000000000000017501473727656100163260ustar00rootroot00000000000000[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.3.0/CODE-OF-CONDUCT.md000066400000000000000000000003151473727656100171100ustar00rootroot00000000000000## 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.3.0/CONTRIBUTING.md000066400000000000000000000103421473727656100167070ustar00rootroot00000000000000# 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. ## 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 discover tests/unit $ python3 -m unittest discover tests/integration $ 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` 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.3.0/Dockerfile000066400000000000000000000012531473727656100164510ustar00rootroot00000000000000# Use a base image with necessary build tools FROM python:3.11-slim AS builder # Install required packages for building RUN apt-get update && apt-get install -y \ gcc \ musl-dev \ build-essential \ python3-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Set the working directory WORKDIR /app # Copy the application code COPY . . # Install PyInstaller RUN pip install pyinstaller RUN pip install -r requirements.txt # Create a binary with PyInstaller RUN pyinstaller --onefile --clean podman_compose.py # Create /result dir in case it is not mounted RUN mkdir -p /result # Export binary RUN cp /app/dist/podman_compose /result/podman-compose podman-compose-1.3.0/LICENSE000066400000000000000000000432541473727656100154730ustar00rootroot00000000000000 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.3.0/README.md000066400000000000000000000113721473727656100157410ustar00rootroot00000000000000# 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 ``` ### Generate binary using docker/podman locally This script will download the repo, generate the binary using [this Dockerfile](https://github.com/containers/podman-compose/blob/main/Dockerfile), and place the binary in the directory where you called this script. ```bash sh -c "$(curl -sSL https://raw.githubusercontent.com/containers/podman-compose/main/scripts/download_and_build_podman-compose.sh)" ``` ### 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 python3 -m unittest discover tests/unit ``` # Contributing guide If you are a user or a developer and want to contribute please check the [CONTRIBUTING](CONTRIBUTING.md) section podman-compose-1.3.0/RELEASING.md000066400000000000000000000023771473727656100163220ustar00rootroot00000000000000Creating a release ================== This file contains instructions for maintainers on how to release new versions of podman-compose. Step 1: Initialize variables for subsequent steps ------------------------------------------------- ``` export VERSION=1.2.3 ``` Step 2: Release notes PR ------------------------ Open a new branch (e.g. `release`) and run the following: ``` ./scripts/make_release_notes.sh $VERSION ``` This collects the release notes using the `towncrier` tool and then commits the result. This step is done as a PR so that CI can check for spelling errors and similar issues. Certain file names are not properly supported by the `towncrier` tool and it ignores them. Check `newsfragments` directory for any forgotten release notes Step 3: Merge the release notes PR ---------------------------------- Step 4: Perform actual release ------------------------------ Pull the merge commit created on the `main` branch during the step 2. Then run: ``` ./scripts/make_release.sh ``` This will create release commit, tag and push everything. Step 5: Create a release on Github ---------------------------------- The release notes must be added manually by drafting a release on the GitHub UI at https://github.com/containers/podman-compose/releases. podman-compose-1.3.0/SECURITY.md000066400000000000000000000003751473727656100162540ustar00rootroot00000000000000## 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.3.0/completion/000077500000000000000000000000001473727656100166275ustar00rootroot00000000000000podman-compose-1.3.0/completion/bash/000077500000000000000000000000001473727656100175445ustar00rootroot00000000000000podman-compose-1.3.0/completion/bash/podman-compose000066400000000000000000000273601473727656100224200ustar00rootroot00000000000000# 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.3.0/docs/000077500000000000000000000000001473727656100154065ustar00rootroot00000000000000podman-compose-1.3.0/docs/Changelog-1.1.0.md000066400000000000000000000031321473727656100202510ustar00rootroot00000000000000Version 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.3.0/docs/Changelog-1.2.0.md000066400000000000000000000031131473727656100202510ustar00rootroot00000000000000Version 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.3.0/docs/Changelog-1.3.0.md000066400000000000000000000032771473727656100202650ustar00rootroot00000000000000Version 1.3.0 (2025-01-07) ========================== Bug fixes --------- - Fixed support for de-facto alternative `Dockerfile` names (e.g. `Containerfile`) - Fixed a bug that caused attempts to create already existing pods multiple times. - Fixed compatibility with docker-compose in how symlinks to docker-compose.yml are handled. - Fixed freeze caused by too long log lines without a newline. - Fixed support for `network_mode: none`. - Improved error detection by rejecting service definitions that contain both `network_mode` and `networks` keys, which is not allowed. Features -------- - Added support for build labels. - Added support for "platform" property in the build command. - Added support for "ssh" property in the build command. - Added support for cache_from and cache_to fields in build section. - Added support for honoring the condition in the depends_on section of the service, if stated. - Added `x-podman.no_hosts` setting to pass `--no-hosts` to podman run - Added support for compatibility with docker compose for default network behavior when no network defined in service. This is controlled via `default_net_behavior_compat` feature flag. - Added a way to get compatibility of default network names with docker compose. This is selected by setting `default_net_name_compat: true` on `x-podman` global dictionary. - Added support for the `device_cgroup_rules` property in services. - Added support for removing networks in `podman-compose down`. - Added support for network scoped service aliases. - Added support for network level `mac_address` attribute. - Added ability to substitute variables with the environment of the service. Misc ---- - Declared compatibility with Python 3.13. podman-compose-1.3.0/docs/Extensions.md000066400000000000000000000123241473727656100200710ustar00rootroot00000000000000# 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.uidmaps` - Run the container in a new user namespace using the supplied UID mapping. * `x-podman.gidmaps` - 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. * `x-podman.no_hosts` - Run the container without creating /etc/hosts file 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. Note that the [compose spec](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#mac_address) now supports `mac_address` on the network level, so we recommend using the standard `mac_address` key for setting the MAC address. The `x-podman.mac_address` is still supported for backwards compatibility. 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" mac_address: "02:bb:bb:bb:bb:bb" # mac_address is supported ``` ## 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. ## Compatibility of default network names between docker-compose and podman-compose Current versions of podman-compose may produce different default external network names than docker-compose under certain conditions. Specifically, docker-compose removes dashes (`-` character) from project name. To enable compatibility between docker-compose and podman-compose, specify `default_net_name_compat: true` under global `x-podman` key: ``` x-podman: default_net_name_compat: true ``` By default `default_net_name_compat` is `false`. This will change to `true` at some point and the setting will be removed. ## Compatibility of default network behavior between docker-compose and podman-compose When there is no network defined (neither network-mode nor networks) in service, The behavior of default network in docker-compose and podman-compose are different. | Top-level networks | podman-compose | docker-compose | | ------------------------------ | -------------------------- | -------------- | | No networks | default | default | | One network named net0 | net0 | default | | Two networks named net0, net1 | podman(`--network=bridge`) | default | | Contains network named default | default | default | To enable compatibility between docker-compose and podman-compose, specify `default_net_behavior_compat: true` under global `x-podman` key: ```yaml x-podman: default_net_behavior_compat: true ``` ## 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.3.0/docs/Mappings.md000066400000000000000000000011141473727656100175030ustar00rootroot00000000000000# 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.3.0/examples/000077500000000000000000000000001473727656100162745ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/000077500000000000000000000000001473727656100172435ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/README.md000066400000000000000000000023321473727656100205220ustar00rootroot00000000000000# 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.3.0/examples/awx17/roles/000077500000000000000000000000001473727656100203675ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/roles/local_docker/000077500000000000000000000000001473727656100230105ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/roles/local_docker/defaults/000077500000000000000000000000001473727656100246175ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/roles/local_docker/defaults/main.yml000066400000000000000000000003711473727656100262670ustar00rootroot00000000000000--- 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.3.0/examples/awx17/roles/local_docker/tasks/000077500000000000000000000000001473727656100241355ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/roles/local_docker/tasks/compose.yml000066400000000000000000000042751473727656100263350ustar00rootroot00000000000000--- - 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.3.0/examples/awx17/roles/local_docker/tasks/main.yml000066400000000000000000000006161473727656100256070ustar00rootroot00000000000000--- - 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.3.0/examples/awx17/roles/local_docker/tasks/set_image.yml000066400000000000000000000034761473727656100266270ustar00rootroot00000000000000--- - 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.3.0/examples/awx17/roles/local_docker/tasks/upgrade_postgres.yml000066400000000000000000000040741473727656100302420ustar00rootroot00000000000000--- - 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.3.0/examples/awx17/roles/local_docker/templates/000077500000000000000000000000001473727656100250065ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx17/roles/local_docker/templates/credentials.py.j2000066400000000000000000000006271473727656100301740ustar00rootroot00000000000000DATABASES = { '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.3.0/examples/awx17/roles/local_docker/templates/docker-compose.yml.j2000066400000000000000000000174541473727656100307700ustar00rootroot00000000000000#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.3.0/examples/awx17/roles/local_docker/templates/environment.sh.j2000066400000000000000000000006611473727656100302230ustar00rootroot00000000000000DATABASE_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.3.0/examples/awx17/roles/local_docker/templates/nginx.conf.j2000066400000000000000000000072731473727656100273230ustar00rootroot00000000000000#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.3.0/examples/awx17/roles/local_docker/templates/redis.conf.j2000066400000000000000000000001161473727656100272730ustar00rootroot00000000000000unixsocket /var/run/redis/redis.sock unixsocketperm 660 port 0 bind 127.0.0.1 podman-compose-1.3.0/examples/awx3/000077500000000000000000000000001473727656100171565ustar00rootroot00000000000000podman-compose-1.3.0/examples/awx3/docker-compose.yml000066400000000000000000000027131473727656100226160ustar00rootroot00000000000000version: '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.3.0/examples/azure-vote/000077500000000000000000000000001473727656100203755ustar00rootroot00000000000000podman-compose-1.3.0/examples/azure-vote/README.md000066400000000000000000000005261473727656100216570ustar00rootroot00000000000000# 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.3.0/examples/azure-vote/docker-compose.yaml000066400000000000000000000007041473727656100241740ustar00rootroot00000000000000--- # 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.3.0/examples/busybox/000077500000000000000000000000001473727656100177675ustar00rootroot00000000000000podman-compose-1.3.0/examples/busybox/docker-compose.yaml000066400000000000000000000013111473727656100235610ustar00rootroot00000000000000version: "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.3.0/examples/echo/000077500000000000000000000000001473727656100172125ustar00rootroot00000000000000podman-compose-1.3.0/examples/echo/README.md000066400000000000000000000007571473727656100205020ustar00rootroot00000000000000# 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.3.0/examples/echo/docker-compose.yaml000066400000000000000000000001671473727656100230140ustar00rootroot00000000000000--- version: '3' services: web: image: k8s.gcr.io/echoserver:1.4 ports: - "${HOST_PORT:-8080}:8080" podman-compose-1.3.0/examples/hello-app-redis/000077500000000000000000000000001473727656100212615ustar00rootroot00000000000000podman-compose-1.3.0/examples/hello-app-redis/README.md000066400000000000000000000005741473727656100225460ustar00rootroot00000000000000# 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.3.0/examples/hello-app-redis/docker-compose.yaml000066400000000000000000000037531473727656100250670ustar00rootroot00000000000000--- 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.3.0/examples/hello-app/000077500000000000000000000000001473727656100201555ustar00rootroot00000000000000podman-compose-1.3.0/examples/hello-app/README.md000066400000000000000000000002221473727656100214300ustar00rootroot00000000000000# 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.3.0/examples/hello-app/docker-compose.yaml000066400000000000000000000002011473727656100237440ustar00rootroot00000000000000--- version: '3' services: web: image: gcr.io/google-samples/hello-app:1.0 ports: - "${HOST_PORT:-8080}:8080" podman-compose-1.3.0/examples/hello-python/000077500000000000000000000000001473727656100207165ustar00rootroot00000000000000podman-compose-1.3.0/examples/hello-python/Dockerfile000066400000000000000000000002641473727656100227120ustar00rootroot00000000000000FROM 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.3.0/examples/hello-python/README.md000066400000000000000000000001721473727656100221750ustar00rootroot00000000000000# Simple Python Demo ## A Redis counter ``` podman-compose up -d curl localhost:8080/ curl localhost:8080/hello.json ``` podman-compose-1.3.0/examples/hello-python/app/000077500000000000000000000000001473727656100214765ustar00rootroot00000000000000podman-compose-1.3.0/examples/hello-python/app/__init__.py000066400000000000000000000000001473727656100235750ustar00rootroot00000000000000podman-compose-1.3.0/examples/hello-python/app/web.py000066400000000000000000000016331473727656100226300ustar00rootroot00000000000000# 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.3.0/examples/hello-python/docker-compose.yaml000066400000000000000000000005711473727656100245170ustar00rootroot00000000000000--- 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.3.0/examples/hello-python/requirements.txt000066400000000000000000000000451473727656100242010ustar00rootroot00000000000000aiohttp aioredis # aioredis[hiredis] podman-compose-1.3.0/examples/nodeproj/000077500000000000000000000000001473727656100201145ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/.eslintrc.json000066400000000000000000000030621473727656100227110ustar00rootroot00000000000000{ "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.3.0/examples/nodeproj/.gitignore000066400000000000000000000000431473727656100221010ustar00rootroot00000000000000local.env .env *.pid node_modules podman-compose-1.3.0/examples/nodeproj/.home/000077500000000000000000000000001473727656100211225ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/.home/.gitignore000066400000000000000000000000021473727656100231020ustar00rootroot00000000000000* podman-compose-1.3.0/examples/nodeproj/README.md000066400000000000000000000003321473727656100213710ustar00rootroot00000000000000# 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.3.0/examples/nodeproj/containers/000077500000000000000000000000001473727656100222615ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/containers/node16-runtime/000077500000000000000000000000001473727656100250365ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/containers/node16-runtime/Dockerfile000066400000000000000000000010641473727656100270310ustar00rootroot00000000000000FROM 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.3.0/examples/nodeproj/docker-compose.yml000066400000000000000000000017441473727656100235570ustar00rootroot00000000000000version: '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.3.0/examples/nodeproj/example.env000066400000000000000000000000601473727656100222550ustar00rootroot00000000000000WEB_LISTEN_PORT=3000 # pass UID= your IDE user podman-compose-1.3.0/examples/nodeproj/example.local.env000066400000000000000000000000221473727656100233440ustar00rootroot00000000000000REDIS_HOST=redis podman-compose-1.3.0/examples/nodeproj/index.js000066400000000000000000000001131473727656100215540ustar00rootroot00000000000000#! /usr/bin/env node "use strict"; import {start} from "./lib"; start(); podman-compose-1.3.0/examples/nodeproj/jsconfig.json000066400000000000000000000004021473727656100226050ustar00rootroot00000000000000{ "compilerOptions": { "target": "es2020", "module": "es2020", "moduleResolution": "node", "allowSyntheticDefaultImports": true }, "files": [ "index.js" ], "include": [ "lib/**/*.js" ] }podman-compose-1.3.0/examples/nodeproj/lib/000077500000000000000000000000001473727656100206625ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/lib/commands/000077500000000000000000000000001473727656100224635ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/lib/commands/task.js000066400000000000000000000013061473727656100237630ustar00rootroot00000000000000"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.3.0/examples/nodeproj/lib/commands/web.js000066400000000000000000000010261473727656100235750ustar00rootroot00000000000000"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.3.0/examples/nodeproj/package.json000066400000000000000000000007561473727656100224120ustar00rootroot00000000000000{ "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.3.0/examples/nodeproj/public/000077500000000000000000000000001473727656100213725ustar00rootroot00000000000000podman-compose-1.3.0/examples/nodeproj/public/index.html000066400000000000000000000006271473727656100233740ustar00rootroot00000000000000 Vote

This is a Heading

This is a paragraph.

podman-compose-1.3.0/examples/nvidia-smi/000077500000000000000000000000001473727656100203345ustar00rootroot00000000000000podman-compose-1.3.0/examples/nvidia-smi/docker-compose.yaml000066400000000000000000000003651473727656100241360ustar00rootroot00000000000000services: 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.3.0/examples/wordpress/000077500000000000000000000000001473727656100203245ustar00rootroot00000000000000podman-compose-1.3.0/examples/wordpress/docker-compose.yaml000066400000000000000000000011301473727656100241150ustar00rootroot00000000000000--- 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.3.0/newsfragments/000077500000000000000000000000001473727656100173415ustar00rootroot00000000000000podman-compose-1.3.0/newsfragments/README.txt000066400000000000000000000011101473727656100210300ustar00rootroot00000000000000This 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.3.0/podman_compose.py000077500000000000000000004010551473727656100200430ustar00rootroot00000000000000#!/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 from __future__ import annotations import argparse import asyncio.exceptions 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 from enum import Enum 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.3.0" script = os.path.realpath(sys.argv[0]) # helper functions def is_list(list_object): return ( not isinstance(list_object, str) and not isinstance(list_object, dict) 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, 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") vol = (vols.get(source, {}) or {}) if source else {} name = vol.get("name") mount_dict["_vol"] = vol # handle anonymous or implied volume if not source: # missing source vol["name"] = "_".join([ compose.project_name, srv_name, hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(), ]) elif not name: external = vol.get("external") if isinstance(external, dict): vol["name"] = external.get("name", f"{source}") elif external: vol["name"] = f"{source}" else: vol["name"] = f"{compose.project_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 isinstance(value, dict): if 'environment' in value and isinstance(value['environment'], dict): # Load service's environment variables subs_dict = subs_dict.copy() svc_envs = {k: v for k, v in value['environment'].items() if k not in subs_dict} # we need to add `svc_envs` to the `subs_dict` so that it can evaluate the # service environment that reference to another service environment. subs_dict.update(svc_envs) svc_envs = rec_subs(svc_envs, subs_dict) subs_dict.update(svc_envs) value = {k: rec_subs(v, subs_dict) for k, v in value.items()} elif isinstance(value, str): 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"] """ dst: list[str] if src is None: dst = [] elif isinstance(src, dict): 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 isinstance(src, dict): 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 isinstance(src, str): 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 isinstance(inner_value, dict): 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")) hard = inner_value.get("hard", inner_value.get("soft")) 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 default_network_name_for_project(compose, net, is_ext): if is_ext: return net default_net_name_compat = compose.x_podman.get("default_net_name_compat", False) if default_net_name_compat is True: return f"{compose.project_name.replace('-', '')}_{net}" return f"{compose.project_name}_{net}" # 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") 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"): return vol_name = vol["name"] is_ext = vol.get("external") 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", []) args = [ "create", "--label", f"io.podman.compose.project={compose.project_name}", "--label", f"com.docker.compose.project={compose.project_name}", ] for item in norm_as_list(labels): args.extend(["--label", item]) driver = vol.get("driver") if driver: args.extend(["--driver", driver]) driver_opts = vol.get("driver_opts", {}) 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") vol = mount_desc.get("_vol") if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source") 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") 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") if tmpfs_size: opts.append(f"tmpfs-size={tmpfs_size}") tmpfs_mode = tmpfs_opts.get("mode") 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") 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 isinstance(ulimit, str): 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") 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") if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source") 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") 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") 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): srv_name = cnt["_service"] basedir = compose.dirname if isinstance(volume, str): volume = parse_short_mount(volume, basedir) return fix_mount_dict(compose, volume, srv_name) async def get_mount_args(compose, cnt, volume): volume = get_mnt_dict(compose, cnt, volume) 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") if size: opts.append(f"size={size}") mode = tmpfs_opts.get("mode") 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 isinstance(secret, str) else secret.get("source") 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") dest_file = "" secret_opts = "" secret_target = None secret_uid = None secret_gid = None secret_mode = None secret_type = None if isinstance(secret, dict): secret_target = secret.get("target") secret_uid = secret.get("uid") secret_gid = secret.get("gid") secret_mode = secret.get("mode") secret_type = secret.get("type") 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"): 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") 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", {}) res = deploy.get("resources", {}) reservations = res.get("reservations", {}) devices = reservations.get("devices", []) gpu_on = False for device in devices: driver = device.get("driver") if driver is None: continue capabilities = device.get("capabilities") 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) cpu_shares_v2 = try_int(cnt.get("cpu_shares"), None) mem_limit_v2 = cnt.get("mem_limit") mem_res_v2 = cnt.get("mem_reservation") # 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", {}) res = deploy.get("resources", {}) limits = res.get("limits", {}) cpus_limit_v3 = try_float(limits.get("cpus"), None) mem_limit_v3 = limits.get("memory") reservations = res.get("reservations", {}) # cpus_res_v3 = try_float(reservations.get('cpus', None), None) mem_res_v3 = reservations.get("memory") # 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") published = port_desc.get("published", "") host_ip = port_desc.get("host_ip") protocol = port_desc.get("protocol", "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", []) for item in norm_as_list(labels): args.extend(["--label", item]) if net_desc.get("internal"): args.append("--internal") driver = net_desc.get("driver") if driver: args.extend(("--driver", driver)) driver_opts = net_desc.get("driver_opts", {}) for key, value in driver_opts.items(): args.extend(("--opt", f"{key}={value}")) ipam = net_desc.get("ipam", {}) ipam_driver = ipam.get("driver") if ipam_driver and ipam_driver != "default": args.extend(("--ipam-driver", ipam_driver)) ipam_config_ls = ipam.get("config", []) if net_desc.get("enable_ipv6"): args.append("--ipv6") if isinstance(ipam_config_ls, dict): ipam_config_ls = [ipam_config_ls] for ipam_config in ipam_config_ls: subnet = ipam_config.get("subnet") ip_range = ipam_config.get("ip_range") gateway = ipam_config.get("gateway") 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") if net: return cnt_nets = cnt.get("networks") if cnt_nets and isinstance(cnt_nets, dict): cnt_nets = list(cnt_nets.keys()) cnt_nets = norm_as_list(cnt_nets or compose.default_net) for net in cnt_nets: net_desc = compose.networks[net] or {} is_ext = net_desc.get("external") ext_desc = is_ext if isinstance(is_ext, dict) else {} default_net_name = default_network_name_for_project(compose, net, is_ext) net_name = ext_desc.get("name") or net_desc.get("name") 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, compose.project_name, net_name) await compose.podman.output([], "network", args) await compose.podman.output([], "network", ["exists", net_name]) def get_net_args_from_network_mode(compose, cnt): net_args = [] net = cnt.get("network_mode") service_name = cnt["service_name"] if "networks" in cnt: raise ValueError( f"networks and network_mode must not be present in the same service [{service_name}]" ) if net == "none": net_args.append("--network=none") 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"): aliases_on_container = [service_name] if cnt.get("_aliases"): aliases_on_container.extend(cnt.get("_aliases")) net_options = [f"alias={alias}" for alias in aliases_on_container] mac_address = cnt.get("mac_address") if mac_address: net_options.append(f"mac={mac_address}") net = f"{net}," if ":" in net else f"{net}:" net_args.append(f"--network={net}{','.join(net_options)}") else: log.fatal("unknown network_mode [%s]", net) sys.exit(1) return net_args def get_net_args(compose, cnt): net = cnt.get("network_mode") if net: return get_net_args_from_network_mode(compose, cnt) return get_net_args_from_networks(compose, cnt) def get_net_args_from_networks(compose, cnt): net_args = [] mac_address = cnt.get("mac_address") service_name = cnt["service_name"] aliases_on_container = [service_name] aliases_on_container.extend(cnt.get("_aliases", [])) multiple_nets = cnt.get("networks", {}) if not multiple_nets: if not compose.default_net: # The bridge mode in podman is using the `podman` network. # It seems weird, but we should keep this behavior to avoid # breaking changes. net_options = [f"alias={alias}" for alias in aliases_on_container] if mac_address: net_options.append(f"mac={mac_address}") net_args.append(f"--network=bridge:{','.join(net_options)}") return net_args multiple_nets = {compose.default_net: {}} # 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("mac_address", net_config.get("x-podman.mac_address")) 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 = compose.networks.get(net_) or {} is_ext = net_desc.get("external") ext_desc = is_ext if isinstance(is_ext, str) else {} default_net_name = default_network_name_for_project(compose, net_, is_ext) net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name ipv4 = net_config_.get("ipv4_address") ipv6 = net_config_.get("ipv6_address") # Note: mac_address is supported by compose spec now, and x-podman.mac_address # is only for backward compatibility # https://github.com/compose-spec/compose-spec/blob/main/05-services.md#mac_address mac = net_config_.get("mac_address", net_config_.get("x-podman.mac_address")) aliases_on_net = norm_as_list(net_config_.get("aliases", [])) # 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"ip6={ipv6}") if mac: net_options.append(f"mac={mac}") # Container level service aliases net_options.extend([f"alias={alias}" for alias in aliases_on_container]) # network level service aliases if aliases_on_net: net_options.extend([f"alias={alias}" for alias in aliases_on_net]) if net_options: net_args.append(f"--network={net_name}:" + ",".join(net_options)) else: net_args.append(f"--network={net_name}") 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", "") 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", []): deps.extend(compose.container_names_by_service.get(dep_srv.name, [])) if deps: deps_csv = ",".join(deps) podman_args.append(f"--requires={deps_csv}") sec = norm_as_list(cnt.get("security_opt")) for sec_item in sec: podman_args.extend(["--security-opt", sec_item]) ann = norm_as_list(cnt.get("annotations")) for a in ann: podman_args.extend(["--annotation", a]) if cnt.get("read_only"): podman_args.append("--read-only") if cnt.get("http_proxy") 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 cnt.get("device_cgroup_rules", []): podman_args.extend(["--device-cgroup-rule", item]) for item in norm_as_list(cnt.get("dns")): podman_args.extend(["--dns", item]) for item in norm_as_list(cnt.get("dns_opt")): podman_args.extend(["--dns-opt", item]) for item in norm_as_list(cnt.get("dns_search")): podman_args.extend(["--dns-search", item]) env_file = cnt.get("env_file", []) if isinstance(env_file, (dict, str)): env_file = [env_file] for i in env_file: if isinstance(i, str): 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 isinstance(tmpfs_ls, str): 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") if log_config is not None: podman_args.append(f'--log-driver={log_config.get("driver", "k8s-file")}') log_opts = log_config.get("options", {}) 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"): podman_args.append("-P") ports = cnt.get("ports", []) 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") if userns_mode is not None: podman_args.extend(["--userns", userns_mode]) user = cnt.get("user") if user is not None: podman_args.extend(["-u", user]) if cnt.get("working_dir") is not None: podman_args.extend(["-w", cnt["working_dir"]]) if cnt.get("hostname"): podman_args.extend(["--hostname", cnt["hostname"]]) if cnt.get("shm_size"): podman_args.extend(["--shm-size", str(cnt["shm_size"])]) if cnt.get("stdin_open"): podman_args.append("-i") if cnt.get("stop_signal"): 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"): podman_args.append("--tty") if cnt.get("privileged"): podman_args.append("--privileged") if cnt.get("pid"): podman_args.extend(["--pid", cnt["pid"]]) pull_policy = cnt.get("pull_policy") if pull_policy is not None and pull_policy != "build": podman_args.extend(["--pull", pull_policy]) if cnt.get("restart") 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"): podman_args.append("--init") if cnt.get("init-path"): podman_args.extend(["--init-path", cnt["init-path"]]) entrypoint = cnt.get("entrypoint") if entrypoint is not None: if isinstance(entrypoint, str): entrypoint = shlex.split(entrypoint) podman_args.extend(["--entrypoint", json.dumps(entrypoint)]) platform = cnt.get("platform") if platform is not None: podman_args.extend(["--platform", platform]) if cnt.get("runtime"): podman_args.extend(["--runtime", cnt["runtime"]]) # WIP: healthchecks are still work in progress healthcheck = cnt.get("healthcheck", {}) if not isinstance(healthcheck, dict): raise ValueError("'healthcheck' must be a key-value mapping") healthcheck_disable = healthcheck.get("disable", False) healthcheck_test = healthcheck.get("test") if healthcheck_disable: healthcheck_test = ["NONE"] if healthcheck_test: # If it's a string, it's equivalent to specifying CMD-SHELL if isinstance(healthcheck_test, str): # 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.uidmaps and ' 'x-podman.gidmaps 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]) if cnt.get("x-podman.no_hosts", False): podman_args.extend(["--no-hosts"]) rootfs = cnt.get('x-podman.rootfs') 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") if command is not None: if isinstance(command, str): podman_args.extend(shlex.split(command)) else: podman_args.extend([str(i) for i in command]) return podman_args class ServiceDependencyCondition(Enum): CONFIGURED = "configured" CREATED = "created" EXITED = "exited" HEALTHY = "healthy" INITIALIZED = "initialized" PAUSED = "paused" REMOVING = "removing" RUNNING = "running" STOPPED = "stopped" STOPPING = "stopping" UNHEALTHY = "unhealthy" @classmethod def from_value(cls, value): # Check if the value exists in the enum for member in cls: if member.value == value: return member # Check if this is a value coming from reference docker_to_podman_cond = { "service_healthy": ServiceDependencyCondition.HEALTHY, "service_started": ServiceDependencyCondition.RUNNING, "service_completed_successfully": ServiceDependencyCondition.STOPPED, } try: return docker_to_podman_cond[value] except KeyError: raise ValueError(f"Value '{value}' is not a valid condition for a service dependency") # pylint: disable=raise-missing-from class ServiceDependency: def __init__(self, name, condition): self._name = name self._condition = ServiceDependencyCondition.from_value(condition) @property def name(self): return self._name @property def condition(self): return self._condition def __hash__(self): # Compute hash based on the frozenset of items to ensure order does not matter return hash(('name', self._name) + ('condition', self._condition)) def __eq__(self, other): # Compare equality based on dictionary content if isinstance(other, ServiceDependency): return self._name == other.name and self._condition == other.condition return False 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.name == service_name: continue dep_srv = services.get(dep_name.name) if not dep_srv: continue # NOTE: avoid creating loops, A->B->A if any(start_point == x.name for x in dep_srv["_deps"]): continue new_deps = rec_deps(services, dep_name.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(): # parse dependencies for each service deps = set() srv["_deps"] = deps # TODO: manage properly the dependencies coming from base services when extended if with_extends: ext = srv.get("extends", {}).get("service") if ext: if ext != name: deps.add(ServiceDependency(ext, "service_started")) continue # the compose file has been normalized. depends_on, if exists, can only be a dictionary # the normalization adds a "service_started" condition by default deps_ls = srv.get("depends_on", {}) deps_ls = [ServiceDependency(k, v["condition"]) for k, v in deps_ls.items()] deps.update(deps_ls) # parse link to get service name and remove alias links_ls = srv.get("links", []) if not is_list(links_ls): links_ls = [links_ls] deps.update([ServiceDependency(c.split(":")[0], "service_started") 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) # expand the dependencies on each service 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) async def _readchunk(self, reader): try: return await reader.readuntil(b"\n") except asyncio.exceptions.IncompleteReadError as e: return e.partial except asyncio.exceptions.LimitOverrunError as e: return await reader.read(e.consumed) async def _format_stream(self, reader, sink, log_formatter): line_ongoing = False def _formatted_print_with_nl(s): if line_ongoing: print(s, file=sink, end="\n") else: print(log_formatter, s, file=sink, end="\n") def _formatted_print_without_nl(s): if line_ongoing: print(s, file=sink, end="") else: print(log_formatter, s, file=sink, end="") while not reader.at_eof(): chunk = await self._readchunk(reader) parts = chunk.split(b"\n") # Iff parts ends with '', the last part is a incomplete line; # The rest are complete lines for i, part in enumerate(parts): if i < len(parts) - 1: _formatted_print_with_nl(part.decode()) line_ongoing = False elif len(part) > 0: _formatted_print_without_nl(part.decode()) line_ongoing = True if line_ongoing: print(file=sink, end="\n") # End the unfinished line 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: 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( self._format_stream(p.stdout, sys.stdout, log_formatter) ) task_reference.add(out_t) out_t.add_done_callback(task_reference.discard) err_t = asyncio.create_task( self._format_stream(p.stderr, sys.stdout, log_formatter) ) 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 network_ls(self): output = ( await self.output( [], "network", [ "ls", "--noheading", "--filter", f"label=io.podman.compose.project={self.compose.project_name}", "--format", "{{.Name}}", ], ) ).decode() networks = output.splitlines() return networks async def volume_ls(self): output = ( await self.output( [], "volume", [ "ls", "--noheading", "--filter", f"label=io.podman.compose.project={self.compose.project_name}", "--format", "{{.Name}}", ], ) ).decode("utf-8") volumes = output.splitlines() return volumes def normalize_service(service, sub_dir=""): if "build" in service: build = service["build"] if isinstance(build, str): service["build"] = {"context": build} if sub_dir and "build" in service: build = service["build"] context = build.get("context", "") 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 isinstance(build["additional_contexts"], dict): 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 isinstance(service[key], str): service[key] = shlex.split(service[key]) for key in ("env_file", "security_opt", "volumes"): if key not in service: continue if isinstance(service[key], str): 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 isinstance(extends, str): extends = {"service": extends} service["extends"] = extends if "depends_on" in service: # deps should become a dictionary of dependencies deps = service["depends_on"] if isinstance(deps, str): deps = {deps: {}} elif is_list(deps): deps = {x: {} for x in deps} # the dependency service_started is set by default # unless requested otherwise. for k, v in deps.items(): v.setdefault('condition', 'service_started') service["depends_on"] = deps return service def normalize(compose): """ convert compose dict of some keys from string or dicts into arrays """ services = compose.get("services", {}) 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 isinstance(build, str) else build.get("context", ".") context = os.path.normpath(os.path.join(project_dir, context)) if not isinstance(service["build"], dict): service["build"] = {} service["build"]["context"] = context return service def normalize_final(compose: dict, project_dir: str) -> dict: services = compose.get("services", {}) for service in services.values(): normalize_service_final(service, project_dir) return compose def clone(value): return value.copy() if is_list(value) or isinstance(value, dict) 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 isinstance(value2, dict): 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 isinstance(ext, str): ext = {"service": ext} from_service_name = ext.get("service") if not from_service_name: continue filename = ext.get("file") 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: Podman self.podman_version = None self.environ = {} self.exit_code = None self.commands = {} self.global_args = argparse.Namespace() self.project_name = None self.dirname = None self.pods = None self.containers = [] 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.x_podman = {} 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 isinstance(services, str): 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", []) for args in cmd_args: xargs.extend(shlex.split(args)) return xargs async def run(self, argv=None): log.info("podman-compose version: %s", __version__) args = self._parse_args(argv) 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): if self.global_args.in_pod_bool is None: self.global_args.in_pod_bool = self.x_podman.get("in_pod", 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") if dirname and os.path.isdir(dirname): os.chdir(dirname) pathsep = os.environ.get("COMPOSE_PATH_SEPARATOR", os.pathsep) if not args.file: default_str = os.environ.get("COMPOSE_FILE") 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 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, }) if args and 'env' in args and args.env: env_vars = norm_as_dict(args.env) self.environ.update(env_vars) 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") 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') if not project_name: project_name = compose.get("name") 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", 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") 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", {}) if not nets: nets["default"] = None self.networks = nets if compose.get("x-podman", {}).get("default_net_behavior_compat", False): # If there is no network_mode and networks in service, # docker-compose will create default network named '_default' # and add the service to the default network. # So we always set `default_net = 'default'` for compatibility if "default" not in self.networks: self.networks["default"] = None else: 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 allnets = set() for name, srv in services.items(): srv_nets = srv.get("networks", self.default_net) srv_nets = ( list(srv_nets.keys()) if isinstance(srv_nets, dict) 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"), fallback=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") rootfs_mode = x_podman is not None and x_podman.get("rootfs") 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")) cnt["ports"] = norm_ports(cnt.get("ports")) 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", []) for volume in volumes: mnt_dict = get_mnt_dict(self, cnt, volume) if ( mnt_dict.get("type") == "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", []))) # log("sorted:", [c["name"] for c in given_containers]) self.x_podman = compose.get("x-podman", {}) args.in_pod_bool = self.resolve_in_pod() 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, argv=None): 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(argv) 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"]]) def container_to_build_args(compose, cnt, args, path_exists): 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") 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 path_exists(dockerfile): break if not path_exists(dockerfile): raise OSError("Dockerfile not found in " + ctx) build_args = ["-f", dockerfile, "-t", cnt["image"]] if "platform" in cnt: build_args.extend(["--platform", cnt["platform"]]) 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]) labels = build_desc.get("labels", []) if isinstance(labels, dict): labels = [f"{k}={v}" for (k, v) in labels.items()] for label in labels: build_args.extend(["--label", label]) 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"]]) for agent_or_key in norm_as_list(build_desc.get("ssh", {})): build_args.extend(["--ssh", agent_or_key]) 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") 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, )) for cache_img in build_desc.get("cache_from", []): build_args.extend(["--cache-from", cache_img]) for cache_img in build_desc.get("cache_to", []): build_args.extend(["--cache-to", cache_img]) build_args.append(ctx) return build_args 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_args = container_to_build_args(compose, cnt, args, os.path.exists) 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 pod_exists(compose, name): exit_code = await compose.podman.run([], "pod", ["exists", name]) return exit_code == 0 async def create_pods(compose, args): # pylint: disable=unused-argument for pod in compose.pods: if await pod_exists(compose, pod["name"]): continue 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", []) 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 -= set(x.name for x in compose.services[service]["_deps"]) excluded.discard(service) log.debug("** excluding: %s", excluded) return excluded async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None: """Enforce that all specified conditions in deps are met""" if not deps: return for condition in ServiceDependencyCondition: deps_cd = [] for d in deps: if d.condition == condition: deps_cd.extend(compose.container_names_by_service[d.name]) if deps_cd: # podman wait will return always with a rc -1. while True: try: await compose.podman.output( [], "wait", [f"--condition={condition.value}"] + deps_cd ) log.debug( "dependencies for condition %s have been fulfilled on containers %s", condition.value, ', '.join(deps_cd), ) break except subprocess.CalledProcessError as _exc: output = list( ((_exc.stdout or b"") + (_exc.stderr or b"")).decode().split('\n') ) log.debug( 'Podman wait returned an error (%d) when executing "%s": %s', _exc.returncode, _exc.cmd, output, ) await asyncio.sleep(1) async def run_container( compose: PodmanCompose, name: str, deps: set, command: tuple, log_formatter: str = None ): """runs a container after waiting for its dependencies to be fulfilled""" # wait for the dependencies to be fulfilled if "start" in command: log.debug("Checking dependencies prior to container %s start", name) await check_dep_conditions(compose, deps) # start the container log.debug("Starting task for container %s", name) return await compose.podman.run(*command, log_formatter=log_formatter) @cmd_run(podman_compose, "up", "Create and start the entire stack or some of its services") async def compose_up(compose: PodmanCompose, args): 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={compose.project_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 run_container(compose, cnt["name"], cnt["_deps"], ([], "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") 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( run_container( compose, cnt["name"], cnt["_deps"], ([], "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): basedir = compose.dirname srv_name = cnt["_service"] ls = [] for volume in cnt.get("volumes", []): if isinstance(volume, str): volume = parse_short_mount(volume, basedir) volume = fix_mount_dict(compose, volume, srv_name) mount_type = volume["type"] if mount_type != "volume": continue volume_name = volume.get("_vol", {}).get("name") ls.append(volume_name) return ls @cmd_run(podman_compose, "down", "tear down entire stack") async def compose_down(compose: PodmanCompose, 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", 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"]]) for network in await compose.podman.network_ls(): await compose.podman.run([], "network", ["rm", network]) @cmd_run(podman_compose, "ps", "show status of containers") async def compose_ps(compose, args): ps_args = ["-a", "--filter", f"label=io.podman.compose.project={compose.project_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=[x.name for x in 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", [])) 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", 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.3.0/pyproject.toml000066400000000000000000000023311473727656100173710ustar00rootroot00000000000000[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" [tool.towncrier] package = "podman_compose" package_dir = "master" directory = "newsfragments" filename = "docs/Changelog-new.md" template = "scripts/Changelog-template.jinja" title_format = "Version {version} ({project_date})" [[tool.towncrier.section]] path = "" [[tool.towncrier.type]] directory = "feature" name = "Features" showcontent = true [[tool.towncrier.type]] directory = "change" name = "Changes" showcontent = true [[tool.towncrier.type]] directory = "bugfix" name = "Bug fixes" showcontent = true [[tool.towncrier.type]] directory = "doc" name = "Improved Documentation" showcontent = true [[tool.towncrier.type]] directory = "removal" name = "Deprecations and Removals" showcontent = true [[tool.towncrier.type]] directory = "misc" name = "Misc" showcontent = true podman-compose-1.3.0/requirements.txt000066400000000000000000000003511473727656100177410ustar00rootroot00000000000000# 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.3.0/scripts/000077500000000000000000000000001473727656100161455ustar00rootroot00000000000000podman-compose-1.3.0/scripts/Changelog-template.jinja000066400000000000000000000012551473727656100226650ustar00rootroot00000000000000{% for section, _ in sections|dictsort(by='key') %} {% set underline = "-" %} {% if section %} {{section}} {{ underline * section|length }}{% set underline = "~" %} {% endif %} {% if sections[section] %} {% for category, val in definitions|dictsort if category in sections[section]%} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% for text, values in sections[section][category]|dictsort(by='value') %} - {{ text }} {% endfor %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} (venv) p12@exec-desktop:~/cod podman-compose-1.3.0/scripts/clean_up.sh000077500000000000000000000003641473727656100202750ustar00rootroot00000000000000#!/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.3.0/scripts/download_and_build_podman-compose.sh000066400000000000000000000005541473727656100253160ustar00rootroot00000000000000#!/bin/sh # Delete repository dir rm -rf podman-compose-src # Clone repository git clone https://github.com/containers/podman-compose podman-compose-src # Generate binary sh podman-compose-src/scripts/generate_binary_using_dockerfile.sh # Move binary outside repo's dir mv podman-compose-src/podman-compose . # Delete repository dir rm -rf podman-compose-src podman-compose-1.3.0/scripts/generate_binary_using_dockerfile.sh000066400000000000000000000034601473727656100252360ustar00rootroot00000000000000#!/bin/sh # Find an available container tool (docker or podman) find_container_tool() { if command -v docker > /dev/null 2>&1; then echo "sudo docker" elif command -v podman > /dev/null 2>&1; then echo "podman" else echo "Error: Neither docker nor podman is available." >&2 exit 1 fi } # Determine which container tool to use CONTAINER_TOOL=$(find_container_tool) # Locate the directory containing dockerfile (root) PROJECT_ROOT_DIR="$(cd "$(dirname "$0")" && pwd)/.." # Check SELinux status and set appropriate mount option check_selinux() { if command -v getenforce > /dev/null 2>&1; then SELINUX_STATUS=$(getenforce) if [ "$SELINUX_STATUS" = "Enforcing" ] || [ "$SELINUX_STATUS" = "Permissive" ]; then echo ":z" else echo "" fi elif [ -f /sys/fs/selinux/enforce ]; then if [ "$(cat /sys/fs/selinux/enforce)" = "1" ]; then echo ":z" else echo "" fi else echo "" fi } # Get the SELinux option for volume mounts if SELinux is enforcing or permissive SELINUX=$(check_selinux) # Build binary $CONTAINER_TOOL image rm build-podman-compose if expr "$CONTAINER_TOOL" : '.*docker.*' >/dev/null; then $CONTAINER_TOOL build -t build-podman-compose "$PROJECT_ROOT_DIR" $CONTAINER_TOOL run --name build-podman-compose build-podman-compose $CONTAINER_TOOL cp build-podman-compose:/result/podman-compose "$PROJECT_ROOT_DIR/podman-compose" $CONTAINER_TOOL container stop build-podman-compose $CONTAINER_TOOL container rm -f build-podman-compose else $CONTAINER_TOOL build -v "$PROJECT_ROOT_DIR:/result$SELINUX" -t build-podman-compose "$PROJECT_ROOT_DIR" fi $CONTAINER_TOOL image rm python:3.11-slim $CONTAINER_TOOL image rm build-podman-compose podman-compose-1.3.0/scripts/make_release.sh000077500000000000000000000005431473727656100211230ustar00rootroot00000000000000#!/bin/bash set -e if [ $# -ne 1 ]; then echo "Usage: make_release.sh VERSION" exit 1 fi VERSION=$1 sed "s/__version__ = .*/__version__ = \"$VERSION\"/g" -i podman_compose.py git add podman_compose.py git commit -m "Release $VERSION" git tag "v$VERSION" -m "v$VERSION" -s git push ssh://github.com/containers/podman-compose main "v$VERSION" podman-compose-1.3.0/scripts/make_release_notes.sh000077500000000000000000000004401473727656100223270ustar00rootroot00000000000000#!/bin/bash set -e if [ $# -ne 1 ]; then echo "Usage: make_release_notes.sh VERSION" exit 1 fi VERSION=$1 towncrier build --version "$VERSION" --yes git mv "docs/Changelog-new.md" "docs/Changelog-$VERSION.md" git add "newsfragments/" git commit -m "Release notes for $VERSION" podman-compose-1.3.0/scripts/make_release_upload.sh000077500000000000000000000002221473727656100224610ustar00rootroot00000000000000#!/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.3.0/scripts/uninstall.sh000077500000000000000000000001131473727656100205100ustar00rootroot00000000000000#!/usr/bin/env bash pip3 uninstall podman-compose -y ./scripts/clean_up.sh podman-compose-1.3.0/setup.cfg000066400000000000000000000002261473727656100162770ustar00rootroot00000000000000[bdist_wheel] universal = 1 [metadata] version = attr: podman_compose.__version__ [flake8] # The GitHub editor is 127 chars wide max-line-length=127podman-compose-1.3.0/setup.py000066400000000000000000000031541473727656100161730ustar00rootroot00000000000000# 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.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "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.3.0/test-requirements.txt000066400000000000000000000013221473727656100207150ustar00rootroot00000000000000-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 requests tomlkit==0.12.4 virtualenv==20.25.1 podman-compose-1.3.0/tests/000077500000000000000000000000001473727656100156205ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/000077500000000000000000000000001473727656100201435ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/__init__.py000066400000000000000000000004011473727656100222470ustar00rootroot00000000000000import 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.3.0/tests/integration/additional_contexts/000077500000000000000000000000001473727656100242025ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/additional_contexts/README.md000066400000000000000000000003121473727656100254550ustar00rootroot00000000000000# 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.3.0/tests/integration/additional_contexts/data_for_dict/000077500000000000000000000000001473727656100267645ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/additional_contexts/data_for_dict/data.txt000066400000000000000000000000161473727656100304330ustar00rootroot00000000000000Data for dict podman-compose-1.3.0/tests/integration/additional_contexts/data_for_list/000077500000000000000000000000001473727656100270145ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/additional_contexts/data_for_list/data.txt000066400000000000000000000000161473727656100304630ustar00rootroot00000000000000Data for list podman-compose-1.3.0/tests/integration/additional_contexts/project/000077500000000000000000000000001473727656100256505ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/additional_contexts/project/Dockerfile000066400000000000000000000001371473727656100276430ustar00rootroot00000000000000FROM busybox COPY --from=data data.txt /data/data.txt CMD ["busybox", "cat", "/data/data.txt"] podman-compose-1.3.0/tests/integration/additional_contexts/project/docker-compose.yml000066400000000000000000000003261473727656100313060ustar00rootroot00000000000000version: "3.7" services: dict: build: context: . additional_contexts: data: ../data_for_dict list: build: context: . additional_contexts: - data=../data_for_list podman-compose-1.3.0/tests/integration/base_image/000077500000000000000000000000001473727656100222175ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/base_image/Dockerfile000066400000000000000000000002201473727656100242030ustar00rootroot00000000000000FROM docker.io/library/debian:bookworm-slim RUN apt-get update \ && apt-get install -y \ dumb-init \ busybox \ wget podman-compose-1.3.0/tests/integration/build/000077500000000000000000000000001473727656100212425ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build/context/000077500000000000000000000000001473727656100227265ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build/context/Dockerfile000066400000000000000000000002271473727656100247210ustar00rootroot00000000000000FROM 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.3.0/tests/integration/build/context/Dockerfile-alt000066400000000000000000000004661473727656100255040ustar00rootroot00000000000000FROM 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.3.0/tests/integration/build/docker-compose.yml000066400000000000000000000010131473727656100246720ustar00rootroot00000000000000version: "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.3.0/tests/integration/build_fail/000077500000000000000000000000001473727656100222355ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_fail/context/000077500000000000000000000000001473727656100237215ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_fail/context/Dockerfile000066400000000000000000000000701473727656100257100ustar00rootroot00000000000000FROM busybox RUN this_command_does_not_exist CMD ["sh"] podman-compose-1.3.0/tests/integration/build_fail/docker-compose.yml000066400000000000000000000001241473727656100256670ustar00rootroot00000000000000version: "3" services: test: build: ./context image: build-fail-img podman-compose-1.3.0/tests/integration/build_labels/000077500000000000000000000000001473727656100225645ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_labels/context/000077500000000000000000000000001473727656100242505ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_labels/context/Dockerfile000066400000000000000000000000151473727656100262360ustar00rootroot00000000000000FROM busybox podman-compose-1.3.0/tests/integration/build_labels/docker-compose.yml000066400000000000000000000011711473727656100262210ustar00rootroot00000000000000version: "3" services: test_build_labels_map: build: context: ./context dockerfile: Dockerfile labels: com.example.description: "Accounting webapp" com.example.department: "Finance" com.example.label-with-empty-value: "" image: my-busybox-build-labels-map command: env test_build_labels_array: build: context: ./context dockerfile: Dockerfile labels: - "com.example.description=Accounting webapp" - "com.example.department=Finance" - "com.example.label-with-empty-value" image: my-busybox-build-labels-array command: env podman-compose-1.3.0/tests/integration/build_labels/test_build_labels.py000066400000000000000000000037571473727656100266320ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import json import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin class TestBuildLabels(unittest.TestCase, RunSubprocessMixin): def test_build_labels(self): """The build context can contain labels which should be added to the resulting image. They can be either an array or a map. """ compose_path = os.path.join(test_path(), "build_labels/docker-compose.yml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "build", "test_build_labels_map", "test_build_labels_array", ]) expected_labels = { "com.example.department": "Finance", "com.example.description": "Accounting webapp", "com.example.label-with-empty-value": "", } out, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "my-busybox-build-labels-map", "my-busybox-build-labels-array", ]) images = json.loads(out) self.assertEqual(len(images), 2) labels_map = images[0].get("Config", {}).get("Labels", {}) labels_array = images[1].get("Config", {}).get("Labels", {}) for k, v in expected_labels.items(): self.assertIn(k, labels_map) self.assertEqual(labels_map[k], v) self.assertIn(k, labels_array) self.assertEqual(labels_array[k], v) finally: self.run_subprocess_assert_returncode([ "podman", "rmi", "my-busybox-build-labels-map", "my-busybox-build-labels-array", ]) podman-compose-1.3.0/tests/integration/build_secrets/000077500000000000000000000000001473727656100227725ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_secrets/Dockerfile000066400000000000000000000004341473727656100247650ustar00rootroot00000000000000FROM 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.3.0/tests/integration/build_secrets/docker-compose.yaml000066400000000000000000000010661473727656100265730ustar00rootroot00000000000000version: "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.3.0/tests/integration/build_secrets/docker-compose.yaml.invalid000066400000000000000000000006701473727656100302200ustar00rootroot00000000000000version: "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.3.0/tests/integration/build_secrets/my_secret000066400000000000000000000000361473727656100247060ustar00rootroot00000000000000important-secret-is-important podman-compose-1.3.0/tests/integration/build_ssh/000077500000000000000000000000001473727656100221175ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_ssh/context/000077500000000000000000000000001473727656100236035ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/build_ssh/context/Dockerfile000066400000000000000000000006041473727656100255750ustar00rootroot00000000000000# Base image FROM alpine:latest # Install OpenSSH client RUN apk add openssh # Test the SSH agents during the build RUN echo -n "default: " >> /result.log RUN --mount=type=ssh ssh-add -L >> /result.log RUN echo -n "id1: " >> /result.log RUN --mount=type=ssh,id=id1 ssh-add -L >> /result.log RUN echo -n "id2: " >> /result.log RUN --mount=type=ssh,id=id2 ssh-add -L >> /result.log podman-compose-1.3.0/tests/integration/build_ssh/docker-compose.yml000066400000000000000000000010541473727656100255540ustar00rootroot00000000000000version: "3" services: test_build_ssh_map: build: context: ./context dockerfile: Dockerfile ssh: default: id1: "./id_ed25519_dummy" id2: "./agent_dummy.sock" image: my-alpine-build-ssh-map command: - cat - /result.log test_build_ssh_array: build: context: ./context dockerfile: Dockerfile ssh: - default - "id1=./id_ed25519_dummy" - "id2=./agent_dummy.sock" image: my-alpine-build-ssh-array command: - cat - /result.log podman-compose-1.3.0/tests/integration/build_ssh/id_ed25519_dummy000066400000000000000000000006331473727656100247310ustar00rootroot00000000000000-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACBWELzfWvraCAeo0rOM2OxTGqWZx7fNBCglK/1oS8FLpgAAAJhzHuERcx7h EQAAAAtzc2gtZWQyNTUxOQAAACBWELzfWvraCAeo0rOM2OxTGqWZx7fNBCglK/1oS8FLpg AAAEAEIrYvY3jJ2IvAnUa5jIrVe8UG+7G7PzWzZqqBQykZllYQvN9a+toIB6jSs4zY7FMa pZnHt80EKCUr/WhLwUumAAAADnJpbmdvQGJuZHRib3gyAQIDBAUGBw== -----END OPENSSH PRIVATE KEY----- podman-compose-1.3.0/tests/integration/build_ssh/test_build_ssh.py000066400000000000000000000216571473727656100255170ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import socket import struct import threading import unittest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin expected_lines = [ "default: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", "id1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", "id2: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", ] class TestBuildSsh(unittest.TestCase, RunSubprocessMixin): def test_build_ssh(self): """The build context can contain the ssh authentications that the image builder should use during image build. They can be either an array or a map. """ compose_path = os.path.join(test_path(), "build_ssh/docker-compose.yml") sock_path = os.path.join(test_path(), "build_ssh/agent_dummy.sock") private_key_file = os.path.join(test_path(), "build_ssh/id_ed25519_dummy") agent = MockSSHAgent(private_key_file) try: # Set SSH_AUTH_SOCK because `default` expects it os.environ['SSH_AUTH_SOCK'] = sock_path # Start a mock SSH agent server agent.start_agent(sock_path) self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "build", "test_build_ssh_map", "test_build_ssh_array", ]) for test_image in [ "test_build_ssh_map", "test_build_ssh_array", ]: out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "run", "--rm", test_image, ]) out = out.decode('utf-8') # Check if all lines are contained in the output self.assertTrue( all(line in out for line in expected_lines), f"Incorrect output for image {test_image}", ) finally: # Now we send the stop command to gracefully shut down the server agent.stop_agent() if os.path.exists(sock_path): os.remove(sock_path) self.run_subprocess_assert_returncode([ "podman", "rmi", "my-alpine-build-ssh-map", "my-alpine-build-ssh-array", ]) # SSH agent message types SSH_AGENTC_REQUEST_IDENTITIES = 11 SSH_AGENT_IDENTITIES_ANSWER = 12 SSH_AGENT_FAILURE = 5 STOP_REQUEST = 0xFF class MockSSHAgent: def __init__(self, private_key_path): self.sock_path = None self.server_sock = None self.running = threading.Event() self.keys = [self._load_ed25519_private_key(private_key_path)] self.agent_thread = None # Thread to run the agent def _load_ed25519_private_key(self, private_key_path): """Load ED25519 private key from an OpenSSH private key file.""" with open(private_key_path, 'rb') as key_file: private_key = serialization.load_ssh_private_key(key_file.read(), password=None) # Ensure it's an Ed25519 key if not isinstance(private_key, Ed25519PrivateKey): raise ValueError("Invalid key type, expected ED25519 private key.") # Get the public key corresponding to the private key public_key = private_key.public_key() # Serialize the public key to the OpenSSH format public_key_blob = public_key.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw ) # SSH key type "ssh-ed25519" key_type = b"ssh-ed25519" # Build the key blob (public key part for the agent) key_blob_full = ( struct.pack(">I", len(key_type)) + key_type # Key type length + type + struct.pack(">I", len(public_key_blob)) + public_key_blob # Public key length + key blob ) # Comment (empty) comment = "" return ("ssh-ed25519", key_blob_full, comment) def start_agent(self, sock_path): """Start the mock SSH agent and create a Unix domain socket.""" self.sock_path = sock_path if os.path.exists(self.sock_path): os.remove(self.sock_path) self.server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.server_sock.bind(self.sock_path) self.server_sock.listen(5) os.environ['SSH_AUTH_SOCK'] = self.sock_path self.running.set() # Set the running event # Start a thread to accept client connections self.agent_thread = threading.Thread(target=self._accept_connections, daemon=True) self.agent_thread.start() def _accept_connections(self): """Accept and handle incoming connections.""" while self.running.is_set(): try: client_sock, _ = self.server_sock.accept() self._handle_client(client_sock) except Exception as e: print(f"Error accepting connection: {e}") def _handle_client(self, client_sock): """Handle a single client request (like ssh-add).""" try: # Read the message length (first 4 bytes) length_message = client_sock.recv(4) if not length_message: raise "no length message received" msg_len = struct.unpack(">I", length_message)[0] request_message = client_sock.recv(msg_len) # Check for STOP_REQUEST if request_message[0] == STOP_REQUEST: client_sock.close() self.running.clear() # Stop accepting connections return # Check for SSH_AGENTC_REQUEST_IDENTITIES if request_message[0] == SSH_AGENTC_REQUEST_IDENTITIES: response = self._mock_list_keys_response() client_sock.sendall(response) else: print("Message not recognized") # Send failure if the message type is not recognized response = struct.pack(">I", 1) + struct.pack(">B", SSH_AGENT_FAILURE) client_sock.sendall(response) except socket.error: print("Client socket error.") pass # You can handle specific errors here if needed finally: client_sock.close() # Ensure the client socket is closed def _mock_list_keys_response(self): """Create a mock response for ssh-add -l, listing keys.""" # Start building the response response = struct.pack(">B", SSH_AGENT_IDENTITIES_ANSWER) # Message type # Number of keys response += struct.pack(">I", len(self.keys)) # For each key, append key blob and comment for key_type, key_blob, comment in self.keys: # Key blob length and content response += struct.pack(">I", len(key_blob)) + key_blob # Comment length and content comment_encoded = comment.encode() response += struct.pack(">I", len(comment_encoded)) + comment_encoded # Prefix the entire response with the total message length response = struct.pack(">I", len(response)) + response return response def stop_agent(self): """Stop the mock SSH agent.""" if self.running.is_set(): # First check if the agent is running # Create a temporary connection to send the stop command with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_sock: client_sock.connect(self.sock_path) # Connect to the server stop_command = struct.pack( ">B", STOP_REQUEST ) # Pack the stop command as a single byte # Send the message length first message_length = struct.pack(">I", len(stop_command)) client_sock.sendall(message_length) # Send the length first client_sock.sendall(stop_command) # Send the stop command self.running.clear() # Stop accepting new connections # Wait for the agent thread to finish if self.agent_thread: self.agent_thread.join() # Wait for the thread to finish self.agent_thread = None # Reset thread reference # Remove the socket file only after the server socket is closed if self.server_sock: # Check if the server socket exists self.server_sock.close() # Close the server socket os.remove(self.sock_path) podman-compose-1.3.0/tests/integration/default_net_behavior/000077500000000000000000000000001473727656100243145ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_no_nets.yaml000066400000000000000000000001131473727656100316320ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_no_nets_compat.yaml000066400000000000000000000001721473727656100332020ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ x-podman: default_net_behavior_compat: true podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_one_net.yaml000066400000000000000000000001411473727656100316150ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_one_net_compat.yaml000066400000000000000000000002201473727656100331560ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} x-podman: default_net_behavior_compat: true podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_two_nets.yaml000066400000000000000000000001541473727656100320340ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} net1: {} podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_two_nets_compat.yaml000066400000000000000000000002331473727656100333750ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} net1: {} x-podman: default_net_behavior_compat: true podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_with_default.yaml000066400000000000000000000001721473727656100326510ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} net1: {} default: {} podman-compose-1.3.0/tests/integration/default_net_behavior/docker-compose_with_default_compat.yaml000066400000000000000000000002511473727656100342120ustar00rootroot00000000000000services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ networks: net0: {} net1: {} default: {} x-podman: default_net_behavior_compat: true podman-compose-1.3.0/tests/integration/deps/000077500000000000000000000000001473727656100210765ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/deps/docker-compose-conditional-fails.yaml000066400000000000000000000011731473727656100302730ustar00rootroot00000000000000version: "3.7" services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp healthcheck: test: ["CMD", "/bin/false"] interval: 10s # Time between health checks timeout: 1s # Time to wait for a response retries: 1 # Number of consecutive failures before marking as unhealthy sleep: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"] depends_on: web: condition: service_healthy tmpfs: - /run - /tmp podman-compose-1.3.0/tests/integration/deps/docker-compose-conditional-succeeds.yaml000066400000000000000000000012341473727656100307710ustar00rootroot00000000000000version: "3.7" services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] tmpfs: - /run - /tmp healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"] interval: 30s # Time between health checks timeout: 5s # Time to wait for a response retries: 3 # Number of consecutive failures before marking as unhealthy sleep: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"] depends_on: web: condition: service_healthy tmpfs: - /run - /tmp podman-compose-1.3.0/tests/integration/deps/docker-compose.yaml000066400000000000000000000011341473727656100246730ustar00rootroot00000000000000version: "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.3.0/tests/integration/env-file-tests/000077500000000000000000000000001473727656100230105ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/.env000066400000000000000000000001611473727656100235770ustar00rootroot00000000000000ZZVAR1='This value is overwritten by env-file-tests/.env' ZZVAR3='This value is loaded from env-file-tests/.env' podman-compose-1.3.0/tests/integration/env-file-tests/.gitignore000066400000000000000000000002361473727656100250010ustar00rootroot00000000000000# 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.3.0/tests/integration/env-file-tests/env-files/000077500000000000000000000000001473727656100247005ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/env-files/project-1.env000066400000000000000000000001101473727656100272060ustar00rootroot00000000000000ZZVAR1=podman-rocks-123 ZZVAR2=podman-rocks-124 ZZVAR3=podman-rocks-125 podman-compose-1.3.0/tests/integration/env-file-tests/env-files/project-2.env000066400000000000000000000000601473727656100272130ustar00rootroot00000000000000ZZVAR1=podman-rocks-223 ZZVAR2=podman-rocks-224 podman-compose-1.3.0/tests/integration/env-file-tests/project/000077500000000000000000000000001473727656100244565ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/project/.env000066400000000000000000000001661473727656100252520ustar00rootroot00000000000000ZZVAR1='This value is loaded but should be overwritten' ZZVAR2='This value is loaded from .env in project/ directory' podman-compose-1.3.0/tests/integration/env-file-tests/project/container-compose.env-file-flat.yaml000066400000000000000000000002651473727656100334220ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp env_file: - ../env-files/project-1.env container-compose.env-file-obj-optional-exists.yaml000066400000000000000000000004171473727656100363260ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/projectservices: 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 # this file exists required: false container-compose.env-file-obj-optional-missing.yaml000066400000000000000000000004231473727656100364550ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/projectservices: 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-3.env # this file is missing required: false podman-compose-1.3.0/tests/integration/env-file-tests/project/container-compose.env-file-obj.yaml000066400000000000000000000002731473727656100332450ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp env_file: - path: ../env-files/project-1.env container-compose.load-.env-in-project.yaml000066400000000000000000000003271473727656100345440ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-file-tests/projectservices: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp environment: ZZVAR1: $ZZVAR1 ZZVAR2: $ZZVAR2 ZZVAR3: $ZZVAR3 podman-compose-1.3.0/tests/integration/env-file-tests/project/container-compose.yaml000066400000000000000000000002531473727656100307670ustar00rootroot00000000000000services: app: image: busybox command: ["/bin/busybox", "sh", "-c", "env | grep ZZ"] tmpfs: - /run - /tmp environment: ZZVAR1: $ZZVAR1 podman-compose-1.3.0/tests/integration/env-tests/000077500000000000000000000000001473727656100220735ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/env-tests/container-compose.yml000066400000000000000000000002631473727656100262440ustar00rootroot00000000000000version: "3" services: env-test: image: busybox command: sh -c "export | grep ZZ" environment: ZZVAR1: myval1 ZZVAR2: 2-$ZZVAR1 ZZVAR3: 3-$ZZVAR2 podman-compose-1.3.0/tests/integration/exit-from/000077500000000000000000000000001473727656100220555ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/exit-from/docker-compose.yaml000066400000000000000000000005501473727656100256530ustar00rootroot00000000000000version: "3" services: 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.3.0/tests/integration/extends/000077500000000000000000000000001473727656100216155ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends/docker-compose.yaml000066400000000000000000000011511473727656100254110ustar00rootroot00000000000000version: "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.3.0/tests/integration/extends_w_empty_service/000077500000000000000000000000001473727656100251015ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_empty_service/common-services.yml000066400000000000000000000001621473727656100307340ustar00rootroot00000000000000services: webapp_default: webapp_special: image: nopush/podman-compose-test volumes: - "/data" podman-compose-1.3.0/tests/integration/extends_w_empty_service/docker-compose.yml000066400000000000000000000003021473727656100305310ustar00rootroot00000000000000version: "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.3.0/tests/integration/extends_w_file/000077500000000000000000000000001473727656100231425ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_file/Dockerfile000066400000000000000000000000501473727656100251270ustar00rootroot00000000000000FROM nopush/podman-compose-test as base podman-compose-1.3.0/tests/integration/extends_w_file/common-services.yml000066400000000000000000000001101473727656100267660ustar00rootroot00000000000000webapp: build: . ports: - "8000:8000" volumes: - "/data" podman-compose-1.3.0/tests/integration/extends_w_file/docker-compose.yml000066400000000000000000000003131473727656100265740ustar00rootroot00000000000000version: "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.3.0/tests/integration/extends_w_file_subdir/000077500000000000000000000000001473727656100245125ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_file_subdir/docker-compose.yml000066400000000000000000000002041473727656100301430ustar00rootroot00000000000000version: "3" services: web: extends: file: sub/docker-compose.yml service: webapp environment: - DEBUG=1podman-compose-1.3.0/tests/integration/extends_w_file_subdir/sub/000077500000000000000000000000001473727656100253035ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_file_subdir/sub/docker-compose.yml000066400000000000000000000003101473727656100307320ustar00rootroot00000000000000version: "3" services: webapp: build: context: docker/example dockerfile: Dockerfile image: localhost/subdir_test:me ports: - "8000:8000" volumes: - "/data" podman-compose-1.3.0/tests/integration/extends_w_file_subdir/sub/docker/000077500000000000000000000000001473727656100265525ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_file_subdir/sub/docker/example/000077500000000000000000000000001473727656100302055ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/extends_w_file_subdir/sub/docker/example/Dockerfile000066400000000000000000000000501473727656100321720ustar00rootroot00000000000000FROM nopush/podman-compose-test as base podman-compose-1.3.0/tests/integration/filesystem/000077500000000000000000000000001473727656100223275ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/filesystem/compose_symlink/000077500000000000000000000000001473727656100255425ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/filesystem/compose_symlink/docker-compose.yml000077700000000000000000000000001473727656100412742../compose_symlink_dest/docker-compose.ymlustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/filesystem/compose_symlink/file000066400000000000000000000000251473727656100264010ustar00rootroot00000000000000data_compose_symlink podman-compose-1.3.0/tests/integration/filesystem/compose_symlink_dest/000077500000000000000000000000001473727656100265615ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/filesystem/compose_symlink_dest/docker-compose.yml000066400000000000000000000002351473727656100322160ustar00rootroot00000000000000version: "3" services: container1: image: nopush/podman-compose-test command: ["/bin/busybox", "cat", "/file"] volumes: - "./file:/file" podman-compose-1.3.0/tests/integration/filesystem/compose_symlink_dest/file000066400000000000000000000000321473727656100274160ustar00rootroot00000000000000data_compose_symlink_dest podman-compose-1.3.0/tests/integration/in_pod/000077500000000000000000000000001473727656100214135ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/in_pod/custom_x-podman_false/000077500000000000000000000000001473727656100257025ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/in_pod/custom_x-podman_false/docker-compose.yml000066400000000000000000000003321473727656100313350ustar00rootroot00000000000000version: "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.3.0/tests/integration/in_pod/custom_x-podman_not_exists/000077500000000000000000000000001473727656100270075ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/in_pod/custom_x-podman_not_exists/docker-compose.yml000066400000000000000000000002751473727656100324500ustar00rootroot00000000000000version: "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.3.0/tests/integration/in_pod/custom_x-podman_true/000077500000000000000000000000001473727656100255675ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/in_pod/custom_x-podman_true/docker-compose.yml000066400000000000000000000003311473727656100312210ustar00rootroot00000000000000version: "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.3.0/tests/integration/include/000077500000000000000000000000001473727656100215665ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/include/docker-compose.base.yaml000066400000000000000000000002331473727656100262730ustar00rootroot00000000000000version: '3.6' services: web: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"] podman-compose-1.3.0/tests/integration/include/docker-compose.extend.yaml000066400000000000000000000002331473727656100266500ustar00rootroot00000000000000version: '3.6' services: web2: image: nopush/podman-compose-test command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8004"] podman-compose-1.3.0/tests/integration/include/docker-compose.yaml000066400000000000000000000001251473727656100253620ustar00rootroot00000000000000version: '3.6' include: - docker-compose.base.yaml - docker-compose.extend.yaml podman-compose-1.3.0/tests/integration/interpolation/000077500000000000000000000000001473727656100230325ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/interpolation/.env000066400000000000000000000000621473727656100236210ustar00rootroot00000000000000DOT_ENV_VARIABLE=This value is from the .env file podman-compose-1.3.0/tests/integration/interpolation/docker-compose-colon-question-error.yml000066400000000000000000000003521473727656100325730ustar00rootroot00000000000000version: "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.3.0/tests/integration/interpolation/docker-compose-question-error.yml000066400000000000000000000003431473727656100314630ustar00rootroot00000000000000version: "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.3.0/tests/integration/interpolation/docker-compose.yml000066400000000000000000000010661473727656100264720ustar00rootroot00000000000000version: "3.7" services: variables: image: busybox command: ["/bin/busybox", "sh", "-c", "export | grep EXAMPLE"] environment: EXAMPLE_VARIABLE: "Host user: $EXAMPLE_VARIABLE_USER" EXAMPLE_BRACES: "Host user: ${EXAMPLE_VARIABLE_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.3.0/tests/integration/ipam_default/000077500000000000000000000000001473727656100225755ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/ipam_default/docker-compose.yaml000066400000000000000000000004061473727656100263730ustar00rootroot00000000000000version: '3' # --ipam-driver must not be passed 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.3.0/tests/integration/lifetime/000077500000000000000000000000001473727656100217415ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/lifetime/test_lifetime.py000066400000000000000000000070271473727656100251560ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from parameterized import parameterized from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin class TestLifetime(unittest.TestCase, RunSubprocessMixin): def test_up_single_container(self): """Podman compose up should be able to start containers one after another""" compose_path = os.path.join(test_path(), "lifetime/up_single_container/docker-compose.yml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "up", "-d", "container1", ]) self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "up", "-d", "container2", ]) out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "logs", "container1", ]) self.assertEqual(out, b"test1\n") out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "logs", "container2", ]) self.assertEqual(out, b"test2\n") finally: out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "down", ]) @parameterized.expand([ ("no_ports", "up_single_container_many_times"), ("with_ports", "up_single_container_many_times_with_ports"), ]) def test_up_single_container_many_times(self, name, subdir): """Podman compose up should be able to start a container many times after it finishes running. """ compose_path = os.path.join(test_path(), f"lifetime/{subdir}/docker-compose.yml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "up", "-d", "container1", ]) for _ in range(0, 3): self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "up", "-d", "container2", ]) out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "logs", "container1", ]) self.assertEqual(out, b"test1\n") out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "logs", "container2", ]) # BUG: container should be started 3 times, not 4. self.assertEqual(out, b"test2\n" * 4) finally: out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "down", ]) podman-compose-1.3.0/tests/integration/lifetime/up_single_container/000077500000000000000000000000001473727656100257705ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/lifetime/up_single_container/docker-compose.yml000066400000000000000000000003751473727656100314320ustar00rootroot00000000000000version: "3" services: container1: image: nopush/podman-compose-test command: ["/bin/bash", "-c", "echo test1; sleep infinity"] container2: image: nopush/podman-compose-test command: ["/bin/bash", "-c", "echo test2; sleep infinity"] podman-compose-1.3.0/tests/integration/lifetime/up_single_container_many_times/000077500000000000000000000000001473727656100302155ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/lifetime/up_single_container_many_times/docker-compose.yml000066400000000000000000000004001473727656100336440ustar00rootroot00000000000000version: "3" services: container1: image: nopush/podman-compose-test command: ["/bin/bash", "-c", "echo test1; sleep infinity"] container2: image: nopush/podman-compose-test restart: never command: ["/bin/bash", "-c", "echo test2"] podman-compose-1.3.0/tests/integration/lifetime/up_single_container_many_times_with_ports/000077500000000000000000000000001473727656100324775ustar00rootroot00000000000000docker-compose.yml000066400000000000000000000004561473727656100360620ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/lifetime/up_single_container_many_times_with_portsversion: "3" services: container1: image: nopush/podman-compose-test ports: "9001:9001" command: ["/bin/bash", "-c", "echo test1; sleep infinity"] container2: image: nopush/podman-compose-test restart: never ports: "9002:9002" command: ["/bin/bash", "-c", "echo test2"] podman-compose-1.3.0/tests/integration/multicompose/000077500000000000000000000000001473727656100226635ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/multicompose/d1/000077500000000000000000000000001473727656100231675ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/multicompose/d1/1.env000066400000000000000000000000161473727656100240360ustar00rootroot00000000000000var1=d1/1.env podman-compose-1.3.0/tests/integration/multicompose/d1/12.env000066400000000000000000000000201473727656100241130ustar00rootroot00000000000000var12=d1/12.env podman-compose-1.3.0/tests/integration/multicompose/d1/2.env000066400000000000000000000000161473727656100240370ustar00rootroot00000000000000var2=d1/2.env podman-compose-1.3.0/tests/integration/multicompose/d1/docker-compose.yml000066400000000000000000000003661473727656100266310ustar00rootroot00000000000000version: '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.3.0/tests/integration/multicompose/d2/000077500000000000000000000000001473727656100231705ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/multicompose/d2/12.env000066400000000000000000000000201473727656100241140ustar00rootroot00000000000000var12=d2/12.env podman-compose-1.3.0/tests/integration/multicompose/d2/2.env000066400000000000000000000000161473727656100240400ustar00rootroot00000000000000var2=d2/2.env podman-compose-1.3.0/tests/integration/multicompose/d2/docker-compose.yml000066400000000000000000000005221473727656100266240ustar00rootroot00000000000000version: '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.3.0/tests/integration/nethost/000077500000000000000000000000001473727656100216275ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/nethost/docker-compose.yaml000066400000000000000000000001571473727656100254300ustar00rootroot00000000000000version: '3' services: web: image: busybox command: httpd -f -p 8123 -h /tmp/ network_mode: host podman-compose-1.3.0/tests/integration/nets_test1/000077500000000000000000000000001473727656100222345ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/nets_test1/docker-compose.yml000066400000000000000000000010601473727656100256660ustar00rootroot00000000000000version: "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.3.0/tests/integration/nets_test1/test1.txt000066400000000000000000000000061473727656100240310ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/nets_test1/test2.txt000066400000000000000000000000061473727656100240320ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/nets_test2/000077500000000000000000000000001473727656100222355ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/nets_test2/docker-compose.yml000066400000000000000000000011071473727656100256710ustar00rootroot00000000000000version: "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.3.0/tests/integration/nets_test2/test1.txt000066400000000000000000000000061473727656100240320ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/nets_test2/test2.txt000066400000000000000000000000061473727656100240330ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/nets_test3/000077500000000000000000000000001473727656100222365ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/nets_test3/docker-compose.yml000066400000000000000000000020611473727656100256720ustar00rootroot00000000000000version: "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.3.0/tests/integration/nets_test3/test1.txt000066400000000000000000000000061473727656100240330ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/nets_test3/test2.txt000066400000000000000000000000061473727656100240340ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/nets_test_ip/000077500000000000000000000000001473727656100226435ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/nets_test_ip/docker-compose.yml000066400000000000000000000034521473727656100263040ustar00rootroot00000000000000version: "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" 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.3.0/tests/integration/nets_test_ip/test1.txt000066400000000000000000000000061473727656100244400ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/nets_test_ip/test2.txt000066400000000000000000000000061473727656100244410ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/nets_test_ip/test3.txt000066400000000000000000000000061473727656100244420ustar00rootroot00000000000000test3 podman-compose-1.3.0/tests/integration/nets_test_ip/test4.txt000066400000000000000000000000061473727656100244430ustar00rootroot00000000000000test4 podman-compose-1.3.0/tests/integration/network/000077500000000000000000000000001473727656100216345ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/network/docker-compose.yml000066400000000000000000000011071473727656100252700ustar00rootroot00000000000000version: "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.3.0/tests/integration/network/test1.txt000066400000000000000000000000061473727656100234310ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/network/test2.txt000066400000000000000000000000061473727656100234320ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/network_scoped_aliases/000077500000000000000000000000001473727656100246725ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/network_scoped_aliases/docker-compose.yaml000066400000000000000000000013121473727656100304650ustar00rootroot00000000000000--- networks: net0: ipam: config: - subnet: "172.19.3.0/24" net1: ipam: config: - subnet: "172.19.4.0/24" services: web1: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] networks: net0: ipv4_address: "172.19.3.11" aliases: - secure-web net1: ipv4_address: "172.19.4.11" aliases: - insecure-web utils-net0: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] networks: - net0 utils-net1: image: busybox command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] networks: - net1 podman-compose-1.3.0/tests/integration/no_services/000077500000000000000000000000001473727656100224625ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/no_services/docker-compose.yaml000066400000000000000000000001661473727656100262630ustar00rootroot00000000000000version: '3' networks: shared-network: driver: bridge ipam: config: - subnet: 172.19.0.0/24 podman-compose-1.3.0/tests/integration/pid/000077500000000000000000000000001473727656100207175ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/pid/docker-compose.yml000066400000000000000000000001341473727656100243520ustar00rootroot00000000000000version: "3" services: serv: image: busybox pid: host command: sh -c "ps all" podman-compose-1.3.0/tests/integration/ports/000077500000000000000000000000001473727656100213125ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/ports/docker-compose.yml000066400000000000000000000017201473727656100247470ustar00rootroot00000000000000version: "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.3.0/tests/integration/ports/test1.txt000066400000000000000000000000061473727656100231070ustar00rootroot00000000000000test1 podman-compose-1.3.0/tests/integration/ports/test2.txt000066400000000000000000000000061473727656100231100ustar00rootroot00000000000000test2 podman-compose-1.3.0/tests/integration/profile/000077500000000000000000000000001473727656100216035ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/profile/docker-compose.yml000066400000000000000000000012261473727656100252410ustar00rootroot00000000000000version: "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.3.0/tests/integration/seccomp/000077500000000000000000000000001473727656100215745ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/seccomp/docker-compose.yml000066400000000000000000000003531473727656100252320ustar00rootroot00000000000000version: "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.3.0/tests/integration/secrets/000077500000000000000000000000001473727656100216135ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/secrets/bad_external_name/000077500000000000000000000000001473727656100252435ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/secrets/bad_external_name/docker-compose.yaml000066400000000000000000000004061473727656100310410ustar00rootroot00000000000000version: "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.3.0/tests/integration/secrets/bad_external_target/000077500000000000000000000000001473727656100256115ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/secrets/bad_external_target/docker-compose.yaml000066400000000000000000000004261473727656100314110ustar00rootroot00000000000000version: "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.3.0/tests/integration/secrets/docker-compose.yaml000066400000000000000000000021121473727656100254050ustar00rootroot00000000000000--- # 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.3.0/tests/integration/secrets/my_secret000066400000000000000000000000361473727656100235270ustar00rootroot00000000000000important-secret-is-important podman-compose-1.3.0/tests/integration/secrets/print_secrets.sh000077500000000000000000000001731473727656100250370ustar00rootroot00000000000000#!/bin/sh ls -la /run/secrets/* ls -la /etc/custom_location cat /run/secrets/* cat /etc/custom_location env | grep SECRET podman-compose-1.3.0/tests/integration/selinux/000077500000000000000000000000001473727656100216325ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/selinux/docker-compose.yml000066400000000000000000000004161473727656100252700ustar00rootroot00000000000000version: "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.3.0/tests/integration/short/000077500000000000000000000000001473727656100213025ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/data/000077500000000000000000000000001473727656100222135ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/data/redis/000077500000000000000000000000001473727656100233215ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/data/redis/.keep000066400000000000000000000000001473727656100242340ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/data/web/000077500000000000000000000000001473727656100227705ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/data/web/.keep000066400000000000000000000000001473727656100237030ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/short/docker-compose.yaml000066400000000000000000000022031473727656100250750ustar00rootroot00000000000000version: "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.3.0/tests/integration/test_podman_compose.py000066400000000000000000000064061473727656100245650ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from pathlib import Path from tests.integration.test_utils import RunSubprocessMixin def base_path(): """Returns the base path for the project""" return Path(__file__).parent.parent.parent def test_path(): """Returns the path to the tests directory""" return os.path.join(base_path(), "tests/integration") 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.parent command_up = [ "coverage", "run", str(main_path.joinpath("podman_compose.py")), "-f", str( main_path.joinpath( "tests", "integration", "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", "integration", "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", "integration", "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.parent command_up = [ "python3", str(main_path.joinpath("podman_compose.py")), "-f", str( main_path.joinpath( "tests", "integration", "extends_w_empty_service", "docker-compose.yml" ) ), "up", "-d", ] self.run_subprocess_assert_returncode(command_up) podman-compose-1.3.0/tests/integration/test_podman_compose_additional_contexts.py000066400000000000000000000025021473727656100306750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how additional contexts are passed to podman.""" import os import subprocess import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.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.3.0/tests/integration/test_podman_compose_build.py000066400000000000000000000041571473727656100257450ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest import requests from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): """ "Returns the path to the compose file used for this test module""" base_path = os.path.join(test_path(), "build") return os.path.join(base_path, "docker-compose.yml") class TestComposeBuild(unittest.TestCase, RunSubprocessMixin): def test_build(self): try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "build", "--no-cache", ]) self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "up", "-d", ]) request = requests.get('http://localhost:8080/index.txt') self.assertEqual(request.status_code, 200) alt_request_success = False try: # FIXME: suspicious behaviour, too often ends up in error alt_request = requests.get('http://localhost:8000/index.txt') self.assertEqual(alt_request.status_code, 200) self.assertIn("ALT buildno=2 port=8000 ", alt_request.text) alt_request_success = True except requests.exceptions.ConnectionError: pass if alt_request_success: output, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "my-busybox-httpd2", ]) self.assertIn("httpd_port=8000", str(output)) self.assertIn("buildno=2", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_build_fail.py000066400000000000000000000020341473727656100267300ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): """ "Returns the path to the compose file used for this test module""" base_path = os.path.join(test_path(), "build_fail") return os.path.join(base_path, "docker-compose.yml") class TestComposeBuildFail(unittest.TestCase, RunSubprocessMixin): def test_build_fail(self): output, error = self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "build", ], expected_returncode=127, ) self.assertIn("RUN this_command_does_not_exist", str(output)) self.assertIn("this_command_does_not_exist: not found", str(error)) self.assertIn("while running runtime: exit status 127", str(error)) podman-compose-1.3.0/tests/integration/test_podman_compose_build_secrets.py000066400000000000000000000057201473727656100274720ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how secrets in files are passed to podman.""" import os import subprocess import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.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.3.0/tests/integration/test_podman_compose_build_ulimits.py000066400000000000000000000060761473727656100275150ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 """Test how ulimits are applied in podman-compose build.""" import os import subprocess import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.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.3.0/tests/integration/test_podman_compose_config.py000066400000000000000000000053331473727656100261100ustar00rootroot00000000000000# 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 tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.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.3.0/tests/integration/test_podman_compose_default_net_behavior.py000066400000000000000000000043621473727656100310150ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from parameterized import parameterized from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(scenario): return os.path.join( os.path.join(test_path(), "default_net_behavior"), f"docker-compose_{scenario}.yaml" ) class TestComposeDefaultNetBehavior(unittest.TestCase, RunSubprocessMixin): @parameterized.expand([ ('no_nets', 'default_net_behavior_default'), ('one_net', 'default_net_behavior_net0'), ('two_nets', 'podman'), ('with_default', 'default_net_behavior_default'), ('no_nets_compat', 'default_net_behavior_default'), ('one_net_compat', 'default_net_behavior_default'), ('two_nets_compat', 'default_net_behavior_default'), ('with_default_compat', 'default_net_behavior_default'), ]) def test_nethost(self, scenario, default_net): try: self.run_subprocess_assert_returncode( [podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"], ) container_id, _ = self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(scenario), "ps", "--format", '{{.ID}}', ], ) container_id = container_id.decode('utf-8').split('\n')[0] output, _ = self.run_subprocess_assert_returncode( [ "podman", "inspect", container_id, "--format", "{{range $key, $value := .NetworkSettings.Networks }}{{ $key }}\n{{ end }}", ], ) self.assertEqual(output.decode('utf-8').strip(), default_net) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(scenario), "down", "-t", "0", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_deps.py000066400000000000000000000052021473727656100255710ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(suffix=""): return os.path.join(os.path.join(test_path(), "deps"), f"docker-compose{suffix}.yaml") class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin): def test_deps(self): try: output, error = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "run", "--rm", "sleep", "/bin/sh", "-c", "wget -O - http://web:8000/hosts", ]) self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output) self.assertIn(b"deps_web_1", output) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin): def test_deps_succeeds(self): suffix = "-conditional-succeeds" try: output, error = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(suffix), "run", "--rm", "sleep", "/bin/sh", "-c", "wget -O - http://web:8000/hosts", ]) self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output) self.assertIn(b"deps_web_1", output) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(suffix), "down", ]) def test_deps_fails(self): suffix = "-conditional-fails" try: output, error = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(suffix), "ps", ]) self.assertNotIn(b"HTTP request sent, awaiting response... 200 OK", output) self.assertNotIn(b"deps_web_1", output) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(suffix), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_env.py000066400000000000000000000022131473727656100254250ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "env-tests"), "container-compose.yml") class TestComposeEnv(unittest.TestCase, RunSubprocessMixin): """Test that inline environment variable overrides environment variable from compose file.""" def test_env(self): try: output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "run", "-l", "monkey", "-e", "ZZVAR1=myval2", "env-test", ]) self.assertIn("ZZVAR1='myval2'", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_env_file.py000066400000000000000000000221051473727656100264260ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_base_path(): return os.path.join(test_path(), "env-file-tests") class TestComposeEnvFile(unittest.TestCase, RunSubprocessMixin): def test_path_env_file_inline(self): # Test taking env variable value directly from env-file when its path is inline path base_path = compose_base_path() path_compose_file = os.path.join(base_path, "project/container-compose.yaml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "--env-file", os.path.join(base_path, "env-files/project-1.env"), "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # takes only value ZZVAR1 as container-compose.yaml file requires self.assertEqual(output, b"ZZVAR1=podman-rocks-123\n") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_path_env_file_flat_in_compose_file(self): # Test taking env variable value from env-file/project-1.env which was declared in # compose file's env_file base_path = compose_base_path() path_compose_file = os.path.join(base_path, "project/container-compose.env-file-flat.yaml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # takes all values with a substring ZZ as container-compose.env-file-flat.yaml # file requires self.assertEqual( output, b"ZZVAR1=podman-rocks-123\nZZVAR2=podman-rocks-124\nZZVAR3=podman-rocks-125\n", ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_path_env_file_obj_in_compose_file(self): # take variable value from env-file project-1.env which was declared in compose # file's env_file by -path: ... base_path = compose_base_path() path_compose_file = os.path.join(base_path, "project/container-compose.env-file-obj.yaml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # takes all values with a substring ZZ as container-compose.env-file-obj.yaml # file requires self.assertEqual( output, b"ZZVAR1=podman-rocks-123\nZZVAR2=podman-rocks-124\nZZVAR3=podman-rocks-125\n", ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_exists_optional_env_file_path_in_compose_file(self): # test taking env variable values from several env-files when one of them is optional # and exists base_path = compose_base_path() path_compose_file = os.path.join( base_path, "project/container-compose.env-file-obj-optional-exists.yaml" ) try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # FIXME: gives a weird output, needs to be double checked self.assertEqual( output, b"ZZVAR1=podman-rocks-223\nZZVAR2=podman-rocks-224\nZZVAR3=podman-rocks-125\n", ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_missing_optional_env_file_path_in_compose_file(self): # test taking env variable values from several env-files when one of them is optional and # is missing (silently skip it) base_path = compose_base_path() path_compose_file = os.path.join( base_path, "project/container-compose.env-file-obj-optional-missing.yaml" ) try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # takes all values with a substring ZZ as container-compose.env-file-obj-optional.yaml # file requires self.assertEqual( output, b"ZZVAR1=podman-rocks-123\nZZVAR2=podman-rocks-124\nZZVAR3=podman-rocks-125\n", ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_var_value_inline_overrides_env_file_path_inline(self): # Test overriding env value when value is declared in inline command base_path = compose_base_path() path_compose_file = os.path.join(base_path, "project/container-compose.yaml") try: self.run_subprocess_assert_returncode([ "env", "ZZVAR1=podman-rocks-321", podman_compose_path(), "-f", path_compose_file, "--env-file", os.path.join(base_path, "env-files/project-1.env"), "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "logs", ]) # takes only value ZZVAR1 as container-compose.yaml file requires self.assertEqual(output, b"ZZVAR1=podman-rocks-321\n") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) def test_taking_env_variables_from_env_files_from_different_directories(self): # FIXME: It is not clear what this test actually tests, but from README.md it looks like: # Test overriding env values by directory env-files-tests/.env file values # and only take value from project/.env, when it does not exist in env-files-tests/.env base_path = compose_base_path() path_compose_file = os.path.join( base_path, "project/container-compose.load-.env-in-project.yaml" ) try: # looks like 'run' command does not actually create a container, so output_logs can not # be used for test comparison output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "run", "--rm", "app", ]) # takes all values with a substring ZZ as container-compose.load-.env-in-project.yaml # file requires # first line is random ID so is ignored in asserting lines = output.decode('utf-8').split('\n')[1:] self.assertEqual( lines, [ 'ZZVAR1=This value is loaded but should be overwritten\r', 'ZZVAR2=This value is loaded from .env in project/ directory\r', 'ZZVAR3=$ZZVAR3\r', '', ], ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", path_compose_file, "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_exit_from.py000066400000000000000000000030101473727656100266250ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "exit-from"), "docker-compose.yaml") class TestComposeExitFrom(unittest.TestCase, RunSubprocessMixin): def test_exit_code_sh1(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", "--exit-code-from=sh1", ], 1, ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) def test_exit_code_sh2(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", "--exit-code-from=sh2", ], 2, ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_extends.py000066400000000000000000000063221473727656100263140ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "extends"), "docker-compose.yaml") class TestComposeExteds(unittest.TestCase, RunSubprocessMixin): def test_extends_service_launch_echo(self): try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "up", "echo", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "logs", "echo", ]) self.assertEqual(output, b"Zero\n") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) def test_extends_service_launch_echo1(self): try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "up", "echo1", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "logs", "echo1", ]) self.assertEqual(output, b"One\n") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) def test_extends_service_launch_env1(self): try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "up", "env1", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "logs", "env1", ]) lines = output.decode('utf-8').split('\n') # HOSTNAME name is random string so is ignored in asserting lines = sorted([line for line in lines if not line.startswith("HOSTNAME")]) self.assertEqual( lines, [ '', 'BAR=local', 'BAZ=local', 'FOO=original', 'HOME=/root', 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'TERM=xterm', 'container=podman', ], ) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_extends_w_empty_service.py000066400000000000000000000023231473727656100315750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "extends_w_empty_service"), "docker-compose.yml") class TestComposeExtendsWithEmptyService(unittest.TestCase, RunSubprocessMixin): def test_extends_w_empty_service(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "ps", ]) self.assertIn("extends_w_empty_service_web_1", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_extends_w_file.py000066400000000000000000000024531473727656100276420ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "extends_w_file"), "docker-compose.yml") class TestComposeExtendsWithFile(unittest.TestCase, RunSubprocessMixin): def test_extends_w_file(self): # when file is Dockerfile for building the image try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "ps", ]) self.assertIn("extends_w_file_web_1", str(output)) self.assertIn("extends_w_file_important_web_1", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_extends_w_file_subdir.py000066400000000000000000000023751473727656100312150ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "extends_w_file_subdir"), "docker-compose.yml") class TestComposeExtendsWithFileSubdir(unittest.TestCase, RunSubprocessMixin): def test_extends_w_file_subdir(self): # when file is Dockerfile for building the image try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "ps", ]) self.assertIn("extends_w_file_subdir_web_1", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_filesystem.py000066400000000000000000000025741473727656100270330ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin class TestFilesystem(unittest.TestCase, RunSubprocessMixin): def test_compose_symlink(self): """The context of podman-compose.yml should come from the same directory as the file even if it is a symlink """ compose_path = os.path.join(test_path(), "filesystem/compose_symlink/docker-compose.yml") try: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "up", "-d", "container1", ]) out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "logs", "container1", ]) # BUG: figure out why cat is called twice self.assertEqual(out, b'data_compose_symlink\ndata_compose_symlink\n') finally: out, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_path, "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_in_pod.py000066400000000000000000000415071473727656100261160ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_utils import RunSubprocessMixin def base_path(): """Returns the base path for the project""" return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) def test_path(): """Returns the path to the tests directory""" return os.path.join(base_path(), "tests/integration") 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 """ command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "-f", os.path.join( base_path(), "tests", "integration", "in_pod", "custom_x-podman_false", "docker-compose.yml", ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=True", "-f", os.path.join( base_path(), "tests", "integration", "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 """ command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=False", "-f", os.path.join( base_path(), "tests", "integration", "in_pod", "custom_x-podman_false", "docker-compose.yml", ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", os.path.join( base_path(), "tests", "integration", "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="" """ command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=", "-f", os.path.join( base_path(), "tests", "integration", "in_pod", "custom_x-podman_false", "docker-compose.yml", ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=True", "-f", os.path.join( base_path(), "tests", "integration", "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 """ command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=False", "-f", os.path.join( base_path(), "tests", "integration", "in_pod", "custom_x-podman_true", "docker-compose.yml", ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=", "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container is not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container was not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=True", "-f", os.path.join( base_path(), "tests", "integration", "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 """ command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=False", "-f", os.path.join( base_path(), "tests", "integration", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml", ), "up", "-d", ] down_cmd = [ "python3", podman_compose_path(), "-f", os.path.join( base_path(), "tests", "integration", "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 """ # FIXME: creates a pod anyway, although it should not # Container was not created, so command 'down' is not needed command_up = [ "python3", os.path.join(base_path(), "podman_compose.py"), "--in-pod=", "-f", os.path.join( base_path(), "tests", "integration", "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.3.0/tests/integration/test_podman_compose_include.py000066400000000000000000000041551473727656100262670ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from pathlib import Path from tests.integration.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.parent command_up = [ "coverage", "run", str(main_path.joinpath("podman_compose.py")), "-f", str(main_path.joinpath("tests", "integration", "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.3.0/tests/integration/test_podman_compose_interpolation.py000066400000000000000000000032631473727656100275320ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "interpolation"), "docker-compose.yml") class TestComposeInterpolation(unittest.TestCase, RunSubprocessMixin): def test_interpolation(self): try: self.run_subprocess_assert_returncode([ "env", "EXAMPLE_VARIABLE_USER=test_user", podman_compose_path(), "-f", compose_yaml_path(), "up", ]) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "logs", ]) self.assertIn("EXAMPLE_VARIABLE='Host user: test_user'", str(output)) self.assertIn("EXAMPLE_BRACES='Host user: test_user'", str(output)) self.assertIn("EXAMPLE_COLON_DASH_DEFAULT='My default'", str(output)) self.assertIn("EXAMPLE_DASH_DEFAULT='My other default'", str(output)) self.assertIn("EXAMPLE_DOT_ENV='This value is from the .env file'", str(output)) self.assertIn("EXAMPLE_EMPTY=''", str(output)) self.assertIn("EXAMPLE_LITERAL='This is a $literal'", str(output)) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_ipam_default.py000066400000000000000000000040321473727656100272700ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import json import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "ipam_default"), "docker-compose.yaml") class TestComposeIpamDefault(unittest.TestCase, RunSubprocessMixin): def test_ipam_default(self): try: self.run_subprocess_assert_returncode( [podman_compose_path(), "-f", compose_yaml_path(), "up", "-d"], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "logs", ]) # when container is created, its command echoes 'ipamtest' # BUG: figure out why echo is called twice self.assertIn("ipamtest", str(output)) output, _ = self.run_subprocess_assert_returncode( [ "podman", "inspect", "ipam_default_testipam_1", ], ) network_info = json.loads(output.decode('utf-8'))[0] network_name = next(iter(network_info["NetworkSettings"]["Networks"].keys())) output, _ = self.run_subprocess_assert_returncode([ "podman", "network", "inspect", "{}".format(network_name), ]) network_info = json.loads(output.decode('utf-8'))[0] # bridge is the default network driver self.assertEqual(network_info['driver'], "bridge") self.assertEqual(network_info['ipam_options'], {'driver': 'host-local'}) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_multicompose.py000066400000000000000000000066371473727656100273730ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "multicompose"), "docker-compose.yml") class TestComposeMulticompose(unittest.TestCase, RunSubprocessMixin): def test_multicompose(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", os.path.join( os.path.join(test_path(), "multicompose"), "d1/docker-compose.yml" ), "-f", os.path.join( os.path.join(test_path(), "multicompose"), "d2/docker-compose.yml" ), "up", "-d", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", os.path.join(os.path.join(test_path(), "multicompose"), "d1/docker-compose.yml"), "-f", os.path.join(os.path.join(test_path(), "multicompose"), "d2/docker-compose.yml"), "ps", ]) self.assertIn(b"d1_web1_1", output) self.assertIn(b"d1_web2_1", output) output, _ = self.run_subprocess_assert_returncode([ "podman", "exec", "-ti", "d1_web1_1", "sh", "-c", "set", ]) # checks if `enf_file` was appended, not replaced # (which means that we normalize to array before merge) self.assertIn(b"var12='d1/12.env'", output) output, _ = self.run_subprocess_assert_returncode([ "podman", "exec", "-ti", "d1_web2_1", "sh", "-c", "set", ]) # checks if paths inside `d2/docker-compose.yml` directory are relative to `d1` self.assertIn(b"var2='d1/2.env'", output) output, _ = self.run_subprocess_assert_returncode([ "podman", "exec", "-ti", "d1_web1_1", "sh", "-c", "cat /var/www/html/index.txt", ]) self.assertIn(b"var1=d1/1.env", output) # check if project base directory and project name is d1 output, _ = self.run_subprocess_assert_returncode([ "podman", "exec", "-ti", "d1_web2_1", "sh", "-c", "cat /var/www/html/index.txt", ]) self.assertIn(b"var2=d1/2.env", output) finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", os.path.join(os.path.join(test_path(), "multicompose"), "d1/docker-compose.yml"), "-f", os.path.join(os.path.join(test_path(), "multicompose"), "d2/docker-compose.yml"), "down", "-t", "0", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_nethost.py000066400000000000000000000036131473727656100263260ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest import requests from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "nethost"), "docker-compose.yaml") class TestComposeNethost(unittest.TestCase, RunSubprocessMixin): # check if container listens for http requests and sends response back # as network_mode: host allows to connect to container easily def test_nethost(self): try: self.run_subprocess_assert_returncode( [podman_compose_path(), "-f", compose_yaml_path(), "up", "-d"], ) container_id, _ = self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "ps", "--format", '{{.ID}}', ], ) container_id = container_id.decode('utf-8').split('\n')[0] output, _ = self.run_subprocess_assert_returncode( [ "podman", "exec", "-it", container_id, "sh", "-c", "echo test_123 >> /tmp/test.txt", ], ) response = requests.get('http://localhost:8123/test.txt') self.assertEqual(response.ok, True) self.assertEqual(response.text, "test_123\n") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", "-t", "0", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_nets_test1.py000066400000000000000000000061441473727656100267350ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import json import os import unittest import requests from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "nets_test1"), "docker-compose.yml") class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin): # test if port mapping works as expected def test_nets_test1(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", "-d", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "ps", ]) self.assertIn(b"nets_test1_web1_1", output) self.assertIn(b"nets_test1_web2_1", output) response = requests.get('http://localhost:8001/index.txt') self.assertTrue(response.ok) self.assertEqual(response.text, "test1\n") response = requests.get('http://localhost:8002/index.txt') self.assertTrue(response.ok) self.assertEqual(response.text, "test2\n") # inspect 1st container output, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "nets_test1_web1_1", ]) container_info = json.loads(output.decode('utf-8'))[0] # check if network got default name self.assertEqual( list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default" ) # check if Host port is the same as provided by the service port self.assertEqual( container_info['NetworkSettings']["Ports"], {"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]}, ) self.assertEqual(container_info["Config"]["Hostname"], "web1") # inspect 2nd container output, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "nets_test1_web2_1", ]) container_info = json.loads(output.decode('utf-8'))[0] self.assertEqual( list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default" ) self.assertEqual( container_info['NetworkSettings']["Ports"], {"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]}, ) self.assertEqual(container_info["Config"]["Hostname"], "web2") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", "-t", "0", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_nets_test2.py000066400000000000000000000062471473727656100267420ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import json import os import unittest import requests from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin def compose_yaml_path(): return os.path.join(os.path.join(test_path(), "nets_test2"), "docker-compose.yml") class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin): # test if port mapping works as expected with networks top-level element def test_nets_test2(self): try: self.run_subprocess_assert_returncode( [ podman_compose_path(), "-f", compose_yaml_path(), "up", "-d", ], ) output, _ = self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "ps", ]) self.assertIn(b"nets_test2_web1_1", output) self.assertIn(b"nets_test2_web2_1", output) response = requests.get('http://localhost:8001/index.txt') self.assertTrue(response.ok) self.assertEqual(response.text, "test1\n") response = requests.get('http://localhost:8002/index.txt') self.assertTrue(response.ok) self.assertEqual(response.text, "test2\n") # inspect 1st container output, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "nets_test2_web1_1", ]) container_info = json.loads(output.decode('utf-8'))[0] # check if network got specific name from networks top-level element self.assertEqual( list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack" ) # check if Host port is the same as prodvided by the service port self.assertEqual( container_info['NetworkSettings']["Ports"], {"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]}, ) self.assertEqual(container_info["Config"]["Hostname"], "web1") # inspect 2nd container output, _ = self.run_subprocess_assert_returncode([ "podman", "inspect", "nets_test2_web2_1", ]) container_info = json.loads(output.decode('utf-8'))[0] self.assertEqual( list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack" ) self.assertEqual( container_info['NetworkSettings']["Ports"], {"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]}, ) self.assertEqual(container_info["Config"]["Hostname"], "web2") finally: self.run_subprocess_assert_returncode([ podman_compose_path(), "-f", compose_yaml_path(), "down", "-t", "0", ]) podman-compose-1.3.0/tests/integration/test_podman_compose_network_scoped_aliases.py000066400000000000000000000046611473727656100313750ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 # pylint: disable=redefined-outer-name import os import unittest from tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.test_utils import RunSubprocessMixin class TestPodmanComposeNetworkScopedAliases(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(), "network_scoped_aliases", "docker-compose.yaml") def test_network_scoped_aliases(self): try: self.up() self.verify() finally: self.down() def up(self): up_cmd = [ "coverage", "run", podman_compose_path(), "-f", self.compose_file(), "up", "-d", "--force-recreate", ] self.run_subprocess_assert_returncode(up_cmd) def down(self): down_cmd = [ "coverage", "run", podman_compose_path(), "-f", self.compose_file(), "kill", "-a", ] self.run_subprocess(down_cmd) def verify(self): expected_results = [ ("utils-net0", "web1", ["172.19.3.11"]), ("utils-net0", "secure-web", ["172.19.3.11"]), ("utils-net0", "insecure-web", []), ("utils-net1", "web1", ["172.19.4.11"]), ("utils-net1", "secure-web", []), ("utils-net1", "insecure-web", ["172.19.4.11"]), ] for utils, service, expected_result in expected_results: cmd = [ podman_compose_path(), "-f", self.compose_file(), "exec", utils, "nslookup", service, ] out, _, _ = self.run_subprocess(cmd) addresses = self.parse_dnslookup(out.decode()) self.assertEqual(addresses, expected_result) def parse_dnslookup(self, output): lines = output.splitlines() addresses = [] for line in lines: if line.startswith("Address"): addr = line.split(":", 1)[1].strip() if ":" not in addr: addresses.append(addr) return list(sorted(set(addresses))) podman-compose-1.3.0/tests/integration/test_podman_compose_networks.py000066400000000000000000000065321473727656100265210ustar00rootroot00000000000000# 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 tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.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.3.0/tests/integration/test_podman_compose_tests.py000066400000000000000000000141411473727656100260020ustar00rootroot00000000000000# 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 tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.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, ) def test_down_with_network(self): try: self.run_subprocess_assert_returncode([ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "network", "docker-compose.yml"), "up", "-d", ]) output, _, _ = self.run_subprocess(["podman", "network", "ls"]) self.assertIn("network_mystack", output.decode()) finally: self.run_subprocess_assert_returncode([ "coverage", "run", podman_compose_path(), "-f", os.path.join(test_path(), "network", "docker-compose.yml"), "down", ]) output, _, _ = self.run_subprocess(["podman", "network", "ls"]) self.assertNotIn("network_mystack", output.decode()) podman-compose-1.3.0/tests/integration/test_podman_compose_up_down.py000066400000000000000000000050651473727656100263200ustar00rootroot00000000000000# 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 tests.integration.test_podman_compose import podman_compose_path from tests.integration.test_podman_compose import test_path from tests.integration.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.3.0/tests/integration/test_utils.py000066400000000000000000000022741473727656100227210ustar00rootroot00000000000000# 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.3.0/tests/integration/testlogs/000077500000000000000000000000001473727656100220075ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/testlogs/docker-compose.yml000066400000000000000000000004341473727656100254450ustar00rootroot00000000000000version: "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.3.0/tests/integration/uidmaps/000077500000000000000000000000001473727656100216055ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/uidmaps/docker-compose.yml000066400000000000000000000003721473727656100252440ustar00rootroot00000000000000version: "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.3.0/tests/integration/ulimit/000077500000000000000000000000001473727656100214465ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/ulimit/Dockerfile000066400000000000000000000000561473727656100234410ustar00rootroot00000000000000FROM busybox COPY ./ulimit.sh /bin/ulimit.sh podman-compose-1.3.0/tests/integration/ulimit/docker-compose.yaml000066400000000000000000000011311473727656100252400ustar00rootroot00000000000000version: "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.3.0/tests/integration/ulimit/ulimit.sh000077500000000000000000000002451473727656100233110ustar00rootroot00000000000000#!/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.3.0/tests/integration/ulimit_build/000077500000000000000000000000001473727656100226255ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/ulimit_build/Dockerfile000066400000000000000000000001021473727656100246100ustar00rootroot00000000000000FROM busybox COPY ./ulimit.sh /bin/ulimit.sh RUN /bin/ulimit.sh podman-compose-1.3.0/tests/integration/ulimit_build/docker-compose.yaml000066400000000000000000000007641473727656100264320ustar00rootroot00000000000000version: "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.3.0/tests/integration/ulimit_build/ulimit.sh000077500000000000000000000002651473727656100244720ustar00rootroot00000000000000#!/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.3.0/tests/integration/vol/000077500000000000000000000000001473727656100207435ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/vol/README.md000066400000000000000000000002231473727656100222170ustar00rootroot00000000000000# 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.3.0/tests/integration/vol/docker-compose.yaml000066400000000000000000000024621473727656100245450ustar00rootroot00000000000000version: "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.3.0/tests/integration/volumes_merge/000077500000000000000000000000001473727656100230145ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/volumes_merge/docker-compose.override.yaml000066400000000000000000000003041473727656100304250ustar00rootroot00000000000000version: "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.3.0/tests/integration/volumes_merge/docker-compose.yaml000066400000000000000000000004761473727656100266210ustar00rootroot00000000000000version: "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.3.0/tests/integration/volumes_merge/index.txt000066400000000000000000000000421473727656100246600ustar00rootroot00000000000000The file from docker-compose.yaml podman-compose-1.3.0/tests/integration/volumes_merge/override.txt000066400000000000000000000000531473727656100253720ustar00rootroot00000000000000The file from docker-compose.override.yaml podman-compose-1.3.0/tests/integration/yamlmagic/000077500000000000000000000000001473727656100221065ustar00rootroot00000000000000podman-compose-1.3.0/tests/integration/yamlmagic/docker-compose.yml000066400000000000000000000007771473727656100255560ustar00rootroot00000000000000version: '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 podman-compose-1.3.0/tests/unit/000077500000000000000000000000001473727656100165775ustar00rootroot00000000000000podman-compose-1.3.0/tests/unit/__init__.py000066400000000000000000000000001473727656100206760ustar00rootroot00000000000000podman-compose-1.3.0/tests/unit/test_can_merge_build.py000066400000000000000000000155111473727656100233120ustar00rootroot00000000000000# 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.3.0/tests/unit/test_compose_exec_args.py000066400000000000000000000021371473727656100237000ustar00rootroot00000000000000# 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.3.0/tests/unit/test_compose_run_log_format.py000066400000000000000000000035771473727656100247660ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 # pylint: disable=protected-access import io import unittest from podman_compose import Podman class DummyReader: def __init__(self, data=[]): self.data = data async def readuntil(self, x): return self.data.pop(0) def at_eof(self): return len(self.data) == 0 class TestComposeRunLogFormat(unittest.IsolatedAsyncioTestCase): def setUp(self): self.p = get_minimal_podman() self.buffer = io.StringIO() async def test_single_line_single_chunk(self): reader = DummyReader([b'hello, world\n']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: hello, world\n') async def test_empty_line(self): reader = DummyReader([b'\n']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: \n') async def test_line_split(self): reader = DummyReader([b'hello,', b' world\n']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: hello, world\n') async def test_two_lines_in_one_chunk(self): reader = DummyReader([b'hello\nbye\n']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: hello\nLL: bye\n') async def test_double_blank(self): reader = DummyReader([b'hello\n\n\nbye\n']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: hello\nLL: \nLL: \nLL: bye\n') async def test_no_new_line_at_end(self): reader = DummyReader([b'hello\nbye']) await self.p._format_stream(reader, self.buffer, 'LL:') self.assertEqual(self.buffer.getvalue(), 'LL: hello\nLL: bye\n') def get_minimal_podman(): return Podman(None) podman-compose-1.3.0/tests/unit/test_compose_run_update_container_from_args.py000066400000000000000000000036251473727656100302120ustar00rootroot00000000000000# 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.3.0/tests/unit/test_container_to_args.py000066400000000000000000000431071473727656100237150ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest 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 = {} compose.x_podman = {} async def podman_output(*args, **kwargs): pass compose.podman.output = mock.Mock(side_effect=podman_output) return compose def get_minimal_container(): return { "name": "project_name_service_name1", "service_name": "service_name", "image": "busybox", } def get_test_file_path(rel_path): repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.realpath(os.path.join(repo_root, rel_path)) 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: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: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: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: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: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: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: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: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:alias=service_name", "--rootfs", "/path/to/rootfs", ], ) async def test_no_hosts_extension(self): c = create_compose_mock() cnt = get_minimal_container() cnt["x-podman.no_hosts"] = True args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--no-hosts", "busybox", ], ) async def test_env_file_str(self): c = create_compose_mock() cnt = get_minimal_container() env_file = get_test_file_path('tests/integration/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: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 = get_test_file_path('tests/integration/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:alias=service_name", "busybox", ], ) async def test_env_file_str_array_two_paths(self): c = create_compose_mock() cnt = get_minimal_container() env_file = get_test_file_path('tests/integration/env-file-tests/env-files/project-1.env') env_file_2 = get_test_file_path('tests/integration/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:alias=service_name", "busybox", ], ) async def test_env_file_obj_required(self): c = create_compose_mock() cnt = get_minimal_container() env_file = get_test_file_path('tests/integration/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: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: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: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: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: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: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:alias=service_name", "busybox", ], ) @parameterized.expand([ ("not_compat", False, "test_project_name", "test_project_name_network1"), ("compat_no_dash", True, "test_project_name", "test_project_name_network1"), ("compat_dash", True, "test_project-name", "test_projectname_network1"), ]) async def test_network_default_name(self, name, is_compat, project_name, expected_network_name): c = create_compose_mock(project_name) c.x_podman = {"default_net_name_compat": is_compat} c.networks = {'network1': {}} cnt = get_minimal_container() cnt['networks'] = ['network1'] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", f"--network={expected_network_name}:alias=service_name", "busybox", ], ) async def test_device(self): c = create_compose_mock() cnt = get_minimal_container() cnt['devices'] = ['/dev/ttyS0'] cnt['device_cgroup_rules'] = ['c 100:200 rwm'] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--device", "/dev/ttyS0", "--device-cgroup-rule", "c 100:200 rwm", "--network=bridge:alias=service_name", "busybox", ], ) podman-compose-1.3.0/tests/unit/test_container_to_args_secrets.py000066400000000000000000000304121473727656100254400ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import os import unittest from parameterized import parameterized from podman_compose import container_to_args from tests.unit.test_container_to_args import create_compose_mock from tests.unit.test_container_to_args import get_minimal_container def repo_root(): return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 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: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: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)) @parameterized.expand([ ( "secret_no_name", {"my_secret": "my_secret_name", "external": "true"}, {}, # must have a name ), ( "no_secret_name_in_declared_secrets", {}, # must have a name { "source": "my_secret_name", }, ), ( "secret_name_does_not_match_declared_secrets_name", { "wrong_name": "my_secret_name", }, { "source": "name", # secret name must match the one in declared_secrets }, ), ( "secret_name_empty_string", {"": "my_secret_name"}, { "source": "", # can not be empty string }, ), ]) async def test_secret_name(self, test_name, declared_secrets, add_to_minimal_container): c = create_compose_mock() c.declared_secrets = declared_secrets cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [add_to_minimal_container] with self.assertRaises(ValueError) as context: await container_to_args(c, cnt) self.assertIn('ERROR: undeclared secret: ', str(context.exception)) async def test_secret_string_no_external_name_in_declared_secrets(self): c = create_compose_mock() c.declared_secrets = {"my_secret_name": {"external": "true"}} cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ "my_secret_name", ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--secret", "my_secret_name", "busybox", ], ) async def test_secret_string_options_external_name_in_declared_secrets(self): c = create_compose_mock() c.declared_secrets = { "my_secret_name": { "external": "true", "name": "my_secret_name", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ { "source": "my_secret_name", "target": "my_secret_name", "uid": "103", "gid": "103", "mode": "400", } ] with self.assertLogs() as cm: args = await container_to_args(c, cnt) self.assertEqual(len(cm.output), 1) self.assertIn('That is un-supported and a no-op and is ignored.', cm.output[0]) self.assertIn('my_secret_name', cm.output[0]) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--secret", "my_secret_name,uid=103,gid=103,mode=400", "busybox", ], ) async def test_secret_string_external_name_in_declared_secrets_does_not_match_secret(self): c = create_compose_mock() c.declared_secrets = { "my_secret_name": { "external": "true", "name": "wrong_secret_name", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ "my_secret_name", ] with self.assertRaises(ValueError) as context: await container_to_args(c, cnt) self.assertIn('ERROR: Custom name/target reference ', str(context.exception)) async def test_secret_target_does_not_match_secret_name_secret_type_not_env(self): c = create_compose_mock() c.declared_secrets = { "my_secret_name": { "external": "true", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ { "source": "my_secret_name", "target": "does_not_equal_secret_name", "type": "does_not_equal_env", } ] with self.assertRaises(ValueError) as context: await container_to_args(c, cnt) self.assertIn('ERROR: Custom name/target reference ', str(context.exception)) async def test_secret_target_does_not_match_secret_name_secret_type_env(self): c = create_compose_mock() c.declared_secrets = { "my_secret_name": { "external": "true", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ {"source": "my_secret_name", "target": "does_not_equal_secret_name", "type": "env"} ] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--secret", "my_secret_name,type=env,target=does_not_equal_secret_name", "busybox", ], ) async def test_secret_target_matches_secret_name_secret_type_not_env(self): c = create_compose_mock() c.declared_secrets = { "my_secret_name": { "external": "true", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ {"source": "my_secret_name", "target": "my_secret_name", "type": "does_not_equal_env"} ] with self.assertLogs() as cm: args = await container_to_args(c, cnt) self.assertEqual(len(cm.output), 1) self.assertIn('That is un-supported and a no-op and is ignored.', cm.output[0]) self.assertIn('my_secret_name', cm.output[0]) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--secret", "my_secret_name,type=does_not_equal_env", "busybox", ], ) @parameterized.expand([ ( "no_secret_target", { "file_secret": { "file": "./my_secret", } }, "file_secret", repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind", ), ( "custom_target_name", { "file_secret": { "file": "./my_secret", } }, { "source": "file_secret", "target": "custom_name", }, repo_root() + "/test_dirname/my_secret:/run/secrets/custom_name:ro,rprivate,rbind", ), ( "no_custom_target_name", { "file_secret": { "file": "./my_secret", } }, { "source": "file_secret", }, repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind", ), ( "custom_location", { "file_secret": { "file": "./my_secret", } }, { "source": "file_secret", "target": "/etc/custom_location", }, repo_root() + "/test_dirname/my_secret:/etc/custom_location:ro,rprivate,rbind", ), ]) async def test_file_secret( self, test_name, declared_secrets, add_to_minimal_container, expected_volume_ref ): c = create_compose_mock() c.declared_secrets = declared_secrets cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [add_to_minimal_container] args = await container_to_args(c, cnt) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--volume", expected_volume_ref, "busybox", ], ) async def test_file_secret_unused_params_warning(self): c = create_compose_mock() c.declared_secrets = { "file_secret": { "file": "./my_secret", } } cnt = get_minimal_container() cnt["_service"] = "test-service" cnt["secrets"] = [ { "source": "file_secret", "target": "unused_params_warning", "uid": "103", "gid": "103", "mode": "400", } ] with self.assertLogs() as cm: args = await container_to_args(c, cnt) self.assertEqual(len(cm.output), 1) self.assertIn('with uid, gid, or mode.', cm.output[0]) self.assertIn('unused_params_warning', cm.output[0]) self.assertEqual( args, [ "--name=project_name_service_name1", "-d", "--network=bridge:alias=service_name", "--volume", repo_root() + "/test_dirname/my_secret:/run/secrets/unused_params_warning:ro,rprivate,rbind", "busybox", ], ) podman-compose-1.3.0/tests/unit/test_container_to_build_args.py000066400000000000000000000100321473727656100250630ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 import unittest from unittest import mock from podman_compose import container_to_build_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 = {} compose.x_podman = {} return compose def get_minimal_container(): return { 'name': 'project_name_service_name1', 'service_name': 'service_name', 'image': 'new-image', 'build': {}, } def get_minimal_args(): args = mock.Mock() args.build_arg = [] return args class TestContainerToBuildArgs(unittest.TestCase): def test_minimal(self): c = create_compose_mock() cnt = get_minimal_container() args = get_minimal_args() args = container_to_build_args(c, cnt, args, lambda path: True) self.assertEqual( args, [ '-f', './Containerfile', '-t', 'new-image', '--no-cache', '--pull-always', '.', ], ) def test_platform(self): c = create_compose_mock() cnt = get_minimal_container() cnt['platform'] = 'linux/amd64' args = get_minimal_args() args = container_to_build_args(c, cnt, args, lambda path: True) self.assertEqual( args, [ '-f', './Containerfile', '-t', 'new-image', '--platform', 'linux/amd64', '--no-cache', '--pull-always', '.', ], ) def test_tags(self): c = create_compose_mock() cnt = get_minimal_container() cnt['build']['tags'] = ['some-tag1', 'some-tag2:2'] args = get_minimal_args() args = container_to_build_args(c, cnt, args, lambda path: True) self.assertEqual( args, [ '-f', './Containerfile', '-t', 'new-image', '-t', 'some-tag1', '-t', 'some-tag2:2', '--no-cache', '--pull-always', '.', ], ) def test_labels(self): c = create_compose_mock() cnt = get_minimal_container() cnt['build']['labels'] = ['some-label1', 'some-label2.2'] args = get_minimal_args() args = container_to_build_args(c, cnt, args, lambda path: True) self.assertEqual( args, [ '-f', './Containerfile', '-t', 'new-image', '--label', 'some-label1', '--label', 'some-label2.2', '--no-cache', '--pull-always', '.', ], ) def test_caches(self): c = create_compose_mock() cnt = get_minimal_container() cnt['build']['cache_from'] = ['registry/image1', 'registry/image2'] cnt['build']['cache_to'] = ['registry/image3', 'registry/image4'] args = get_minimal_args() args = container_to_build_args(c, cnt, args, lambda path: True) self.assertEqual( args, [ '-f', './Containerfile', '-t', 'new-image', '--no-cache', '--pull-always', '--cache-from', 'registry/image1', '--cache-from', 'registry/image2', '--cache-to', 'registry/image3', '--cache-to', 'registry/image4', '.', ], ) podman-compose-1.3.0/tests/unit/test_get_net_args.py000066400000000000000000000271761473727656100226660ustar00rootroot00000000000000import unittest from parameterized import parameterized from podman_compose import get_net_args from tests.unit.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, } if num_networks == 1: compose.default_net = "net0" 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 = [ f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_default_net_is_None(self): compose = get_networked_compose() container = get_minimal_container() mac_address = "11:22:33:44:55:66" container["mac_address"] = mac_address compose.default_net = None expected_args = [ f"--network=bridge:alias={SERVICE_NAME},mac={mac_address}", ] 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: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:alias={SERVICE_NAME},alias=alias1,alias=alias2", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) def test_aliases_on_network_scope(self): compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {"aliases": ["alias1"]}} expected_args = [ f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME},alias=alias1", ] 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:ip={ip},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:ip6={ipv6_address},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:mac={mac},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},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @parameterized.expand([ "mac_address", "x-podman.mac_address", ]) def test_mac_on_network(self, mac_attr): mac = "00:11:22:33:44:55" compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {mac_attr: mac}} expected_args = [ f"--network={PROJECT_NAME}_net0:mac={mac},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:alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1: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:alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1: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},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1:ip={ip1},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:ip6={ip0},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1:ip6={ip1},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},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1:mac={mac1},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 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},ip6={ip6_0},mac={mac},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net1:ip={ip4_1},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net2:ip6={ip6_2},alias={SERVICE_NAME}", f"--network={PROJECT_NAME}_net3:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @parameterized.expand([ ("bridge", [f"--network=bridge:alias={SERVICE_NAME},mac=11:22:33:44:55:66"]), ( "bridge:ip=10.88.0.3", [f"--network=bridge:ip=10.88.0.3,alias={SERVICE_NAME},mac=11:22:33:44:55:66"], ), ("host", ["--network=host"]), ("none", ["--network=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 mac_address = "11:22:33:44:55:66" container["network_mode"] = network_mode container["mac_address"] = mac_address 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.3.0/tests/unit/test_get_network_create_args.py000066400000000000000000000136651473727656100251120ustar00rootroot00000000000000import 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.3.0/tests/unit/test_normalize_depends_on.py000066400000000000000000000025111473727656100244050ustar00rootroot00000000000000import 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.3.0/tests/unit/test_normalize_final_build.py000066400000000000000000000207151473727656100245450ustar00rootroot00000000000000# 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}, }, ), ( {"build": "../relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "../relative")), }, }, ), ( {"build": "./relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "./relative")), }, }, ), ( {"build": "/workspace/absolute"}, { "build": { "context": "/workspace/absolute", }, }, ), ( { "build": { "dockerfile": "Dockerfile", }, }, { "build": { "context": cwd, "dockerfile": "Dockerfile", }, }, ), ( { "build": { "context": ".", }, }, { "build": { "context": cwd, }, }, ), ( { "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}}, ), ( {"build": "."}, {}, {"build": {"context": cwd}}, ), ( {"build": "/workspace/absolute"}, {"build": "./relative"}, { "build": { "context": os.path.normpath(os.path.join(cwd, "./relative")), } }, ), ( {"build": "./relative"}, {"build": "/workspace/absolute"}, {"build": {"context": "/workspace/absolute"}}, ), ( {"build": "./relative"}, {"build": "/workspace/absolute"}, {"build": {"context": "/workspace/absolute"}}, ), ( {"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.3.0/tests/unit/test_normalize_service.py000066400000000000000000000052021473727656100237270ustar00rootroot00000000000000# 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.3.0/tests/unit/test_rec_subs.py000066400000000000000000000037431473727656100220240ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-2.0 # pylint: disable=protected-access import unittest from parameterized import parameterized from podman_compose import rec_subs class TestRecSubs(unittest.TestCase): substitutions = [ # dict with environment variables ( "service's environment is low priority", {"environment": {"v1": "low priority", "actual-v1": "$v1"}}, {"environment": {"v1": "low priority", "actual-v1": "high priority"}}, ), ( "service's environment can be used in other values", {"environment": {"v100": "v1.0.0", "image": "abc:$v100"}}, {"environment": {"v100": "v1.0.0", "image": "abc:v1.0.0"}}, ), ( "Non-variable should not be substituted", {"environment": {"non_var": "$$v1", "vx": "$non_var"}, "image": "abc:$non_var"}, {"environment": {"non_var": "$v1", "vx": "$v1"}, "image": "abc:$v1"}, ), # list ( "Values in list are substituted", ["$v1", "low priority"], ["high priority", "low priority"], ), # str ( "Value with ${VARIABLE} format", "${v1}", "high priority", ), ( "Value with ${VARIABLE:-default} format", ["${v1:-default}", "${empty:-default}", "${not_exits:-default}"], ["high priority", "default", "default"], ), ( "Value with ${VARIABLE-default} format", ["${v1-default}", "${empty-default}", "${not_exits-default}"], ["high priority", "", "default"], ), ( "Value $$ means $", "$$v1", "$v1", ), ] @parameterized.expand(substitutions) def test_rec_subs(self, desc, input, expected): sub_dict = {"v1": "high priority", "empty": ""} result = rec_subs(input, sub_dict) self.assertEqual(result, expected, msg=desc) podman-compose-1.3.0/tests/unit/test_volumes.py000066400000000000000000000010301473727656100216740ustar00rootroot00000000000000# 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", }, }, )