diff --git a/.github/workflows/basic-ci.yaml b/.github/workflows/basic-ci.yaml
index db169448..57cd2047 100644
--- a/.github/workflows/basic-ci.yaml
+++ b/.github/workflows/basic-ci.yaml
@@ -4,25 +4,52 @@ on: [push, pull_request]
jobs:
basic_ci:
name: Basic CI
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
- python-version: [3.8]
+ python-version: ['3.8', 3.x]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- - name: Install Dependencies And Self
+ - name: Install Test Dependencies And Self
run: |
python -m pip install --upgrade pip setuptools wheel
- # Workaround for https://github.com/docker/docker-py/issues/2807
- python -m pip install six
python -m pip install -e .[test] codecov pytest-cov
- name: Run headless tests
- uses: GabrielBB/xvfb-action@v1
+ uses: coactions/setup-xvfb@v1
with:
run: python -m pytest -s -v --cov=rocker -m "not nvidia"
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
+ cli_smoke_tests:
+ name: CLI smoke tests
+ runs-on: ubuntu-22.04
+ strategy:
+ matrix:
+ python-version: [3.8, '3.x']
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install Dependencies And Self
+ run: |
+ python -m pip install --upgrade pip setuptools wheel
+ python -m pip install -e .
+ - name: Check os_detector script
+ run: |
+ detect_docker_image_os ubuntu
+ - name: Check detector help
+ run: |
+ detect_docker_image_os ubuntu -h
+ - name: Check main runs
+ run: |
+ rocker ubuntu 'true'
+ - name: Check rocker help
+ run: |
+ rocker -h
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..9fdcaf35
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,324 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+
+## [v0.2.18](https://github.com/osrf/rocker/releases/tag/v0.2.18) - 2025-01-10
+
+[Compare with v0.2.17](https://github.com/osrf/rocker/compare/v0.2.17...v0.2.18)
+
+### Added
+
+- Add UI feedback that image is being cleaned up, and how to avoid. ([3b21f78](https://github.com/osrf/rocker/commit/3b21f78fd4372a5451611a2a3e607f6941bcc398) by Tully Foote).
+- Add clear_image to ImageGenerator to enable non-persistence and not taking up all disk space. Especially for the tests. ([24dd5e1](https://github.com/osrf/rocker/commit/24dd5e1e7a6387e6c9b6c1df3644bf493f04b5c5) by Tully Foote).
+- Add support for ulimit flag (#291) ([cff5cb2](https://github.com/osrf/rocker/commit/cff5cb27c04f4db8d115493c2e2704c6a10726df) by Felipe Padula Sanches).
+- Add support for --shm-size flag (#306)
+
+### Fixed
+
+- fix test for new cuda installation package ([3449afa](https://github.com/osrf/rocker/commit/3449afabd700c4723d21c22163e73f9cbf9b358d) by Tully Foote).
+- Removed deprecation warning from volume extensino (#292)
+- Updated and simplified CUDA installation (#299 by jonazpiazu)
+
+### Removed
+
+- Remove default value for defaults (#289) ([3f02bdc](https://github.com/osrf/rocker/commit/3f02bdcc542c786eb08bdb3b324887bfa21698a2) by Tully Foote).
+
+## [v0.2.17](https://github.com/osrf/rocker/releases/tag/v0.2.17) - 2024-08-28
+
+[Compare with v0.2.16](https://github.com/osrf/rocker/compare/v0.2.16...v0.2.17)
+
+### Added
+- [nvidia_extension] add 24.04 to supported_versions (#279)
+
+## [v0.2.16](https://github.com/osrf/rocker/releases/tag/v0.2.16) - 2024-04-01
+
+[Compare with v0.2.15](https://github.com/osrf/rocker/compare/v0.2.15...v0.2.16)
+
+### Fixed
+
+- Fix --user arg when using empy 4 (#276) ([4078d22](https://github.com/osrf/rocker/commit/4078d223a2158e9a6fea31d8871dc7df7ee094f0) by Gary Servin).
+
+## [v0.2.15](https://github.com/osrf/rocker/releases/tag/v0.2.15) - 2024-03-01
+
+[Compare with v0.2.14](https://github.com/osrf/rocker/compare/v0.2.14...v0.2.15)
+
+### Added
+
+- Add changelog ([39a2842](https://github.com/osrf/rocker/commit/39a2842d03fec73b4b0d70c3922c4ab1c93c2cc0) by Tully Foote).
+- Add a shorthand option for switching to alternate rmws (#268) ([35e68c1](https://github.com/osrf/rocker/commit/35e68c17232a50cfb3a781b066e2c0d6691cb07e) by Tully Foote).
+
+## [v0.2.14](https://github.com/osrf/rocker/releases/tag/v0.2.14) - 2024-01-26
+
+[Compare with v0.2.13](https://github.com/osrf/rocker/compare/v0.2.13...v0.2.14)
+
+### Removed
+
+- Remove deprecated use of pkg_resources (#265) ([998761f](https://github.com/osrf/rocker/commit/998761f919302f29db2377f53efb5cb472c335fa) by Tully Foote).
+
+## [v0.2.13](https://github.com/osrf/rocker/releases/tag/v0.2.13) - 2023-12-10
+
+[Compare with v0.2.12](https://github.com/osrf/rocker/compare/v0.2.12...v0.2.13)
+
+### Added
+
+- Add indirection for changes in empy 3 vs 4 (#261) ([4de56c1](https://github.com/osrf/rocker/commit/4de56c18cdac09f3a9ec62a6b81cca7a4cdd46aa) by Tully Foote).
+- add simple test of CLI (#256) ([6d276a4](https://github.com/osrf/rocker/commit/6d276a4fe965f50246e2ff5eb0a03c9f16ed06c8) by Tully Foote).
+- Add extension ordering (#242) ([3afa6ac](https://github.com/osrf/rocker/commit/3afa6acbf6b0323f540ac16c2a0091b29099086a) by agyoungs).
+
+### Fixed
+
+- fix: nvidia arg (#234) ([2b8d5ab](https://github.com/osrf/rocker/commit/2b8d5abb18c829d22f290679b739dc53765198ad) by Amadeusz Szymko).
+- Fix nvidia runtime fail on arm64 & add --group-add plugin (#211) ([e01d9cc](https://github.com/osrf/rocker/commit/e01d9cca8672e0a85b7ff42118ee456c4c414d9e) by Amadeusz Szymko).
+
+### Removed
+
+- Remove old tutorial links (#254) ([41502b3](https://github.com/osrf/rocker/commit/41502b3f41e7d36bc108d9685bce61b6d27286bc) by Tully Foote).
+
+## [v0.2.12](https://github.com/osrf/rocker/releases/tag/v0.2.12) - 2023-05-04
+
+[Compare with v0.2.11](https://github.com/osrf/rocker/compare/v0.2.11...v0.2.12)
+
+### Fixed
+
+- Fix default logic for user-preserve-groups (#224) ([04cfc29](https://github.com/osrf/rocker/commit/04cfc290351a60b7df28c7eff67dc2c27e9ab918) by Tully Foote).
+
+## [v0.2.11](https://github.com/osrf/rocker/releases/tag/v0.2.11) - 2023-05-04
+
+[Compare with v0.2.10](https://github.com/osrf/rocker/compare/v0.2.10...v0.2.11)
+
+### Added
+
+- adding debian bookworm support ([b00c1c6](https://github.com/osrf/rocker/commit/b00c1c6e2832c6d375f8b12bcee8fe74642ef043) by Tully Foote).
+- add error messages for group issues ([4ce4e29](https://github.com/osrf/rocker/commit/4ce4e29041ac81830f78e1b00afbff9168194929) by Tully Foote).
+- Adding a permissive option to user-preserve-groups incase there are groups on the host that aren't permissible on the target but you'd like best-effort. ([44f7946](https://github.com/osrf/rocker/commit/44f7946653f1d58cbf52a088f8c482090f5f033c) by Tully Foote).
+- add link to mp_rocker (#213) ([49a23e7](https://github.com/osrf/rocker/commit/49a23e70a3055898daba80fc35f1742dc01d9a09) by Tully Foote).
+- Add ability to preserve host user groups inside container ([b16136e](https://github.com/osrf/rocker/commit/b16136e589ace3519e4b5c85697e770b29a151fa) by Miguel Prada).
+- Add port and expose extensions with tests (#201) ([2770045](https://github.com/osrf/rocker/commit/27700451d0b1a0701d8fe5c80c64a52942b61ce0) by Will Baker).
+- Added note for linking Intel Xe cards with rocker (#190) ([3a2bf74](https://github.com/osrf/rocker/commit/3a2bf74209f577f666857054095a57bbeb3c6c1d) by Zahi Kakish).
+
+### Removed
+
+- Removed '-v' for rocker's version. Only --version should be available. (#205) ([67a4142](https://github.com/osrf/rocker/commit/67a414278c5940d587b9e3060eb7ab9e2e293510) by George Stavrinos).
+
+## [v0.2.10](https://github.com/osrf/rocker/releases/tag/v0.2.10) - 2022-07-30
+
+[Compare with v0.2.9](https://github.com/osrf/rocker/compare/v0.2.9...v0.2.10)
+
+## [v0.2.9](https://github.com/osrf/rocker/releases/tag/v0.2.9) - 2022-03-23
+
+[Compare with v0.2.8](https://github.com/osrf/rocker/compare/v0.2.8...v0.2.9)
+
+## [v0.2.8](https://github.com/osrf/rocker/releases/tag/v0.2.8) - 2022-02-14
+
+[Compare with v0.2.7](https://github.com/osrf/rocker/compare/v0.2.7...v0.2.8)
+
+## [v0.2.7](https://github.com/osrf/rocker/releases/tag/v0.2.7) - 2021-12-01
+
+[Compare with v0.2.6](https://github.com/osrf/rocker/compare/v0.2.6...v0.2.7)
+
+### Added
+
+- add an option to preserve the home directory instead of deleting it if the uid or username collide (#164) ([0f1c7d1](https://github.com/osrf/rocker/commit/0f1c7d19da389be5d37977d2e4c8b87a4c5fe1e2) by Tully Foote).
+
+## [v0.2.6](https://github.com/osrf/rocker/releases/tag/v0.2.6) - 2021-10-05
+
+[Compare with v0.2.5](https://github.com/osrf/rocker/compare/v0.2.5...v0.2.6)
+
+## [v0.2.5](https://github.com/osrf/rocker/releases/tag/v0.2.5) - 2021-10-05
+
+[Compare with v0.2.4](https://github.com/osrf/rocker/compare/v0.2.4...v0.2.5)
+
+### Added
+
+- Add an option to tag the image (#159) ([ef09014](https://github.com/osrf/rocker/commit/ef0901419dd99132bef5c1b797892927e63f5925) by Tully Foote).
+
+## [v0.2.4](https://github.com/osrf/rocker/releases/tag/v0.2.4) - 2021-08-03
+
+[Compare with 0.2.3](https://github.com/osrf/rocker/compare/0.2.3...v0.2.4)
+
+### Added
+
+- add an action to mirror main to master Providing backwards compatibility for renaming master to main ([9d7a392](https://github.com/osrf/rocker/commit/9d7a3924cd0d9a4c45ea163a7ab5296e4cdfdd3c) by Tully Foote).
+- Add the privileged extension (#132) ([4214d73](https://github.com/osrf/rocker/commit/4214d73aeb3de235153905344d14ee18edc3c229) by Gaël Écorchard).
+- Add --nocleanup option (#111) ([5507dc5](https://github.com/osrf/rocker/commit/5507dc510c3e339beb3573e52dcc77d8585bb285) by Tim Redick).
+
+### Fixed
+
+- Fix detector for the deprecated distro syntax (#156) ([2760838](https://github.com/osrf/rocker/commit/2760838dc4bc8c3bd436344ec8ab9225ad914696) by Tully Foote).
+- Fix os detection for non-root images (cont.) (#150) ([8ecaf53](https://github.com/osrf/rocker/commit/8ecaf530cc374b30be121eef7a324b8200788eea) by Miguel Prada).
+
+### Removed
+
+- Remove intermediate containers and name os_detect images (#133) ([80602eb](https://github.com/osrf/rocker/commit/80602eb6868a9044279d88c13650828f654a32f9) by Daniel Stonier).
+- Remove redundant device additions to docker_args ([a759fe6](https://github.com/osrf/rocker/commit/a759fe6b241edf37b95d104f238eb6ef9fb3d70b) by Peter Polidoro).
+
+## [0.2.3](https://github.com/osrf/rocker/releases/tag/0.2.3) - 2020-11-25
+
+[Compare with v0.2.2](https://github.com/osrf/rocker/compare/v0.2.2...0.2.3)
+
+### Added
+
+- add x11 flags to examples (#104) ([ae7d3f6](https://github.com/osrf/rocker/commit/ae7d3f6edcd74cb74b345f005f401061b33bda3a) by Tully Foote).
+- add the ability to inject an arbitrary file which can be used via a COPY command in the snippet (#101) ([eef5516](https://github.com/osrf/rocker/commit/eef5516e65a992832be6d30a9c12459e582cba7c) by Tully Foote).
+- Added --name argument ([019a543](https://github.com/osrf/rocker/commit/019a543afb759664751afe847162ab9ba09db889) by ahcorde).
+
+### Fixed
+
+- Fix using rocker for non-root containers (#50) ([3585298](https://github.com/osrf/rocker/commit/35852984cc4554bbfcf55f3612fa17cc89d5d715) by Johannes Meyer).
+
+## [v0.2.2](https://github.com/osrf/rocker/releases/tag/v0.2.2) - 2020-06-24
+
+[Compare with v0.2.1](https://github.com/osrf/rocker/compare/v0.2.1...v0.2.2)
+
+## [v0.2.1](https://github.com/osrf/rocker/releases/tag/v0.2.1) - 2020-06-24
+
+[Compare with v0.2.0](https://github.com/osrf/rocker/compare/v0.2.0...v0.2.1)
+
+### Added
+
+- add bullseye target too ([b9f407b](https://github.com/osrf/rocker/commit/b9f407ba5423c61bd9dd0a46f36d21cd7e795adf) by Tully Foote).
+- add focal as a target ([55b704f](https://github.com/osrf/rocker/commit/55b704f575dc5f6af1150e29fb18faf51cfa3336) by Tully Foote).
+
+## [v0.2.0](https://github.com/osrf/rocker/releases/tag/v0.2.0) - 2020-05-08
+
+[Compare with v0.1.10](https://github.com/osrf/rocker/compare/v0.1.10...v0.2.0)
+
+### Added
+
+- Add support for focal and buster (#83) ([a76acb2](https://github.com/osrf/rocker/commit/a76acb2aafb1c5d5609e923b42d8d2a351e28906) by Tully Foote).
+- Add support for --env-file option pass through (#82) ([135e73f](https://github.com/osrf/rocker/commit/135e73fc11cb1a6751cda6253efa26d543345e47) by Tully Foote).
+- Add an Extension Manager class (#77) ([413d25a](https://github.com/osrf/rocker/commit/413d25a7e578d4eab410eda99042cabb8dc8026b) by Tully Foote).
+- add codeowners so I get auto review requested (#74) ([15b604b](https://github.com/osrf/rocker/commit/15b604be5d1a940e4e242dbebff45d9298fcb787) by Tully Foote).
+
+### Fixed
+
+- Fix documentation for User extension (#81) ([6e9ee9c](https://github.com/osrf/rocker/commit/6e9ee9c96aa2664b51974724377cd771c465dbbf) by Emerson Knapp).
+- Fix import paths (#76) ([d26004b](https://github.com/osrf/rocker/commit/d26004b2dcc919bf08ae7aeeef0e669f0a66a7f4) by Tully Foote).
+
+## [v0.1.10](https://github.com/osrf/rocker/releases/tag/v0.1.10) - 2019-12-05
+
+[Compare with v0.1.9](https://github.com/osrf/rocker/compare/v0.1.9...v0.1.10)
+
+## [v0.1.9](https://github.com/osrf/rocker/releases/tag/v0.1.9) - 2019-10-14
+
+[Compare with v0.1.8](https://github.com/osrf/rocker/compare/v0.1.8...v0.1.9)
+
+### Added
+
+- Add verbosity outputs ([6246570](https://github.com/osrf/rocker/commit/6246570e1630592c9426e64c7477d53339edc568) by Tully Foote).
+
+### Removed
+
+- Remove xvfb-run which started failing CI. (#69) ([d38927a](https://github.com/osrf/rocker/commit/d38927ab13ffe6ccf84bb9408261feb6a3df3231) by Tully Foote).
+
+## [v0.1.8](https://github.com/osrf/rocker/releases/tag/v0.1.8) - 2019-09-18
+
+[Compare with v0.1.7](https://github.com/osrf/rocker/compare/v0.1.7...v0.1.8)
+
+### Added
+
+- Add a version cli option (#65) ([28fc9ff](https://github.com/osrf/rocker/commit/28fc9ff1fab435ab71c1077ac6882380d38d66f1) by Tully Foote).
+
+### Fixed
+
+- FIx the window resize (#66) ([7757df0](https://github.com/osrf/rocker/commit/7757df091bee54714368a2d08236f6ba9caf4bc5) by Tully Foote).
+
+## [v0.1.7](https://github.com/osrf/rocker/releases/tag/v0.1.7) - 2019-09-17
+
+[Compare with v0.1.6](https://github.com/osrf/rocker/compare/v0.1.6...v0.1.7)
+
+### Added
+
+- add --home (#64) ([733e29b](https://github.com/osrf/rocker/commit/733e29b7521a4037ba381d3d219cabaf3855a608) by Tully Foote).
+- Add sigwinch passthrough for xterm resizing (#57) ([efec0f9](https://github.com/osrf/rocker/commit/efec0f964a12b239f6c201498b2aa478fe56d83c) by Ruffin).
+- Add new extensions git and ssh (#58) ([5405f3f](https://github.com/osrf/rocker/commit/5405f3fc824b3948772c6b60efff4370954497d3) by Johannes Meyer).
+
+### Fixed
+
+- Fix sudo with user extension for some usernames (#55) ([0bc20b8](https://github.com/osrf/rocker/commit/0bc20b84c72d16b5f81dda59283c6e242e203938) by Johannes Meyer).
+
+## [v0.1.6](https://github.com/osrf/rocker/releases/tag/v0.1.6) - 2019-08-29
+
+[Compare with 0.1.5](https://github.com/osrf/rocker/compare/0.1.5...v0.1.6)
+
+## [0.1.5](https://github.com/osrf/rocker/releases/tag/0.1.5) - 2019-08-28
+
+[Compare with 0.1.4](https://github.com/osrf/rocker/compare/0.1.4...0.1.5)
+
+### Added
+
+- Add extension to pass custom environment variables (#51) ([e27cc0b](https://github.com/osrf/rocker/commit/e27cc0bb8d6677807a9c791470b023253569fedf) by Johannes Meyer).
+- Add Cosmic, Disco, and Eoan suites. ([daf23a7](https://github.com/osrf/rocker/commit/daf23a7ca4f10e557e1dcdf9edb854b83d2186bf) by Steven! Ragnarök).
+
+### Fixed
+
+- Fix OS detection (fix #43) ([7a419f9](https://github.com/osrf/rocker/commit/7a419f91b0e8aa0fb0beae040844b4ed77e3f7a1) by Johannes Meyer).
+
+### Removed
+
+- remove unused imports ([39118bb](https://github.com/osrf/rocker/commit/39118bb877553364f70e082be24aaf07559f7ef4) by Tully Foote).
+
+## [0.1.4](https://github.com/osrf/rocker/releases/tag/0.1.4) - 2019-03-13
+
+[Compare with 0.1.3](https://github.com/osrf/rocker/compare/0.1.3...0.1.4)
+
+### Added
+
+- add documentation about how to use an intel integrated graphics card ([f03299f](https://github.com/osrf/rocker/commit/f03299ffddeb3d050f1f16cb048e2a9983895d90) by Tully Foote).
+- Add No-Python2 flag ([101813f](https://github.com/osrf/rocker/commit/101813fa2fdf75d80e628ee0871d8cb238e11337) by Tully Foote).
+- adding coverage of nvidia ([6f8121f](https://github.com/osrf/rocker/commit/6f8121f7b5c5aad379708ea468c01b97671af159) by Tully Foote).
+- add coverage for extensions (#32) ([ac3afc5](https://github.com/osrf/rocker/commit/ac3afc5592771e45f866f22ba4357346f4353db4) by Tully Foote).
+- adding a few basic unit tests (#30) ([ea951b7](https://github.com/osrf/rocker/commit/ea951b77cfaae6fd6921555779bfb195c748402e) by Tully Foote).
+- Add codecov reports (#28) ([9ef5f36](https://github.com/osrf/rocker/commit/9ef5f361cdd94f1361a48c80deae48b5a58dea15) by Tully Foote).
+- adding basic travis (#27) ([b538350](https://github.com/osrf/rocker/commit/b538350fcef5cc08eba4ad39a0911127270f9a20) by Tully Foote).
+- Add unit tests for os_detection ([4b42a2d](https://github.com/osrf/rocker/commit/4b42a2de5d5dd9df7f8af6fed17c0080fdc4cbe8) by Tully Foote).
+- add dependencies to backport script ([0df081e](https://github.com/osrf/rocker/commit/0df081efe22e8c4853c4bc1b942d7a2fe89aba9a) by Tully Foote).
+
+### Fixed
+
+- Fix empy stdout proxy logic for unit tests When the test runner changes std out for logging it breaks empy's stdout proxy logic. Fixes #9 ([0671e14](https://github.com/osrf/rocker/commit/0671e1429c0d54dba477bf475cae35252226c5f6) by Tully Foote).
+
+## [0.1.3](https://github.com/osrf/rocker/releases/tag/0.1.3) - 2019-01-10
+
+[Compare with 0.1.2](https://github.com/osrf/rocker/compare/0.1.2...0.1.3)
+
+### Added
+
+- add comment about python3-distro ([e28b546](https://github.com/osrf/rocker/commit/e28b54661dcd2c3fbc08f677d228c2ce7e649e3e) by Tully Foote).
+
+### Fixed
+
+- fix dependencies ([fafcab5](https://github.com/osrf/rocker/commit/fafcab54b24ec6cf0aa92b3adad3a16057717829) by Tully Foote).
+
+## [0.1.2](https://github.com/osrf/rocker/releases/tag/0.1.2) - 2019-01-09
+
+[Compare with first commit](https://github.com/osrf/rocker/compare/abc236cbf234c8ac7bff30b865836aefb751dbef...0.1.2)
+
+### Added
+
+- add command explicitly to argument parsing ([5e308a6](https://github.com/osrf/rocker/commit/5e308a61443c721f4a0de9129040f9898f538a04) by Tully Foote).
+- Add tests for extensions ([50ac127](https://github.com/osrf/rocker/commit/50ac1278a488049ae6ce4325356151e24f4df4b7) by Tully Foote).
+- add a few ignores core cleaner workspace diffs ([b498f03](https://github.com/osrf/rocker/commit/b498f031e50acff8c5e9d62c2e41365b289dd808) by Tully Foote).
+- Add stdeb.cfg ([034abf1](https://github.com/osrf/rocker/commit/034abf1e41ed0ea0c6c5abf2703377257a660e24) by Tully Foote).
+- add support for mounting devices, with a soft fail on not existing ([d3244ab](https://github.com/osrf/rocker/commit/d3244ab8f788bbc1e4355000e99176502d7f7d47) by Tully Foote).
+- adding copyright headers ([601a156](https://github.com/osrf/rocker/commit/601a1561ccb8bbb2107e85a8afce14f623c12a04) by Tully Foote).
+- Add extra arguments necessary for pulse Found here: https://github.com/jacknlliu/ros-docker-images/issues/7 Also fixed typo in the config. ([a9b6eba](https://github.com/osrf/rocker/commit/a9b6ebab3d09664ec2a014e67bd0ab83732326be) by Tully Foote).
+- add to the wishlist ([6e224e2](https://github.com/osrf/rocker/commit/6e224e2435cedb74b3c42e727bd388190610f46b) by Tully Foote).
+- add readme ([fa0d251](https://github.com/osrf/rocker/commit/fa0d251779bbf2add0842dcaefb40464451f3fd6) by Tully Foote).
+- add a readme ([9d6bd66](https://github.com/osrf/rocker/commit/9d6bd6636d858e2b2bac3cb8ae74f683cad311b1) by Tully Foote).
+- add parameter for disabling caching ([79a6beb](https://github.com/osrf/rocker/commit/79a6beb9301e0c446d9fe79d25d54067094c5c4d) by Tully Foote).
+
+### Fixed
+
+- fix assert spacing ([02601da](https://github.com/osrf/rocker/commit/02601da2fa9413192fa96df32cb65bf8a07cb9c6) by Tully Foote).
+- fix network argument parsing ([d8b12af](https://github.com/osrf/rocker/commit/d8b12af21fe6ebf7bafdadf8e5f36e151ceb0ec6) by Tully Foote).
+- fix docker API usage for pull ([a749985](https://github.com/osrf/rocker/commit/a749985104855bc44a5615eb8e25aa63833bd27a) by Tully Foote).
+
+### Removed
+
+- remove legacy test function ([4c1466a](https://github.com/osrf/rocker/commit/4c1466a217e4e150aa722836b3768abcd2efe425) by Tully Foote).
+- remove legacy comments ([38b5903](https://github.com/osrf/rocker/commit/38b59034eb373ff12619529383dc8812446890ad) by Tully Foote).
diff --git a/README.md b/README.md
index f851f0f6..38f14f24 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Whereas with `rocker` you can invoke your specific plugins and it will use multi
-## Know extensions
+## Known extensions
Rocker supports extensions via entry points there are some built in but you can add your own.
@@ -38,7 +38,7 @@ You can get full details on the extensions from the main `rocker --help` command
- pulse -- Mount pulse audio into the container
- ssh -- Pass through ssh access to the container.
-As well as access to many of the docker arguments as well such as `device`, `env`, `volume`, `name`, `network`, and `privileged`.
+As well as access to many of the docker arguments as well such as `device`, `env`, `volume`, `name`, `network`, `ipc`, and `privileged`.
### Externally maintained extensions
@@ -47,8 +47,21 @@ Here's a list of public repositories with extensions.
- Off-your-rocker: https://github.com/sloretz/off-your-rocker
- mp_rocker: https://github.com/miguelprada/mp_rocker
- ghrocker: https://github.com/tfoote/ghrocker
+- novnc_rocker: https://github.com/tfoote/novnc-rocker
+- template_rocker: https://github.com/blooop/template_rocker
+- deps_rocker: https://github.com/blooop/deps_rocker
+- pixi_rocker: https://github.com/blooop/pixi_rocker
+- conda_rocker: https://github.com/blooop/conda_rocker
+- palanteer_rocker: https://github.com/blooop/palanteer_rocker
+- lazygit_rocker: https://github.com/blooop/lazygit_rocker
+### Externally maintained rocker wrappers
+
+Here is a list of public repositories that wrap rocker and extend its functionality. These tools are meant to be a drop in replacement of rocker so that all the existing behavior stays the same.
+
+- rockerc: https://github.com/blooop/rockerc wraps rocker to enable putting rocker commands into a yaml config file.
+- rockervsc: https://github.com/blooop/rockervsc wraps rocker so that a vscode instance attaches to the launched container.
# Prerequisites
@@ -153,7 +166,7 @@ Then you can run pytest.
Notes:
- Make sure to use the python3 instance of pytest from inside the environment.
-- The tests include an nvidia test which assumes you're using a machine with an nvidia gpu.
+- The tests include an nvidia test which assumes you're using a machine with an nvidia gpu. To skip them use `-m "not nvidia"`
# Example usage
@@ -169,15 +182,6 @@ After the ekf converges,
You can send a takeoff command and then click to command the vehicle to fly to a point on the map.
-
-## Fly a plane
-
-Example usage with a plane
-
- rocker --nvidia --x11 --user --home --pull --pulse tfoote/drone_demo roslaunch sitl_launcher plane_demo.launch world_name:=worlds/plane.world gui:=false
-
-In QGroundControl go ahead and make a mission, upload it, and then start the mission.
-
## ROS 2 rviz
rocker --nvidia --x11 osrf/ros:crystal-desktop rviz2
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..252003bf
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,3 @@
+comment:
+ layout: "reach, diff, flags, files"
+ behavior: default
diff --git a/setup.py b/setup.py
index a217acf4..5a80203a 100644
--- a/setup.py
+++ b/setup.py
@@ -3,10 +3,14 @@
import os
from setuptools import setup
+# importlib-metadata dependency can be removed when RHEL8 and other 3.6 based systems are not in support cycles
+
install_requires = [
'empy',
+ 'importlib-metadata; python_version < "3.8"',
'pexpect',
'packaging',
+ 'urllib3',
]
# docker API used to be in a package called `docker-py` before the 2.0 release
@@ -31,7 +35,7 @@
kwargs = {
'name': 'rocker',
- 'version': '0.2.10',
+ 'version': '0.2.18',
'packages': ['rocker'],
'package_dir': {'': 'src'},
'package_data': {'rocker': ['templates/*.em']},
@@ -39,7 +43,7 @@
'console_scripts': [
'rocker = rocker.cli:main',
'detect_docker_image_os = rocker.cli:detect_image_os',
- ],
+ ],
'rocker.extensions': [
'cuda = rocker.nvidia_extension:Cuda',
'devices = rocker.extensions:Devices',
@@ -47,19 +51,25 @@
'env = rocker.extensions:Environment',
'expose = rocker.extensions:Expose',
'git = rocker.git_extension:Git',
+ 'group_add = rocker.extensions:GroupAdd',
'home = rocker.extensions:HomeDir',
+ 'hostname = rocker.extensions:Hostname',
+ 'ipc = rocker.extensions:Ipc',
'name = rocker.extensions:Name',
'network = rocker.extensions:Network',
'nvidia = rocker.nvidia_extension:Nvidia',
'port = rocker.extensions:Port',
'privileged = rocker.extensions:Privileged',
'pulse = rocker.extensions:PulseAudio',
+ 'rmw = rocker.rmw_extension:RMW',
+ 'shm_size = rocker.extensions:ShmSize',
'ssh = rocker.ssh_extension:Ssh',
+ 'ulimit = rocker.ulimit_extension:Ulimit',
'user = rocker.extensions:User',
'volume = rocker.volume_extension:Volume',
'x11 = rocker.nvidia_extension:X11',
]
- },
+ },
'author': 'Tully Foote',
'author_email': 'tfoote@osrfoundation.org',
'keywords': ['Docker'],
@@ -83,4 +93,3 @@
}
setup(**kwargs)
-
diff --git a/src/rocker/cli.py b/src/rocker/cli.py
index 7d78641c..e74791b7 100644
--- a/src/rocker/cli.py
+++ b/src/rocker/cli.py
@@ -20,6 +20,7 @@
from .core import get_rocker_version
from .core import RockerExtensionManager
from .core import DependencyMissing
+from .core import ExtensionError
from .os_detector import detect_os
@@ -34,6 +35,7 @@ def main():
parser.add_argument('--noexecute', action='store_true', help='Deprecated')
parser.add_argument('--nocache', action='store_true')
parser.add_argument('--nocleanup', action='store_true', help='do not remove the docker container when stopped')
+ parser.add_argument('--persist-image', action='store_true', help='do not remove the docker image when stopped', default=False) #TODO(tfoote) Add a name to it if persisting
parser.add_argument('--pull', action='store_true')
parser.add_argument('--version', action='version',
version='%(prog)s ' + get_rocker_version())
@@ -54,9 +56,11 @@ def main():
args_dict['mode'] = OPERATIONS_DRY_RUN
print('DEPRECATION Warning: --noexecute is deprecated for --mode dry-run please switch your usage by December 2020')
- active_extensions = extension_manager.get_active_extensions(args_dict)
- # Force user to end if present otherwise it will break other extensions
- active_extensions.sort(key=lambda e:e.get_name().startswith('user'))
+ try:
+ active_extensions = extension_manager.get_active_extensions(args_dict)
+ except ExtensionError as e:
+ print(f"ERROR! {str(e)}")
+ return 1
print("Active extensions %s" % [e.get_name() for e in active_extensions])
base_image = args.image
@@ -65,10 +69,16 @@ def main():
exit_code = dig.build(**vars(args))
if exit_code != 0:
print("Build failed exiting")
+ if not args_dict['persist_image']:
+ dig.clear_image()
return exit_code
# Convert command into string
args.command = ' '.join(args.command)
- return dig.run(**args_dict)
+ result = dig.run(**args_dict)
+ if not args_dict['persist_image']:
+ print(f'Clearing Image: {dig.image_id}s\nTo not clean up use --persist-images')
+ dig.clear_image()
+ return result
def detect_image_os():
diff --git a/src/rocker/core.py b/src/rocker/core.py
index 2f1a5353..03af51c3 100644
--- a/src/rocker/core.py
+++ b/src/rocker/core.py
@@ -15,10 +15,17 @@
from collections import OrderedDict
import io
import os
+import pwd
import re
import sys
-import pkg_resources
+# importlib-metadata dependency can be removed when RHEL8 and other 3.6 based systems are not in support cycles
+if sys.version_info >= (3, 8):
+ import importlib.metadata as importlib_metadata
+else:
+ import importlib_metadata
+
+
import pkgutil
from requests.exceptions import ConnectionError
import shlex
@@ -29,9 +36,11 @@
import pexpect
import fcntl
+from pathlib import Path
import signal
import struct
import termios
+import typing
SYS_STDOUT = sys.stdout
@@ -45,6 +54,10 @@ class DependencyMissing(RuntimeError):
pass
+class ExtensionError(RuntimeError):
+ pass
+
+
class RockerExtension(object):
"""The base class for Rocker extension points"""
@@ -58,10 +71,31 @@ def validate_environment(self, cliargs):
necessary resources are available, like hardware."""
pass
+ def invoke_after(self, cliargs) -> typing.Set[str]:
+ """
+ This extension should be loaded after the extensions in the returned
+ set. These extensions are not required to be present, but if they are,
+ they will be loaded before this extension.
+ """
+ return set()
+
+ def required(self, cliargs) -> typing.Set[str]:
+ """
+ Ensures the specified extensions are present and combined with
+ this extension. If the required extension should be loaded before
+ this extension, it should also be added to the `invoke_after` set.
+ """
+ return set()
+
def get_preamble(self, cliargs):
return ''
def get_snippet(self, cliargs):
+ """ Get a dockerfile snippet to be executed as ROOT."""
+ return ''
+
+ def get_user_snippet(self, cliargs):
+ """ Get a dockerfile snippet to be executed after switchingto the expected USER."""
return ''
def get_files(self, cliargs):
@@ -83,7 +117,7 @@ def check_args_for_activation(cls, cli_args):
return True if cli_args.get(cls.get_name()) else False
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
raise NotImplementedError
@@ -106,13 +140,70 @@ def extend_cli_parser(self, parser, default_args={}):
parser.add_argument('--extension-blacklist', nargs='*',
default=[],
help='Prevent any of these extensions from being loaded.')
+ parser.add_argument('--strict-extension-selection', action='store_true',
+ help='When enabled, causes an error if required extensions are not explicitly '
+ 'called out on the command line. Otherwise, the required extensions will '
+ 'automatically be loaded if available.')
def get_active_extensions(self, cli_args):
- active_extensions = [e() for e in self.available_plugins.values() if e.check_args_for_activation(cli_args) and e.get_name() not in cli_args['extension_blacklist']]
- active_extensions.sort(key=lambda e:e.get_name().startswith('user'))
- return active_extensions
+ """
+ Checks for missing dependencies (specified by each extension's
+ required() method) and additionally sorts them.
+ """
+ def sort_extensions(extensions, cli_args):
+
+ def topological_sort(source: typing.Dict[str, typing.Set[str]]) -> typing.List[str]:
+ """Perform a topological sort on names and dependencies and returns the sorted list of names."""
+ names = set(source.keys())
+ # prune optional dependencies if they are not present (at this point the required check has already occurred)
+ pending = [(name, dependencies.intersection(names)) for name, dependencies in source.items()]
+ emitted = []
+ while pending:
+ next_pending = []
+ next_emitted = []
+ for entry in pending:
+ name, deps = entry
+ deps.difference_update(emitted) # remove dependencies already emitted
+ if deps: # still has dependencies? recheck during next pass
+ next_pending.append(entry)
+ else: # no more dependencies? time to emit
+ yield name
+ next_emitted.append(name) # remember what was emitted for difference_update()
+ if not next_emitted:
+ raise ExtensionError("Cyclic dependancy detected: %r" % (next_pending,))
+ pending = next_pending
+ emitted = next_emitted
+
+ extension_graph = {name: cls.invoke_after(cli_args) for name, cls in sorted(extensions.items())}
+ active_extension_list = [extensions[name] for name in topological_sort(extension_graph)]
+ return active_extension_list
+
+ active_extensions = {}
+ find_reqs = set([name for name, cls in self.available_plugins.items() if cls.check_args_for_activation(cli_args)])
+ while find_reqs:
+ name = find_reqs.pop()
+
+ if name in self.available_plugins.keys():
+ if name not in cli_args['extension_blacklist']:
+ ext = self.available_plugins[name]()
+ active_extensions[name] = ext
+ else:
+ raise ExtensionError(f"Extension '{name}' is blacklisted.")
+ else:
+ raise ExtensionError(f"Extension '{name}' not found. Is it installed?")
+
+ # add additional reqs for processing not already known about
+ known_reqs = set(active_extensions.keys()).union(find_reqs)
+ missing_reqs = ext.required(cli_args).difference(known_reqs)
+ if missing_reqs:
+ if cli_args['strict_extension_selection']:
+ raise ExtensionError(f"Extension '{name}' is missing required extension(s) {list(missing_reqs)}")
+ else:
+ print(f"Adding implicilty required extension(s) {list(missing_reqs)} required by extension '{name}'")
+ find_reqs = find_reqs.union(missing_reqs)
+ return sort_extensions(active_extensions, cli_args)
def get_docker_client():
"""Simple helper function for pre 2.0 imports"""
@@ -132,6 +223,9 @@ def get_docker_client():
' This is usually by being a member of the docker group.'
' The underlying error was:\n"""\n%s\n"""\n' % ex)
+def get_user_name():
+ userinfo = pwd.getpwuid(os.getuid())
+ return getattr(userinfo, 'pw_' + 'name')
def docker_build(docker_client = None, output_callback = None, **kwargs):
image_id = None
@@ -157,6 +251,12 @@ def docker_build(docker_client = None, output_callback = None, **kwargs):
print("no more output and success not detected")
return None
+def docker_remove_image(image_id, docker_client = None, output_callback = None, **kwargs):
+
+ if not docker_client:
+ docker_client = get_docker_client()
+
+ docker_client.remove_image(image_id)
class SIGWINCHPassthrough(object):
def __init__ (self, process):
@@ -254,7 +354,6 @@ def get_operating_mode(self, args):
print("No tty detected for stdin forcing non-interactive")
return operating_mode
-
def generate_docker_cmd(self, command='', **kwargs):
docker_args = ''
@@ -320,17 +419,28 @@ def run(self, command='', **kwargs):
print("Docker run failed\n", ex)
return ex.returncode
+ def clear_image(self):
+ if self.image_id:
+ docker_remove_image(self.image_id)
+ self.image_id = None
+ self.built = False
def write_files(extensions, args_dict, target_directory):
all_files = {}
for active_extension in extensions:
- for file_name, contents in active_extension.get_files(args_dict).items():
- if os.path.isabs(file_name):
+ for file_path, contents in active_extension.get_files(args_dict).items():
+ if os.path.isabs(file_path):
print('WARNING!! Path %s from extension %s is absolute'
- 'and cannot be written out, skipping' % (file_name, active_extension.get_name()))
+ 'and cannot be written out, skipping' % (file_path, active_extension.get_name()))
continue
- full_path = os.path.join(target_directory, file_name)
- with open(full_path, 'w') as fh:
+ full_path = os.path.join(target_directory, file_path)
+ if Path(target_directory).resolve() not in Path(full_path).resolve().parents:
+ print('WARNING!! Path %s from extension %s is outside target directory'
+ 'and cannot be written out, skipping' % (file_path, active_extension.get_name()))
+ continue
+ Path(os.path.dirname(full_path)).mkdir(exist_ok=True, parents=True)
+ mode = 'wb' if isinstance(contents, bytes) else 'w' # check to see if contents should be written as binary
+ with open(full_path, mode) as fh:
print('Writing to file %s' % full_path)
fh.write(contents)
return all_files
@@ -338,22 +448,50 @@ def write_files(extensions, args_dict, target_directory):
def generate_dockerfile(extensions, args_dict, base_image):
dockerfile_str = ''
+ # Preamble snippets
for el in extensions:
- dockerfile_str += '# Preamble from extension [%s]\n' % el.name
+ dockerfile_str += '# Preamble from extension [%s]\n' % el.get_name()
dockerfile_str += el.get_preamble(args_dict) + '\n'
dockerfile_str += '\nFROM %s\n' % base_image
+ # ROOT snippets
dockerfile_str += 'USER root\n'
for el in extensions:
- dockerfile_str += '# Snippet from extension [%s]\n' % el.name
+ dockerfile_str += '# Snippet from extension [%s]\n' % el.get_name()
dockerfile_str += el.get_snippet(args_dict) + '\n'
+ # Set USER if user extension activated
+ if 'user' in args_dict and args_dict['user']:
+ if 'user_override_name' in args_dict and args_dict['user_override_name']:
+ username = args_dict['user_override_name']
+ else:
+ username = get_user_name()
+ dockerfile_str += f'USER {username}\n'
+ # USER snippets
+ for el in extensions:
+ dockerfile_str += '# User Snippet from extension [%s]\n' % el.get_name()
+ dockerfile_str += el.get_user_snippet(args_dict) + '\n'
return dockerfile_str
+def list_entry_points():
+ entry_points = importlib_metadata.entry_points()
+ if hasattr(entry_points, 'select'):
+ styles_groups = entry_points.select(group='flake8_import_order.styles')
+ else:
+ styles_groups = entry_points.get('flake8_import_order.styles', [])
+
+ return styles_groups
+
def list_plugins(extension_point='rocker.extensions'):
+
+ all_entry_points = importlib_metadata.entry_points()
+ if hasattr(all_entry_points, 'select'):
+ rocker_extensions = all_entry_points.select(group=extension_point)
+ else:
+ rocker_extensions = all_entry_points.get(extension_point, [])
+
unordered_plugins = {
entry_point.name: entry_point.load()
- for entry_point
- in pkg_resources.iter_entry_points(extension_point)
+ for entry_point in rocker_extensions
}
# Order plugins by extension point name for consistent ordering below
plugin_names = list(unordered_plugins.keys())
@@ -362,4 +500,4 @@ def list_plugins(extension_point='rocker.extensions'):
def get_rocker_version():
- return pkg_resources.require('rocker')[0].version
+ return importlib_metadata.version('rocker')
diff --git a/src/rocker/em.py b/src/rocker/em.py
new file mode 100644
index 00000000..c2c3df53
--- /dev/null
+++ b/src/rocker/em.py
@@ -0,0 +1,23 @@
+# Copyright 2019 Open Source Robotics Foundation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import em
+
+def empy_expand(template, substitution_variables):
+ """Indirection for empy version compatibility."""
+ if em.__version__.startswith('3'):
+ return em.expand(template, substitution_variables)
+ else:
+ return em.expand(template, globals=substitution_variables)
\ No newline at end of file
diff --git a/src/rocker/extensions.py b/src/rocker/extensions.py
index bdfcd4a2..bee93639 100644
--- a/src/rocker/extensions.py
+++ b/src/rocker/extensions.py
@@ -15,7 +15,6 @@
import grp
import os
import docker
-import em
import getpass
import pwd
import pkgutil
@@ -25,6 +24,7 @@
import sys
from .core import get_docker_client
+from .em import empy_expand
def name_to_argument(name):
@@ -54,7 +54,7 @@ def get_docker_args(self, cliargs):
return args
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--devices',
default=defaults.get('devices', None),
nargs='*',
@@ -81,16 +81,88 @@ def get_preamble(self, cliargs):
def get_snippet(self, cliargs):
snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
- return em.expand(snippet, self.get_environment_subs())
+ return empy_expand(snippet, self.get_environment_subs())
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(DevHelpers.get_name()),
action='store_true',
default=defaults.get('dev_helpers', None),
help="add development tools emacs and byobu to your environment")
+class Expose(RockerExtension):
+ @staticmethod
+ def get_name():
+ return 'expose'
+
+ def __init__(self):
+ self.name = Expose.get_name()
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_docker_args(self, cliargs):
+ args = ['']
+ ports = cliargs.get('expose', [])
+ for port in ports:
+ args.append(' --expose {0}'.format(port))
+ return ' '.join(args)
+
+ @staticmethod
+ def register_arguments(parser, defaults={}):
+ parser.add_argument('--expose',
+ default=defaults.get('expose', None),
+ action='append',
+ help="Exposes a port from the container to host machine.")
+
+
+class Hostname(RockerExtension):
+ @staticmethod
+ def get_name():
+ return 'hostname'
+
+ def __init__(self):
+ self.name = Hostname.get_name()
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_docker_args(self, cliargs):
+ args = ''
+ hostname = cliargs.get('hostname', None)
+ if hostname:
+ args += ' --hostname %s ' % hostname
+ return args
+
+ @staticmethod
+ def register_arguments(parser, defaults):
+ parser.add_argument('--hostname', default=defaults.get('hostname', ''),
+ help='Hostname of the container.')
+
+
+class Ipc(RockerExtension):
+ @staticmethod
+ def get_name():
+ return 'ipc'
+ def __init__(self):
+ self.name = Ipc.get_name()
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_docker_args(self, cliargs):
+ args = ''
+ ipc = cliargs.get('ipc', None)
+ args += ' --ipc %s ' % ipc
+ return args
+
+ @staticmethod
+ def register_arguments(parser, defaults={}):
+ parser.add_argument('--ipc', default=defaults.get('ipc', None),
+ help='IPC namespace to use. To share ipc with the host use host. More details can be found at https://docs.docker.com/reference/cli/docker/container/run/#ipc')
+
+
class Name(RockerExtension):
@staticmethod
def get_name():
@@ -110,7 +182,7 @@ def get_docker_args(self, cliargs):
return args
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--name', default=defaults.get('name', ''),
help='Name of the container.')
@@ -133,39 +205,13 @@ def get_docker_args(self, cliargs):
return args
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
client = get_docker_client()
parser.add_argument('--network', choices=[n['Name'] for n in client.networks()],
default=defaults.get('network', None),
help="What network configuration to use.")
-class Expose(RockerExtension):
- @staticmethod
- def get_name():
- return 'expose'
-
- def __init__(self):
- self.name = Expose.get_name()
-
- def get_preamble(self, cliargs):
- return ''
-
- def get_docker_args(self, cliargs):
- args = ['']
- ports = cliargs.get('expose', [])
- for port in ports:
- args.append(' --expose {0}'.format(port))
- return ' '.join(args)
-
- @staticmethod
- def register_arguments(parser, defaults={}):
- parser.add_argument('--expose',
- default=defaults.get('expose', None),
- action='append',
- help="Exposes a port from the container to host machine.")
-
-
class Port(RockerExtension):
@staticmethod
def get_name():
@@ -185,7 +231,7 @@ def get_docker_args(self, cliargs):
return ' '.join(args)
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--port',
default=defaults.get('port', None),
action='append',
@@ -215,7 +261,7 @@ def get_preamble(self, cliargs):
def get_snippet(self, cliargs):
snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
- return em.expand(snippet, self.get_environment_subs())
+ return empy_expand(snippet, self.get_environment_subs())
def get_docker_args(self, cliargs):
args = ' -v /run/user/%(user_id)s/pulse:/run/user/%(user_id)s/pulse --device /dev/snd '\
@@ -223,7 +269,7 @@ def get_docker_args(self, cliargs):
return args % self.get_environment_subs()
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(PulseAudio.get_name()),
action='store_true',
default=defaults.get(PulseAudio.get_name(), None),
@@ -242,7 +288,7 @@ def get_docker_args(self, cliargs):
return ' -v %s:%s ' % (Path.home(), Path.home())
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(HomeDir.get_name()),
action='store_true',
default=defaults.get(HomeDir.get_name(), None),
@@ -274,16 +320,31 @@ def get_snippet(self, cliargs):
substitutions['name'] = cliargs['user_override_name']
substitutions['dir'] = os.path.join('/home/', cliargs['user_override_name'])
substitutions['user_preserve_home'] = True if 'user_preserve_home' in cliargs and cliargs['user_preserve_home'] else False
+ if 'user_preserve_groups' in cliargs and isinstance(cliargs['user_preserve_groups'], list):
+ query_groups = cliargs['user_preserve_groups']
+ all_groups = grp.getgrall()
+ if query_groups:
+ matched_groups = [g for g in all_groups if g.gr_name in query_groups]
+ matched_group_names = [g.gr_name for g in matched_groups]
+ unmatched_groups = [n for n in cliargs['user_preserve_groups'] if n not in matched_group_names]
+ if unmatched_groups:
+ print('Warning skipping groups %s because they do not exist on the host.' % unmatched_groups)
+ substitutions['user_groups'] = ' '.join(['{};{}'.format(g.gr_name, g.gr_gid) for g in matched_groups])
+ else:
+ substitutions['user_groups'] = ' '.join(['{};{}'.format(g.gr_name, g.gr_gid) for g in all_groups if substitutions['name'] in g.gr_mem])
+ else:
+ substitutions['user_groups'] = ''
+ substitutions['user_preserve_groups_permissive'] = True if 'user_preserve_groups_permissive' in cliargs and cliargs['user_preserve_groups_permissive'] else False
substitutions['home_extension_active'] = True if 'home' in cliargs and cliargs['home'] else False
if 'user_override_shell' in cliargs and cliargs['user_override_shell'] is not None:
if cliargs['user_override_shell'] == '':
substitutions['shell'] = None
else:
substitutions['shell'] = cliargs['user_override_shell']
- return em.expand(snippet, substitutions)
+ return empy_expand(snippet, substitutions)
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(User.get_name()),
action='store_true',
default=defaults.get('user', None),
@@ -296,6 +357,16 @@ def register_arguments(parser, defaults={}):
action='store_true',
default=defaults.get('user-preserve-home', False),
help="Do not delete home directory if it exists when making a new user.")
+ parser.add_argument('--user-preserve-groups',
+ action='store',
+ nargs='*',
+ default=defaults.get('user-preserve-groups', False),
+ help="Assign user to same groups as he belongs in host. If arguments provided they are the explicit list of groups.")
+ parser.add_argument('--user-preserve-groups-permissive',
+ action='store_true',
+ default=defaults.get('user-preserve-groups-permissive', False),
+ help="If using user-preserve-groups allow failures in assignment."
+ "This is important if the host and target have different rules. https://unix.stackexchange.com/a/11481/83370" )
parser.add_argument('--user-override-shell',
action='store',
default=defaults.get('user-override-shell', None),
@@ -329,7 +400,7 @@ def get_docker_args(self, cli_args):
return ' '.join(args)
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--env', '-e',
metavar='NAME[=VALUE]',
type=str,
@@ -365,8 +436,59 @@ def get_docker_args(self, cli_args):
return ' --privileged'
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(Privileged.get_name()),
action='store_true',
default=defaults.get(Privileged.get_name(), None),
help="give extended privileges to the container")
+
+
+class GroupAdd(RockerExtension):
+ """Add additional groups to running container."""
+ @staticmethod
+ def get_name():
+ return 'group_add'
+
+ def __init__(self):
+ self.name = GroupAdd.get_name()
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_docker_args(self, cliargs):
+ args = ['']
+ groups = cliargs.get('group_add', [])
+ for group in groups:
+ args.append(' --group-add {0}'.format(group))
+ return ' '.join(args)
+
+ @staticmethod
+ def register_arguments(parser, defaults):
+ parser.add_argument(name_to_argument(GroupAdd.get_name()),
+ default=defaults.get(GroupAdd.get_name(), None),
+ action='append',
+ help="Add additional groups to join.")
+
+class ShmSize(RockerExtension):
+ @staticmethod
+ def get_name():
+ return 'shm_size'
+
+ def __init__(self):
+ self.name = ShmSize.get_name()
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_docker_args(self, cliargs):
+ args = ''
+ shm_size = cliargs.get('shm_size', None)
+ if shm_size:
+ args += f' --shm-size {shm_size} '
+ return args
+
+ @staticmethod
+ def register_arguments(parser, defaults={}):
+ parser.add_argument('--shm-size',
+ default=defaults.get('shm_size', None),
+ help="Set the size of the shared memory for the container (e.g., 512m, 1g).")
\ No newline at end of file
diff --git a/src/rocker/git_extension.py b/src/rocker/git_extension.py
index b35c3979..4fc0905f 100644
--- a/src/rocker/git_extension.py
+++ b/src/rocker/git_extension.py
@@ -35,7 +35,9 @@ def get_docker_args(self, cli_args):
user_gitconfig = cli_args.get('git_config_path', os.path.expanduser('~/.gitconfig'))
user_gitconfig_target = '/root/.gitconfig'
if 'user' in cli_args and cli_args['user']:
- username = cli_args.get('user_override_name', getpass.getuser())
+ username = getpass.getuser()
+ if 'user_override_name' in cli_args and cli_args['user_override_name']:
+ username = cli_args['user_override_name']
user_gitconfig_target = '/home/%(username)s/.gitconfig' % locals()
if os.path.exists(system_gitconfig):
args += ' -v {system_gitconfig}:{system_gitconfig_target}:ro'.format(**locals())
@@ -44,7 +46,7 @@ def get_docker_args(self, cli_args):
return args
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--git',
action='store_true',
default=defaults.get(Git.get_name(), None),
diff --git a/src/rocker/nvidia_extension.py b/src/rocker/nvidia_extension.py
index 1bae9fa9..3644c220 100644
--- a/src/rocker/nvidia_extension.py
+++ b/src/rocker/nvidia_extension.py
@@ -13,7 +13,6 @@
# limitations under the License.
import os
-import em
import getpass
import tempfile
from packaging.version import Version
@@ -27,12 +26,37 @@
from .extensions import name_to_argument
from .core import get_docker_client
from .core import RockerExtension
+from .em import empy_expand
+
+GLVND_VERSION_POLICY_LATEST_LTS='latest_lts'
+
+NVIDIA_GLVND_VALID_VERSIONS=['16.04', '18.04','20.04', '22.04', '24.04']
def get_docker_version():
docker_version_raw = get_docker_client().version()['Version']
# Fix for version 17.09.0-ce
return Version(docker_version_raw.split('-')[0])
+def glvnd_version_from_policy(image_version, policy):
+ # Default policy GLVND_VERSION_POLICY_LATEST_LTS
+ if not policy:
+ policy = GLVND_VERSION_POLICY_LATEST_LTS
+
+ if policy == GLVND_VERSION_POLICY_LATEST_LTS:
+ if image_version in ['16.04', '16.10', '17.04', '17.10']:
+ return '16.04'
+ if image_version in ['18.04', '18.10', '19.04', '19.10']:
+ return '18.04'
+ if image_version in ['20.04', '20.10', '21.04', '21.10']:
+ return '20.04'
+ if image_version in ['22.04', '22.10', '23.04', '23.10']:
+ return '22.04'
+ # 24.04 is not available yet
+ # if image_version in ['24.04', '24.10', '25.04', '25.10']:
+ # return '24.04'
+ return '22.04'
+ return None
+
class X11(RockerExtension):
@staticmethod
def get_name():
@@ -70,7 +94,7 @@ def precondition_environment(self, cliargs):
raise ex
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(X11.get_name()),
action='store_true',
default=defaults.get(X11.get_name(), None),
@@ -86,7 +110,7 @@ def __init__(self):
self._env_subs = None
self.name = Nvidia.get_name()
self.supported_distros = ['Ubuntu', 'Debian GNU/Linux']
- self.supported_versions = ['16.04', '18.04', '20.04', '10', '22.04']
+ self.supported_versions = ['16.04', '18.04', '20.04', '10', '22.04', '24.04']
def get_environment_subs(self, cliargs={}):
@@ -94,7 +118,7 @@ def get_environment_subs(self, cliargs={}):
self._env_subs = {}
self._env_subs['user_id'] = os.getuid()
self._env_subs['username'] = getpass.getuser()
-
+
# non static elements test every time
detected_os = detect_os(cliargs['base_image'], print, nocache=cliargs.get('nocache', False))
if detected_os is None:
@@ -111,28 +135,47 @@ def get_environment_subs(self, cliargs={}):
print("WARNING distro %s version %s not in supported list by Nvidia supported versions" % (dist, ver), self.supported_versions)
sys.exit(1)
# TODO(tfoote) add a standard mechanism for checking preconditions and disabling plugins
+ nvidia_glvnd_version = cliargs.get('nvidia_glvnd_version', None)
+ if not nvidia_glvnd_version:
+ nvidia_glvnd_version = glvnd_version_from_policy(ver, cliargs.get('nvidia_glvnd_policy', None) )
+ self._env_subs['nvidia_glvnd_version'] = nvidia_glvnd_version
return self._env_subs
def get_preamble(self, cliargs):
preamble = pkgutil.get_data('rocker', 'templates/%s_preamble.Dockerfile.em' % self.name).decode('utf-8')
- return em.expand(preamble, self.get_environment_subs(cliargs))
+ return empy_expand(preamble, self.get_environment_subs(cliargs))
def get_snippet(self, cliargs):
snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
- return em.expand(snippet, self.get_environment_subs(cliargs))
+ return empy_expand(snippet, self.get_environment_subs(cliargs))
def get_docker_args(self, cliargs):
+ force_flag = cliargs.get('nvidia', None)
+ if force_flag == 'runtime':
+ return " --runtime=nvidia"
+ if force_flag == 'gpus':
+ return " --gpus all"
if get_docker_version() >= Version("19.03"):
return " --gpus all"
return " --runtime=nvidia"
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(Nvidia.get_name()),
- action='store_true',
+ choices=['auto', 'runtime', 'gpus'],
+ nargs='?',
+ const='auto',
default=defaults.get(Nvidia.get_name(), None),
- help="Enable nvidia")
+ help="Enable nvidia. Default behavior is to pick flag based on docker version.")
+ parser.add_argument('--nvidia-glvnd-version',
+ choices=NVIDIA_GLVND_VALID_VERSIONS,
+ default=defaults.get('nvidia-glvnd-version', None),
+ help="Explicitly select an nvidia glvnd version")
+ parser.add_argument('--nvidia-glvnd-policy',
+ choices=[GLVND_VERSION_POLICY_LATEST_LTS],
+ default=defaults.get('nvidia-glvnd-policy', GLVND_VERSION_POLICY_LATEST_LTS),
+ help="Set an nvidia glvnd version policy if version is unset")
class Cuda(RockerExtension):
@staticmethod
@@ -143,7 +186,7 @@ def __init__(self):
self._env_subs = None
self.name = Cuda.get_name()
self.supported_distros = ['Ubuntu', 'Debian GNU/Linux']
- self.supported_versions = ['20.04', '22.04', '18.04', '11'] # Debian 11
+ self.supported_versions = ['20.04', '22.04', '24.04', '11', '12'] # Debian 11 and 12
def get_environment_subs(self, cliargs={}):
if not self._env_subs:
@@ -177,21 +220,19 @@ def get_environment_subs(self, cliargs={}):
def get_preamble(self, cliargs):
return ''
# preamble = pkgutil.get_data('rocker', 'templates/%s_preamble.Dockerfile.em' % self.name).decode('utf-8')
- # return em.expand(preamble, self.get_environment_subs(cliargs))
+ # return empy_expand(preamble, self.get_environment_subs(cliargs))
def get_snippet(self, cliargs):
snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
- return em.expand(snippet, self.get_environment_subs(cliargs))
+ return empy_expand(snippet, self.get_environment_subs(cliargs))
def get_docker_args(self, cliargs):
return ""
# Runtime requires --nvidia option too
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument(name_to_argument(Cuda.get_name()),
action='store_true',
default=defaults.get('cuda', None),
help="Install cuda and nvidia-cuda-dev into the container")
-
-
diff --git a/src/rocker/rmw_extension.py b/src/rocker/rmw_extension.py
new file mode 100644
index 00000000..5c52ee32
--- /dev/null
+++ b/src/rocker/rmw_extension.py
@@ -0,0 +1,84 @@
+# Copyright 2024 Open Source Robotics Foundation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from argparse import ArgumentTypeError
+import os
+import pkgutil
+
+from .em import empy_expand
+from rocker.extensions import RockerExtension
+from rocker.extensions import name_to_argument
+
+
+class RMW(RockerExtension):
+ rmw_map = {
+ 'cyclonedds': ['ros-${ROS_DISTRO}-rmw-cyclonedds-cpp'],
+ 'fastrtps' : ['ros-${ROS_DISTRO}-rmw-fastrtps-cpp'],
+ # TODO(tfoote) Enable connext with license acceptance method
+ # 'connextdds': ['ros-${ROS_DISTRO}-rmw-connextdds'],
+ }
+
+ @staticmethod
+ def get_package_names(rmw_name):
+ return RMW.rmw_map[rmw_name]
+
+ @staticmethod
+ def get_name():
+ return 'rmw'
+
+ def __init__(self):
+ self._env_subs = None
+ self.name = RMW.get_name()
+
+ def get_docker_args(self, cli_args):
+ rmw_config = cli_args.get('rmw')
+ if not rmw_config:
+ return '' # not active
+ implementation = rmw_config[0]
+ args = f' -e RMW_IMPLEMENTATION=rmw_{implementation}_cpp'
+ return args #% self.get_environment_subs()
+
+ def get_environment_subs(self):
+ if not self._env_subs:
+ self._env_subs = {}
+ return self._env_subs
+
+ def get_preamble(self, cliargs):
+ return ''
+
+ def get_snippet(self, cliargs):
+ snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % RMW.get_name()).decode('utf-8')
+ data = self.get_environment_subs()
+ # data['rosdistro'] = cliargs.get('rosdistro', 'rolling')
+ rmw = cliargs.get('rmw', None)
+ if rmw:
+ rmw = rmw[0]
+ else:
+ return '' # rmw not active
+ data['rmw'] = rmw
+ data['packages'] = RMW.get_package_names(rmw)
+ # data['rosdistro'] = 'rolling'
+ return empy_expand(snippet, data)
+
+ @staticmethod
+ def register_arguments(parser, defaults):
+ parser.add_argument(name_to_argument(RMW.get_name()),
+ default=defaults.get('rmw', None),
+ nargs=1,
+ choices=RMW.rmw_map.keys(),
+ help="Set the default RMW implementation")
+
+ # parser.add_argument('rosdistro',
+ # default=defaults.get('rosdistro', None),
+ # help="Set the default rosdistro, else autodetect")
diff --git a/src/rocker/ssh_extension.py b/src/rocker/ssh_extension.py
index 37de6c68..4417b605 100644
--- a/src/rocker/ssh_extension.py
+++ b/src/rocker/ssh_extension.py
@@ -46,7 +46,7 @@ def get_docker_args(self, cli_args):
return args
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--ssh',
action='store_true',
default=defaults.get(Ssh.get_name(), None),
diff --git a/src/rocker/templates/cuda_snippet.Dockerfile.em b/src/rocker/templates/cuda_snippet.Dockerfile.em
index 92a12d4f..c4575b6f 100644
--- a/src/rocker/templates/cuda_snippet.Dockerfile.em
+++ b/src/rocker/templates/cuda_snippet.Dockerfile.em
@@ -13,23 +13,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Enable contrib on debian to get required
# https://packages.debian.org/bullseye/glx-alternative-nvidia
-# Enable non-free for nvidia-cuda-dev
-# https://packages.debian.org/bullseye/nvidia-cuda-dev
RUN \
- @[if download_osstring == 'ubuntu']@
- wget https://developer.download.nvidia.com/compute/cuda/repos/@(download_osstring)@(download_verstring)/x86_64/cuda-@(download_osstring)@(download_verstring).pin \
- && mv cuda-@(download_osstring)@(download_verstring).pin /etc/apt/preferences.d/cuda-repository-pin-600 && \
- add-apt-repository restricted && \
- @[else]@
+ wget -q https://developer.download.nvidia.com/compute/cuda/repos/@(download_osstring)@(download_verstring)/x86_64/cuda-keyring_1.1-1_all.deb && \
+ dpkg -i cuda-keyring_1.1-1_all.deb && \
+ rm cuda-keyring_1.1-1_all.deb && \
+ \@[if download_osstring == 'debian']@
add-apt-repository contrib && \
- add-apt-repository non-free && \
- @[end if]@
- apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/@(download_osstring)@(download_verstring)/x86_64/@(download_keyid).pub \
- && add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/@(download_osstring)@(download_verstring)/x86_64/ /" \
- && apt-get update \
- && apt-get -y install cuda \
- && rm -rf /var/lib/apt/lists/*
+ \@[end if]@
+ apt-get update && \
+ apt-get -y install cuda-toolkit && \
+ rm -rf /var/lib/apt/lists/*
# File conflict problem with libnvidia-ml.so.1 and libcuda.so.1
# https://github.com/NVIDIA/nvidia-docker/issues/1551
diff --git a/src/rocker/templates/nvidia_preamble.Dockerfile.em b/src/rocker/templates/nvidia_preamble.Dockerfile.em
index 1bab5337..5b9681c6 100644
--- a/src/rocker/templates/nvidia_preamble.Dockerfile.em
+++ b/src/rocker/templates/nvidia_preamble.Dockerfile.em
@@ -1,3 +1 @@
-# Ubuntu 16.04 with nvidia-docker2 beta opengl support
-@{suffix = '16.04' if image_distro_version == '16.04' else '18.04'}@
-FROM nvidia/opengl:1.0-glvnd-devel-ubuntu@(suffix) as glvnd
+FROM nvidia/opengl:1.0-glvnd-devel-ubuntu@(nvidia_glvnd_version) as glvnd
diff --git a/src/rocker/templates/rmw_snippet.Dockerfile.em b/src/rocker/templates/rmw_snippet.Dockerfile.em
new file mode 100644
index 00000000..87f50275
--- /dev/null
+++ b/src/rocker/templates/rmw_snippet.Dockerfile.em
@@ -0,0 +1,16 @@
+# workspace development helpers
+
+
+@[ if rmw ]@
+RUN \
+ if [ -z "${ROS_DISTRO}" ]; then echo "ROS_DISTRO is unset cannot override RMW" ; exit 1 ; fi ;\
+ if dpkg -l @(' '.join(packages)) > /dev/null 2>&1; then \
+ apt-get update \
+ && DEBIAN_FRONTENT=non-interactive apt-get install -qy --no-install-recommends\
+ @(' '.join(packages)) \
+ && apt-get clean ;\
+ else \
+ echo "Found rmw packages @(' '.join(packages)) no need to install" ; \
+ fi
+@[ end if ]@
+
diff --git a/src/rocker/templates/user_snippet.Dockerfile.em b/src/rocker/templates/user_snippet.Dockerfile.em
index 63911409..2d94d619 100644
--- a/src/rocker/templates/user_snippet.Dockerfile.em
+++ b/src/rocker/templates/user_snippet.Dockerfile.em
@@ -17,14 +17,24 @@ RUN existing_user_by_uid=`getent passwd "@(uid)" | cut -f1 -d: || true` && \
groupadd -g "@(gid)" "@name"; \
fi && \
useradd --no-log-init --no-create-home --uid "@(uid)" @(str('-s ' + shell) if shell else '') -c "@(gecos)" -g "@(gid)" -d "@(dir)" "@(name)" && \
+@[if user_groups != '']@
+ user_groups="@(user_groups)" && \
+ for groupinfo in ${user_groups}; do \
+ existing_group_by_name=`getent group ${groupinfo%;*} || true`; \
+ existing_group_by_gid=`getent group ${groupinfo#*;} || true`; \
+ if [ -z "${existing_group_by_name}" ] && [ -z "${existing_group_by_gid}" ]; then \
+ groupadd -g "${groupinfo#*;}" "${groupinfo%;*}" && usermod -aG "${groupinfo%;*}" "@(name)" @( ('|| (true && echo "user-preserve-group-permissive Enabled, continuing without processing group $groupinfo" )') if user_preserve_groups_permissive else '') || (echo "Failed to add group ${groupinfo%;*}, consider option --user-preserve-group-permissive" && exit 2); \
+ elif [ "${existing_group_by_name}" = "${existing_group_by_gid}" ]; then \
+ usermod -aG "${groupinfo%;*}" "@(name)" @( ('|| (true && echo "user-preserve-group-permissive Enabled, continuing without processing group $groupinfo" )') if user_preserve_groups_permissive else '') || (echo "Failed to adjust group ${groupinfo%;*}, consider option --user-preserve-group-permissive" && exit 2); \
+ fi; \
+ done && \
+@[end if]@
echo "@(name) ALL=NOPASSWD: ALL" >> /etc/sudoers.d/rocker
@[if not home_extension_active ]@
# Making sure a home directory exists if we haven't mounted the user's home directory explicitly
RUN mkdir -p "$(dirname "@(dir)")" && mkhomedir_helper @(name)
@[end if]@
-# Commands below run as the developer user
-USER @(name)
WORKDIR @(dir)
@[else]@
# Detected user is root, which already exists so not creating new user.
diff --git a/src/rocker/ulimit_extension.py b/src/rocker/ulimit_extension.py
new file mode 100644
index 00000000..105650a1
--- /dev/null
+++ b/src/rocker/ulimit_extension.py
@@ -0,0 +1,67 @@
+# Copyright 2019 Open Source Robotics Foundation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from argparse import ArgumentTypeError
+import re
+from rocker.extensions import RockerExtension, name_to_argument
+
+
+class Ulimit(RockerExtension):
+ """
+ A RockerExtension to handle ulimit settings for Docker containers.
+
+ This extension allows specifying ulimit options in the format TYPE=SOFT_LIMIT[:HARD_LIMIT]
+ and validates the format before passing them as Docker arguments.
+ """
+ EXPECTED_FORMAT = "TYPE=SOFT_LIMIT[:HARD_LIMIT]"
+
+ @staticmethod
+ def get_name():
+ return 'ulimit'
+
+ def get_docker_args(self, cliargs):
+ args = ['']
+ ulimits = [x for sublist in cliargs[Ulimit.get_name()] for x in sublist]
+ for ulimit in ulimits:
+ if self.arg_format_is_valid(ulimit):
+ args.append(f"--ulimit {ulimit}")
+ else:
+ raise ArgumentTypeError(
+ f"Error processing {Ulimit.get_name()} flag '{ulimit}': expected format"
+ f" {Ulimit.EXPECTED_FORMAT}")
+ return ' '.join(args)
+
+ def arg_format_is_valid(self, arg: str):
+ """
+ Validate the format of the ulimit argument.
+
+ Args:
+ arg (str): The ulimit argument to validate.
+
+ Returns:
+ bool: True if the format is valid, False otherwise.
+ """
+ ulimit_format = r'(\w+)=(\w+)(:\w+)?$'
+ match = re.match(ulimit_format, arg)
+ return match is not None
+
+ @staticmethod
+ def register_arguments(parser, defaults):
+ parser.add_argument(name_to_argument(Ulimit.get_name()),
+ type=str,
+ nargs='+',
+ action='append',
+ metavar=Ulimit.EXPECTED_FORMAT,
+ default=defaults.get(Ulimit.get_name(), None),
+ help='ulimit options to add into the container.')
diff --git a/src/rocker/volume_extension.py b/src/rocker/volume_extension.py
index 6f2626fb..7958afc1 100644
--- a/src/rocker/volume_extension.py
+++ b/src/rocker/volume_extension.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from argparse import ArgumentParser
from argparse import ArgumentTypeError
import os
from rocker.extensions import RockerExtension
@@ -61,7 +62,7 @@ def get_docker_args(self, cli_args):
return ' '.join(args)
@staticmethod
- def register_arguments(parser):
+ def register_arguments(parser: ArgumentParser, defaults: dict):
parser.add_argument(Volume.ARG_ROCKER_VOLUME,
metavar='HOST-DIR[:CONTAINER-DIR[:OPTIONS]]',
type=str,
diff --git a/stdeb.cfg b/stdeb.cfg
index 4cd9cbd4..2e413391 100644
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -3,5 +3,5 @@ Debian-Version: 100
No-Python2:
Depends3: python3-docker, python3-empy, python3-pexpect, python3-packaging
Conflicts3: python-rocker
-Suite: bionic focal jammy stretch buster bullseye
+Suite: focal jammy noble bookworm trixie
X-Python3-Version: >= 3.2
diff --git a/test/test_core.py b/test/test_core.py
index f57e659b..855d585e 100644
--- a/test/test_core.py
+++ b/test/test_core.py
@@ -17,15 +17,20 @@
import argparse
import em
+import os
+import pwd
import pytest
import unittest
from itertools import chain
from rocker.core import DockerImageGenerator
+from rocker.core import ExtensionError
from rocker.core import list_plugins
from rocker.core import get_docker_client
from rocker.core import get_rocker_version
+from rocker.core import get_user_name
+from rocker.core import RockerExtension
from rocker.core import RockerExtensionManager
class RockerCoreTest(unittest.TestCase):
@@ -63,6 +68,7 @@ def test_run_before_build(self):
self.assertEqual(dig.run('true'), 1)
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true'), 0)
+ dig.clear_image()
@pytest.mark.docker
def test_return_code_no_extensions(self):
@@ -70,6 +76,7 @@ def test_return_code_no_extensions(self):
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true'), 0)
self.assertEqual(dig.run('false'), 1)
+ dig.clear_image()
@pytest.mark.docker
def test_return_code_multiple_extensions(self):
@@ -80,12 +87,14 @@ def test_return_code_multiple_extensions(self):
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true'), 0)
self.assertEqual(dig.run('false'), 1)
+ dig.clear_image()
@pytest.mark.docker
def test_noexecute(self):
dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true', noexecute=True), 0)
+ dig.clear_image()
@pytest.mark.docker
def test_dry_run(self):
@@ -93,6 +102,7 @@ def test_dry_run(self):
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true', mode='dry-run'), 0)
self.assertEqual(dig.run('false', mode='dry-run'), 0)
+ dig.clear_image()
@pytest.mark.docker
def test_non_interactive(self):
@@ -100,6 +110,7 @@ def test_non_interactive(self):
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true', mode='non-interactive'), 0)
self.assertEqual(dig.run('false', mode='non-interactive'), 1)
+ dig.clear_image()
@pytest.mark.docker
def test_device(self):
@@ -107,6 +118,7 @@ def test_device(self):
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run('true', devices=['/dev/random']), 0)
self.assertEqual(dig.run('true', devices=['/dev/does_not_exist']), 0)
+ dig.clear_image()
@pytest.mark.docker
def test_network(self):
@@ -115,6 +127,7 @@ def test_network(self):
networks = ['bridge', 'host', 'none']
for n in networks:
self.assertEqual(dig.run('true', network=n), 0)
+ dig.clear_image()
@pytest.mark.docker
def test_extension_manager(self):
@@ -128,9 +141,82 @@ def test_extension_manager(self):
self.assertIn('non-interactive', help_str)
self.assertIn('--extension-blacklist', help_str)
- active_extensions = active_extensions = extension_manager.get_active_extensions({'user': True, 'ssh': True, 'extension_blacklist': ['ssh']})
- self.assertEqual(len(active_extensions), 1)
- self.assertEqual(active_extensions[0].get_name(), 'user')
+ self.assertRaises(ExtensionError,
+ extension_manager.get_active_extensions,
+ {'user': True, 'ssh': True, 'extension_blacklist': ['ssh']})
+
+ def test_strict_required_extensions(self):
+ class Foo(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'foo'
+
+ class Bar(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'bar'
+
+ def required(self, cli_args):
+ return {'foo'}
+
+ extension_manager = RockerExtensionManager()
+ extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
+
+ correct_extensions_args = {'strict_extension_selection': True, 'bar': True, 'foo': True, 'extension_blacklist': []}
+ extension_manager.get_active_extensions(correct_extensions_args)
+
+ incorrect_extensions_args = {'strict_extension_selection': True, 'bar': True, 'extension_blacklist': []}
+ self.assertRaises(ExtensionError,
+ extension_manager.get_active_extensions, incorrect_extensions_args)
+
+ def test_implicit_required_extensions(self):
+ class Foo(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'foo'
+
+ class Bar(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'bar'
+
+ def required(self, cli_args):
+ return {'foo'}
+
+ extension_manager = RockerExtensionManager()
+ extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
+
+ implicit_extensions_args = {'strict_extension_selection': False, 'bar': True, 'extension_blacklist': []}
+ active_extensions = extension_manager.get_active_extensions(implicit_extensions_args)
+ self.assertEqual(len(active_extensions), 2)
+ # required extensions are not ordered, just check to make sure they are both present
+ if active_extensions[0].get_name() == 'foo':
+ self.assertEqual(active_extensions[1].get_name(), 'bar')
+ else:
+ self.assertEqual(active_extensions[0].get_name(), 'bar')
+ self.assertEqual(active_extensions[1].get_name(), 'foo')
+
+ def test_extension_sorting(self):
+ class Foo(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'foo'
+
+ class Bar(RockerExtension):
+ @classmethod
+ def get_name(cls):
+ return 'bar'
+
+ def invoke_after(self, cli_args):
+ return {'foo', 'absent_extension'}
+
+ extension_manager = RockerExtensionManager()
+ extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
+
+ args = {'bar': True, 'foo': True, 'extension_blacklist': []}
+ active_extensions = extension_manager.get_active_extensions(args)
+ self.assertEqual(active_extensions[0].get_name(), 'foo')
+ self.assertEqual(active_extensions[1].get_name(), 'bar')
def test_docker_cmd_interactive(self):
dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
@@ -148,6 +234,63 @@ def test_docker_cmd_interactive(self):
self.assertNotIn('-it', dig.generate_docker_cmd(mode='non-interactive'))
+ def test_docker_user_detection(self):
+ userinfo = pwd.getpwuid(os.getuid())
+ username_detected = getattr(userinfo, 'pw_' + 'name')
+ self.assertEqual(username_detected, get_user_name())
+
+ @pytest.mark.docker
+ def test_docker_user_setting(self):
+ parser = argparse.ArgumentParser()
+ extension_manager = RockerExtensionManager()
+ default_args = {}
+ extension_manager.extend_cli_parser(parser, default_args)
+ active_extensions = extension_manager.get_active_extensions({'user': True, 'extension_blacklist': ['ssh']})
+ dig = DockerImageGenerator(active_extensions, {'user_override_name': 'foo'}, 'ubuntu:bionic')
+
+ self.assertIn('USER root', dig.dockerfile)
+ self.assertNotIn('USER foo', dig.dockerfile)
+ dig = DockerImageGenerator(active_extensions, {'user': True, 'user_override_name': 'foo'}, 'ubuntu:bionic')
+ self.assertIn('USER root', dig.dockerfile)
+ self.assertIn('USER foo', dig.dockerfile)
+
+ def test_docker_user_snippet(self):
+
+ root_snippet_content = "RUN echo run as root"
+ user_snippet_content = "RUN echo run as user"
+
+ class UserSnippet(RockerExtension):
+ def __init__(self):
+ self.name = 'usersnippet'
+
+ @classmethod
+ def get_name(cls):
+ return 'usersnippet'
+
+ def get_snippet(self, cli_args):
+ return root_snippet_content
+
+
+ def get_user_snippet(self, cli_args):
+ return user_snippet_content
+
+ extension_manager = RockerExtensionManager()
+ extension_manager.available_plugins = {'usersnippet': UserSnippet}
+ active_extensions = extension_manager.get_active_extensions({'user': True, 'usersnippet': UserSnippet, 'extension_blacklist': ['ssh']})
+ self.assertTrue(active_extensions)
+
+ # No user snippet
+ mock_cli_args = {'user': False, 'usersnippet': True, 'user_override_name': 'foo'}
+ dig = DockerImageGenerator(active_extensions, mock_cli_args, 'ubuntu:bionic')
+ self.assertIn('USER root', dig.dockerfile)
+ self.assertNotIn('USER foo', dig.dockerfile)
+
+ # User snippet added
+ mock_cli_args = {'user': True, 'usersnippet': True, 'user_override_name': 'foo'}
+ dig = DockerImageGenerator(active_extensions, mock_cli_args, 'ubuntu:bionic')
+ self.assertIn(root_snippet_content, dig.dockerfile)
+ self.assertIn('USER foo', dig.dockerfile)
+ self.assertIn(user_snippet_content, dig.dockerfile)
def test_docker_cmd_nocleanup(self):
dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
diff --git a/test/test_extension.py b/test/test_extension.py
index 0c6aa7b4..1ad88d85 100644
--- a/test/test_extension.py
+++ b/test/test_extension.py
@@ -36,7 +36,7 @@ def plugin_load_parser_correctly(plugin):
"""A helper function to test that the plugins at least
register an option for their own name."""
parser = argparse.ArgumentParser(description='test_parser')
- plugin.register_arguments(parser)
+ plugin.register_arguments(parser, {})
argument_name = name_to_argument(plugin.get_name())
for action in parser._actions:
option_strings = getattr(action, 'option_strings', [])
@@ -114,6 +114,40 @@ def test_home_extension(self):
args = p.get_docker_args(mock_cliargs)
self.assertTrue('-v %s:%s' % (Path.home(), Path.home()) in args)
+
+class IpcExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ @pytest.mark.docker
+ def test_ipc_extension(self):
+ plugins = list_plugins()
+ ipc_plugin = plugins['ipc']
+ self.assertEqual(ipc_plugin.get_name(), 'ipc')
+
+ p = ipc_plugin()
+ self.assertTrue(plugin_load_parser_correctly(ipc_plugin))
+
+ mock_cliargs = {'ipc': 'none'}
+ self.assertEqual(p.get_snippet(mock_cliargs), '')
+ self.assertEqual(p.get_preamble(mock_cliargs), '')
+ args = p.get_docker_args(mock_cliargs)
+ self.assertTrue('--ipc none' in args)
+
+ mock_cliargs = {'ipc': 'host'}
+ args = p.get_docker_args(mock_cliargs)
+ self.assertTrue('--ipc host' in args)
+
+
class NetworkExtensionTest(unittest.TestCase):
def setUp(self):
@@ -243,6 +277,37 @@ def test_name_extension(self):
args = p.get_docker_args(mock_cliargs)
self.assertTrue('--name docker_name' in args)
+class HostnameExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ def test_name_extension(self):
+ plugins = list_plugins()
+ name_plugin = plugins['hostname']
+ self.assertEqual(name_plugin.get_name(), 'hostname')
+
+ p = name_plugin()
+ self.assertTrue(plugin_load_parser_correctly(name_plugin))
+
+ mock_cliargs = {'hostname': 'none'}
+ self.assertEqual(p.get_snippet(mock_cliargs), '')
+ self.assertEqual(p.get_preamble(mock_cliargs), '')
+ args = p.get_docker_args(mock_cliargs)
+ self.assertTrue('--hostname none' in args)
+
+ mock_cliargs = {'hostname': 'docker-hostname'}
+ args = p.get_docker_args(mock_cliargs)
+ self.assertTrue('--hostname docker-hostname' in args)
+
class PrivilegedExtensionTest(unittest.TestCase):
@@ -317,9 +382,25 @@ def test_user_extension(self):
self.assertFalse('mkhomedir_helper' in p.get_snippet(home_active_cliargs))
user_override_active_cliargs = mock_cliargs
+ user_override_active_cliargs['user_preserve_groups'] = []
+ snippet_result = p.get_snippet(user_override_active_cliargs)
+ self.assertTrue('usermod -aG' in snippet_result)
+
+ user_override_active_cliargs = mock_cliargs
+ user_override_active_cliargs['user_preserve_groups'] = ['cdrom', 'audio']
+ snippet_result = p.get_snippet(user_override_active_cliargs)
+ self.assertTrue('cdrom' in snippet_result)
+ self.assertTrue('audio' in snippet_result)
+
+ user_override_active_cliargs = mock_cliargs
+ user_override_active_cliargs['user_preserve_groups'] = []
+ user_override_active_cliargs['user_preserve_groups_permissive'] = True
+ snippet_result = p.get_snippet(user_override_active_cliargs)
+ self.assertTrue('usermod -aG' in snippet_result)
+ self.assertTrue('user-preserve-group-permissive Enabled' in snippet_result)
+
user_override_active_cliargs['user_override_name'] = 'testusername'
snippet_result = p.get_snippet(user_override_active_cliargs)
- self.assertTrue('USER testusername' in snippet_result)
self.assertTrue('WORKDIR /home/testusername' in snippet_result)
self.assertTrue('userdel -r' in snippet_result)
@@ -373,6 +454,7 @@ def test_user_collisions(self):
self.assertTrue(exit_code == 0, f"Build failed with exit code {exit_code}")
run_exit_code = dig.run(**build_args)
self.assertTrue(run_exit_code == 0, f"Run failed with exit code {run_exit_code}")
+ dig.clear_image()
# Test colliding UID and name
@@ -383,6 +465,7 @@ def test_user_collisions(self):
self.assertTrue(exit_code == 0, f"Build failed with exit code {exit_code}")
run_exit_code = dig.run(**build_args)
self.assertTrue(run_exit_code == 0, f"Run failed with exit code {run_exit_code}")
+ dig.clear_image()
class PulseExtensionTest(unittest.TestCase):
@@ -500,3 +583,69 @@ def test_env_file_extension(self):
self.assertEqual(p.get_snippet(mock_cliargs), '')
self.assertEqual(p.get_preamble(mock_cliargs), '')
self.assertEqual(p.get_docker_args(mock_cliargs), ' --env-file foo --env-file bar')
+
+
+class GroupAddExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ @pytest.mark.docker
+ def test_group_add_extension(self):
+ plugins = list_plugins()
+ group_add_plugin = plugins['group_add']
+ self.assertEqual(group_add_plugin.get_name(), 'group_add')
+
+ p = group_add_plugin()
+ self.assertTrue(plugin_load_parser_correctly(group_add_plugin))
+
+ mock_cliargs = {}
+ self.assertEqual(p.get_snippet(mock_cliargs), '')
+ self.assertEqual(p.get_preamble(mock_cliargs), '')
+ args = p.get_docker_args(mock_cliargs)
+ self.assertNotIn('--group_add', args)
+
+ mock_cliargs = {'group_add': ['sudo', 'docker']}
+ args = p.get_docker_args(mock_cliargs)
+ self.assertIn('--group-add sudo', args)
+ self.assertIn('--group-add docker', args)
+
+class ShmSizeExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ @pytest.mark.docker
+ def test_shm_size_extension(self):
+ plugins = list_plugins()
+ shm_size_plugin = plugins['shm_size']
+ self.assertEqual(shm_size_plugin.get_name(), 'shm_size')
+
+ p = shm_size_plugin()
+ self.assertTrue(plugin_load_parser_correctly(shm_size_plugin))
+
+ mock_cliargs = {}
+ self.assertEqual(p.get_snippet(mock_cliargs), '')
+ self.assertEqual(p.get_preamble(mock_cliargs), '')
+ args = p.get_docker_args(mock_cliargs)
+ self.assertNotIn('--shm-size', args)
+
+ mock_cliargs = {'shm_size': '12g'}
+ args = p.get_docker_args(mock_cliargs)
+ self.assertIn('--shm-size 12g', args)
\ No newline at end of file
diff --git a/test/test_file_writing.py b/test/test_file_writing.py
index 03194812..e3e84ae0 100644
--- a/test/test_file_writing.py
+++ b/test/test_file_writing.py
@@ -48,15 +48,17 @@ def get_name(cls):
def get_files(self, cliargs):
all_files = {}
- all_files['test_file.txt'] = """The quick brown fox jumped over the lazy dog.
-%s""" % cliargs
+ all_files['test_file.txt'] = """The quick brown fox jumped over the lazy dog. %s""" % cliargs
+ all_files['path/to/test_file.txt'] = """The quick brown fox jumped over the lazy dog. %s""" % cliargs
+ all_files['test_file.bin'] = bytes("""The quick brown fox jumped over the lazy dog. %s""" % cliargs, 'utf-8')
+ all_files['../outside/path/to/test_file.txt'] = """Path outside directory should be skipped"""
all_files['/absolute.txt'] = """Absolute file path should be skipped"""
return all_files
@staticmethod
- def register_arguments(parser, defaults={}):
+ def register_arguments(parser, defaults):
parser.add_argument('--test-file-injection',
action='store_true',
default=defaults.get('test_file_injection', False),
@@ -89,4 +91,17 @@ def test_file_injection(self):
self.assertIn('test_key', content)
self.assertIn('test_value', content)
+ with open(os.path.join(td, 'path/to/test_file.txt'), 'r') as fh:
+ content = fh.read()
+ self.assertIn('quick brown', content)
+ self.assertIn('test_key', content)
+ self.assertIn('test_value', content)
+
+ with open(os.path.join(td, 'test_file.bin'), 'r') as fh: # this particular binary file can be read in text mode
+ content = fh.read()
+ self.assertIn('quick brown', content)
+ self.assertIn('test_key', content)
+ self.assertIn('test_value', content)
+
+ self.assertFalse(os.path.exists('../outside/path/to/test_file.txt'))
self.assertFalse(os.path.exists('/absolute.txt'))
diff --git a/test/test_git_extension.py b/test/test_git_extension.py
index 7bdf0e3c..ef6941f5 100644
--- a/test/test_git_extension.py
+++ b/test/test_git_extension.py
@@ -57,7 +57,7 @@ def test_git_extension(self):
p = git_plugin()
self.assertTrue(plugin_load_parser_correctly(git_plugin))
-
+
mock_cliargs = {}
mock_config_file = tempfile.NamedTemporaryFile()
@@ -79,6 +79,12 @@ def test_git_extension(self):
user_gitconfig_target = os.path.expanduser('~/.gitconfig')
self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), user_args)
+ # Test with an existing overridden user key, but with None value
+ mock_cliargs['user_override_name'] = None
+ user_args = p.get_docker_args(mock_cliargs)
+ user_gitconfig_target = os.path.expanduser('~/.gitconfig')
+ self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), user_args)
+
# Test with overridden user
mock_cliargs['user_override_name'] = 'testusername'
user_args = p.get_docker_args(mock_cliargs)
diff --git a/test/test_nvidia.py b/test/test_nvidia.py
index 29ae0c2c..b5ae2ded 100644
--- a/test/test_nvidia.py
+++ b/test/test_nvidia.py
@@ -172,7 +172,15 @@ def test_nvidia_extension_basic(self):
preamble = p.get_preamble(mock_cliargs)
- self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-', preamble)
+ self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu18.04', preamble)
+
+ mock_cliargs = {'base_image': 'ubuntu:jammy'}
+ preamble = p.get_preamble(mock_cliargs)
+ self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu22.04', preamble)
+
+ mock_cliargs = {'base_image': 'ubuntu:jammy', 'nvidia_glvnd_version': '20.04'}
+ preamble = p.get_preamble(mock_cliargs)
+ self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu20.04', preamble)
docker_args = p.get_docker_args(mock_cliargs)
#TODO(tfoote) restore with #37 self.assertIn(' -e DISPLAY -e TERM', docker_args)
@@ -185,6 +193,21 @@ def test_nvidia_extension_basic(self):
else:
self.assertIn(' --runtime=nvidia', docker_args)
+ mock_cliargs = {'nvidia': 'auto'}
+ docker_args = p.get_docker_args(mock_cliargs)
+ if get_docker_version() >= Version("19.03"):
+ self.assertIn(' --gpus all', docker_args)
+ else:
+ self.assertIn(' --runtime=nvidia', docker_args)
+
+ mock_cliargs = {'nvidia': 'gpus'}
+ docker_args = p.get_docker_args(mock_cliargs)
+ self.assertIn(' --gpus all', docker_args)
+
+ mock_cliargs = {'nvidia': 'runtime'}
+ docker_args = p.get_docker_args(mock_cliargs)
+ self.assertIn(' --runtime=nvidia', docker_args)
+
def test_no_nvidia_glmark2(self):
for tag in self.dockerfile_tags:
@@ -227,15 +250,22 @@ def test_nvidia_env_subs(self):
p.get_environment_subs(mock_cliargs)
self.assertEqual(cm.exception.code, 1)
+@pytest.mark.docker
class CudaTest(unittest.TestCase):
@classmethod
def setUpClass(self):
client = get_docker_client()
self.dockerfile_tags = []
- for distro_version in ['focal', 'jammy']:
+ for (distro_name, distro_version) in [
+ ('ubuntu','focal'),
+ ('ubuntu','jammy'),
+ ('ubuntu','noble'),
+ ('debian','bookworm'),
+ ('debian','bullseye'),
+ ]:
dockerfile = """
-FROM ubuntu:%(distro_version)s
-CMD dpkg -s cuda
+FROM %(distro_name)s:%(distro_version)s
+CMD dpkg -s cuda-toolkit
"""
dockerfile_tag = 'testfixture_%s_cuda' % distro_version
iof = StringIO((dockerfile % locals()).encode())
@@ -257,24 +287,22 @@ def setUp(self):
em.Interpreter._wasProxyInstalled = False
- @pytest.mark.docker
def test_no_cuda(self):
for tag in self.dockerfile_tags:
dig = DockerImageGenerator([], {}, tag)
self.assertEqual(dig.build(), 0)
self.assertNotEqual(dig.run(), 0)
+ dig.clear_image()
- @pytest.mark.nvidia
- @pytest.mark.x11
- @pytest.mark.docker
- def test_cuda(self):
+ def test_cuda_install(self):
plugins = list_plugins()
- desired_plugins = ['x11', 'nvidia', 'cuda'] #TODO(Tfoote) encode the x11 dependency into the plugin and remove from test here
+ desired_plugins = ['cuda']
active_extensions = [e() for e in plugins.values() if e.get_name() in desired_plugins]
for tag in self.dockerfile_tags:
dig = DockerImageGenerator(active_extensions, {}, tag)
self.assertEqual(dig.build(), 0)
self.assertEqual(dig.run(), 0)
+ dig.clear_image()
def test_cuda_env_subs(self):
plugins = list_plugins()
diff --git a/test/test_rmw_extension.py b/test/test_rmw_extension.py
new file mode 100644
index 00000000..5c40a458
--- /dev/null
+++ b/test/test_rmw_extension.py
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import em
+import os
+import unittest
+import pytest
+
+
+from rocker.core import DockerImageGenerator
+from rocker.core import list_plugins
+
+from test_extension import plugin_load_parser_correctly
+
+
+
+class rmwExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ def test_rmw_extension(self):
+ plugins = list_plugins()
+ rmw_plugin = plugins['rmw']
+ self.assertEqual(rmw_plugin.get_name(), 'rmw')
+
+ p = rmw_plugin()
+ self.assertTrue(plugin_load_parser_correctly(rmw_plugin))
+
+
+ mock_cliargs = {'rmw': ['cyclonedds']}
+ self.assertEqual(p.get_preamble(mock_cliargs), '')
+ args = p.get_docker_args(mock_cliargs)
+ self.assertIn('-e RMW_IMPLEMENTATION=rmw_cyclonedds_cpp', args)
+ snippet = p.get_snippet(mock_cliargs)
+ self.assertIn('rmw-cyclonedds-cpp', snippet)
+
+
+ #without it set
+ mock_cliargs = {'rmw': None}
+ args = p.get_docker_args(mock_cliargs)
+ snippet = p.get_snippet(mock_cliargs)
+ self.assertNotIn('RMW_IMPLEMENTATION', args)
+ self.assertNotIn('rmw-cyclonedds-cpp', snippet)
+
+
+@pytest.mark.docker
+class rmwRuntimeExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ # Work around interference between empy Interpreter
+ # stdout proxy and test runner. empy installs a proxy on stdout
+ # to be able to capture the information.
+ # And the test runner creates a new stdout object for each test.
+ # This breaks empy as it assumes that the proxy has persistent
+ # between instances of the Interpreter class
+ # empy will error with the exception
+ # "em.Error: interpreter stdout proxy lost"
+ em.Interpreter._wasProxyInstalled = False
+
+ def test_rmw_extension(self):
+ plugins = list_plugins()
+ rmw_plugin = plugins['rmw']
+
+ p = rmw_plugin()
+ self.assertTrue(plugin_load_parser_correctly(rmw_plugin))
+
+ mock_cliargs = {'rmw': ['cyclonedds']}
+ dig = DockerImageGenerator([rmw_plugin()], mock_cliargs, 'ros:rolling')
+ self.assertEqual(dig.build(), 0)
+ self.assertEqual(dig.run(command='dpkg -l ros-rolling-rmw-cyclonedds-cpp'), 0)
+ self.assertIn('-e RMW_IMPLEMENTATION=rmw_cyclonedds_cpp', dig.generate_docker_cmd('', mode='dry-run'))
+ dig.clear_image()
diff --git a/test/test_ulimit.py b/test/test_ulimit.py
new file mode 100644
index 00000000..4db8a6d4
--- /dev/null
+++ b/test/test_ulimit.py
@@ -0,0 +1,100 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+from argparse import ArgumentTypeError
+
+from rocker.ulimit_extension import Ulimit
+
+
+class UlimitTest(unittest.TestCase):
+ """Unit tests for the Ulimit class."""
+
+ def setUp(self):
+ self._instance = Ulimit()
+
+ def _is_arg_translation_ok(self, mock_cliargs, expected):
+ is_ok = False
+ message_string = ""
+ try:
+ docker_args = self._instance.get_docker_args(
+ {self._instance.get_name(): [mock_cliargs]})
+ is_ok = docker_args == expected
+ message_string = f"Expected: '{expected}', got: '{docker_args}'"
+ except ArgumentTypeError:
+ message_string = "Incorrect argument format"
+ return (is_ok, message_string)
+
+ def test_args_single_soft(self):
+ """Test single soft limit argument."""
+ mock_cliargs = ["rtprio=99"]
+ expected = " --ulimit rtprio=99"
+ self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_multiple_soft(self):
+ """Test multiple soft limit arguments."""
+ mock_cliargs = ["rtprio=99", "memlock=102400"]
+ expected = " --ulimit rtprio=99 --ulimit memlock=102400"
+ self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_single_hard(self):
+ """Test single hard limit argument."""
+ mock_cliargs = ["nofile=1024:524288"]
+ expected = " --ulimit nofile=1024:524288"
+ self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_multiple_hard(self):
+ """Test multiple hard limit arguments."""
+ mock_cliargs = ["nofile=1024:524288", "rtprio=90:99"]
+ expected = " --ulimit nofile=1024:524288 --ulimit rtprio=90:99"
+ self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_multiple_mix(self):
+ """Test multiple mixed limit arguments."""
+ mock_cliargs = ["rtprio=99", "memlock=102400", "nofile=1024:524288"]
+ expected = " --ulimit rtprio=99 --ulimit memlock=102400 --ulimit nofile=1024:524288"
+ self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_wrong_single_soft(self):
+ """Test if single soft limit argument is wrong."""
+ mock_cliargs = ["rtprio99"]
+ expected = " --ulimit rtprio99"
+ self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_wrong_multiple_soft(self):
+ """Test if multiple soft limit arguments are wrong."""
+ mock_cliargs = ["rtprio=99", "memlock102400"]
+ expected = " --ulimit rtprio=99 --ulimit memlock=102400"
+ self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_wrong_single_hard(self):
+ """Test if single hard limit arguments are wrong."""
+ mock_cliargs = ["nofile=1024:524288:"]
+ expected = " --ulimit nofile=1024:524288"
+ self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_wrong_multiple_hard(self):
+ """Test if multiple hard limit arguments are wrong."""
+ mock_cliargs = ["nofile1024524288", "rtprio=90:99"]
+ expected = " --ulimit nofile=1024:524288 --ulimit rtprio=90:99"
+ self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
+
+ def test_args_wrong_multiple_mix(self):
+ """Test if multiple mixed limit arguments are wrong."""
+ mock_cliargs = ["rtprio=:", "memlock102400", "nofile1024:524288:"]
+ expected = " --ulimit rtprio=99 --ulimit memlock=102400 --ulimit nofile=1024:524288"
+ self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
diff --git a/test/test_volume.py b/test/test_volume.py
index d9f4fda5..571599bc 100644
--- a/test/test_volume.py
+++ b/test/test_volume.py
@@ -45,19 +45,19 @@ def test_args_single(self):
"""Passing source path"""
arg = [[self._curr_path]]
expected = [['{}:{}'.format(self._curr_path, self._curr_path)]]
- mock_cliargs = {Volume.name: arg}
+ mock_cliargs = {Volume.get_name(): arg}
self._test_equals_args(mock_cliargs, expected)
def test_args_twopaths(self):
"""Passing source path, dest path"""
arg = ["{}:{}".format(self._curr_path, self._virtual_path)]
- mock_cliargs = {Volume.name: [arg]}
+ mock_cliargs = {Volume.get_name(): [arg]}
self._test_equals_args(mock_cliargs, arg)
def test_args_twopaths_opt(self):
"""Passing source path, dest path, and Docker's volume option"""
arg = ["{}:{}:ro".format(self._curr_path, self._virtual_path)]
- mock_cliargs = {Volume.name: [arg]}
+ mock_cliargs = {Volume.get_name(): [arg]}
self._test_equals_args(mock_cliargs, arg)
def test_args_two_volumes(self):
@@ -65,5 +65,5 @@ def test_args_two_volumes(self):
arg_first = ["{}:{}:ro".format(self._curr_path, self._virtual_path)]
arg_second = ["/tmp:{}".format(os.path.join(self._virtual_path, "tmp"))]
args = [arg_first, arg_second]
- mock_cliargs = {Volume.name: args}
+ mock_cliargs = {Volume.get_name(): args}
self._test_equals_args(mock_cliargs, args)