├── .github
├── CODEOWNERS
└── workflows
│ ├── basic-ci.yaml
│ └── mirror-main-to-master.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── codecov.yml
├── setup.cfg
├── setup.py
├── src
└── rocker
│ ├── __init__.py
│ ├── cli.py
│ ├── core.py
│ ├── em.py
│ ├── extensions.py
│ ├── git_extension.py
│ ├── nvidia_extension.py
│ ├── os_detector.py
│ ├── rmw_extension.py
│ ├── ssh_extension.py
│ ├── templates
│ ├── Dockerfile.em
│ ├── bash_cmd_snippet.Dockerfile.em
│ ├── cuda_snippet.Dockerfile.em
│ ├── dev_helpers_snippet.Dockerfile.em
│ ├── nvidia_preamble.Dockerfile.em
│ ├── nvidia_snippet.Dockerfile.em
│ ├── pulse_snippet.Dockerfile.em
│ ├── rmw_snippet.Dockerfile.em
│ └── user_snippet.Dockerfile.em
│ ├── ulimit_extension.py
│ └── volume_extension.py
├── stdeb.cfg
├── test
├── test_core.py
├── test_extension.py
├── test_file_writing.py
├── test_git_extension.py
├── test_nvidia.py
├── test_os_detect.py
├── test_rmw_extension.py
├── test_ssh_extension.py
├── test_ulimit.py
└── test_volume.py
└── wishlist.txt
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Global codeowners
2 |
3 | * @tfoote
4 |
--------------------------------------------------------------------------------
/.github/workflows/basic-ci.yaml:
--------------------------------------------------------------------------------
1 | name: Basic CI
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | basic_ci:
6 | name: Basic CI
7 | runs-on: ubuntu-22.04
8 | strategy:
9 | matrix:
10 | python-version: ['3.8', 3.x]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install Test Dependencies And Self
18 | run: |
19 | python -m pip install --upgrade pip setuptools wheel
20 | python -m pip install -e .[test] codecov pytest-cov
21 | - name: Run headless tests
22 | uses: coactions/setup-xvfb@v1
23 | with:
24 | run: python -m pytest -s -v --cov=rocker -m "not nvidia"
25 | - name: Upload coverage to Codecov
26 | uses: codecov/codecov-action@v3
27 | cli_smoke_tests:
28 | name: CLI smoke tests
29 | runs-on: ubuntu-22.04
30 | strategy:
31 | matrix:
32 | python-version: [3.8, '3.x']
33 | steps:
34 | - uses: actions/checkout@v4
35 | - name: Set up Python ${{ matrix.python-version }}
36 | uses: actions/setup-python@v5
37 | with:
38 | python-version: ${{ matrix.python-version }}
39 | - name: Install Dependencies And Self
40 | run: |
41 | python -m pip install --upgrade pip setuptools wheel
42 | python -m pip install -e .
43 | - name: Check os_detector script
44 | run: |
45 | detect_docker_image_os ubuntu
46 | - name: Check detector help
47 | run: |
48 | detect_docker_image_os ubuntu -h
49 | - name: Check main runs
50 | run: |
51 | rocker ubuntu 'true'
52 | - name: Check rocker help
53 | run: |
54 | rocker -h
55 |
56 |
--------------------------------------------------------------------------------
/.github/workflows/mirror-main-to-master.yaml:
--------------------------------------------------------------------------------
1 | name: Mirror main to master
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | mirror-to-master:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: zofrex/mirror-branch@v1
12 | with:
13 | target-branch: master
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | __pycache__
3 | dist
4 | deb_dist
5 | *.pyc
6 | .coverage
7 | build
8 | rocker_venv
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7 |
8 |
9 | ## [v0.2.19](https://github.com/osrf/rocker/releases/tag/v0.2.19) - 2025-02-06
10 |
11 | [Compare with v0.2.18](https://github.com/osrf/rocker/compare/v0.2.18...v0.2.19)
12 |
13 | ### Added
14 |
15 | - add rmw zenoh support to rmw plugin (#315) ([5c6e8e7](https://github.com/osrf/rocker/commit/5c6e8e7fe45b34868bb74a40610c69b293341505) by Tully Foote).
16 |
17 | - Improve robustness of image cleanup (#313) ([149d4ef
18 | ](https://github.com/osrf/rocker/commit/149d4ef86ab5b42e7373066ba84ca1e985555011))
19 |
20 | - Enable redirecting non-interactive run console output to file (#317) ([66fe159](https://github.com/osrf/rocker/commit/66fe159e3be5a80d3ba189320bf064da84371e73))
21 |
22 | ## [v0.2.18](https://github.com/osrf/rocker/releases/tag/v0.2.18) - 2025-01-10
23 |
24 | [Compare with v0.2.17](https://github.com/osrf/rocker/compare/v0.2.17...v0.2.18)
25 |
26 | ### Added
27 |
28 | - Add UI feedback that image is being cleaned up, and how to avoid. ([3b21f78](https://github.com/osrf/rocker/commit/3b21f78fd4372a5451611a2a3e607f6941bcc398) by Tully Foote).
29 | - 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).
30 | - Add support for ulimit flag (#291) ([cff5cb2](https://github.com/osrf/rocker/commit/cff5cb27c04f4db8d115493c2e2704c6a10726df) by Felipe Padula Sanches).
31 | - Add support for --shm-size flag (#306)
32 |
33 | ### Fixed
34 |
35 | - fix test for new cuda installation package ([3449afa](https://github.com/osrf/rocker/commit/3449afabd700c4723d21c22163e73f9cbf9b358d) by Tully Foote).
36 | - Removed deprecation warning from volume extensino (#292)
37 | - Updated and simplified CUDA installation (#299 by jonazpiazu)
38 |
39 | ### Removed
40 |
41 | - Remove default value for defaults (#289) ([3f02bdc](https://github.com/osrf/rocker/commit/3f02bdcc542c786eb08bdb3b324887bfa21698a2) by Tully Foote).
42 |
43 | ## [v0.2.17](https://github.com/osrf/rocker/releases/tag/v0.2.17) - 2024-08-28
44 |
45 | [Compare with v0.2.16](https://github.com/osrf/rocker/compare/v0.2.16...v0.2.17)
46 |
47 | ### Added
48 | - [nvidia_extension] add 24.04 to supported_versions (#279)
49 |
50 | ## [v0.2.16](https://github.com/osrf/rocker/releases/tag/v0.2.16) - 2024-04-01
51 |
52 | [Compare with v0.2.15](https://github.com/osrf/rocker/compare/v0.2.15...v0.2.16)
53 |
54 | ### Fixed
55 |
56 | - Fix --user arg when using empy 4 (#276) ([4078d22](https://github.com/osrf/rocker/commit/4078d223a2158e9a6fea31d8871dc7df7ee094f0) by Gary Servin).
57 |
58 | ## [v0.2.15](https://github.com/osrf/rocker/releases/tag/v0.2.15) - 2024-03-01
59 |
60 | [Compare with v0.2.14](https://github.com/osrf/rocker/compare/v0.2.14...v0.2.15)
61 |
62 | ### Added
63 |
64 | - Add changelog ([39a2842](https://github.com/osrf/rocker/commit/39a2842d03fec73b4b0d70c3922c4ab1c93c2cc0) by Tully Foote).
65 | - Add a shorthand option for switching to alternate rmws (#268) ([35e68c1](https://github.com/osrf/rocker/commit/35e68c17232a50cfb3a781b066e2c0d6691cb07e) by Tully Foote).
66 |
67 | ## [v0.2.14](https://github.com/osrf/rocker/releases/tag/v0.2.14) - 2024-01-26
68 |
69 | [Compare with v0.2.13](https://github.com/osrf/rocker/compare/v0.2.13...v0.2.14)
70 |
71 | ### Removed
72 |
73 | - Remove deprecated use of pkg_resources (#265) ([998761f](https://github.com/osrf/rocker/commit/998761f919302f29db2377f53efb5cb472c335fa) by Tully Foote).
74 |
75 | ## [v0.2.13](https://github.com/osrf/rocker/releases/tag/v0.2.13) - 2023-12-10
76 |
77 | [Compare with v0.2.12](https://github.com/osrf/rocker/compare/v0.2.12...v0.2.13)
78 |
79 | ### Added
80 |
81 | - Add indirection for changes in empy 3 vs 4 (#261) ([4de56c1](https://github.com/osrf/rocker/commit/4de56c18cdac09f3a9ec62a6b81cca7a4cdd46aa) by Tully Foote).
82 | - add simple test of CLI (#256) ([6d276a4](https://github.com/osrf/rocker/commit/6d276a4fe965f50246e2ff5eb0a03c9f16ed06c8) by Tully Foote).
83 | - Add extension ordering (#242) ([3afa6ac](https://github.com/osrf/rocker/commit/3afa6acbf6b0323f540ac16c2a0091b29099086a) by agyoungs).
84 |
85 | ### Fixed
86 |
87 | - fix: nvidia arg (#234) ([2b8d5ab](https://github.com/osrf/rocker/commit/2b8d5abb18c829d22f290679b739dc53765198ad) by Amadeusz Szymko).
88 | - Fix nvidia runtime fail on arm64 & add --group-add plugin (#211) ([e01d9cc](https://github.com/osrf/rocker/commit/e01d9cca8672e0a85b7ff42118ee456c4c414d9e) by Amadeusz Szymko).
89 |
90 | ### Removed
91 |
92 | - Remove old tutorial links (#254) ([41502b3](https://github.com/osrf/rocker/commit/41502b3f41e7d36bc108d9685bce61b6d27286bc) by Tully Foote).
93 |
94 | ## [v0.2.12](https://github.com/osrf/rocker/releases/tag/v0.2.12) - 2023-05-04
95 |
96 | [Compare with v0.2.11](https://github.com/osrf/rocker/compare/v0.2.11...v0.2.12)
97 |
98 | ### Fixed
99 |
100 | - Fix default logic for user-preserve-groups (#224) ([04cfc29](https://github.com/osrf/rocker/commit/04cfc290351a60b7df28c7eff67dc2c27e9ab918) by Tully Foote).
101 |
102 | ## [v0.2.11](https://github.com/osrf/rocker/releases/tag/v0.2.11) - 2023-05-04
103 |
104 | [Compare with v0.2.10](https://github.com/osrf/rocker/compare/v0.2.10...v0.2.11)
105 |
106 | ### Added
107 |
108 | - adding debian bookworm support ([b00c1c6](https://github.com/osrf/rocker/commit/b00c1c6e2832c6d375f8b12bcee8fe74642ef043) by Tully Foote).
109 | - add error messages for group issues ([4ce4e29](https://github.com/osrf/rocker/commit/4ce4e29041ac81830f78e1b00afbff9168194929) by Tully Foote).
110 | - 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).
111 | - add link to mp_rocker (#213) ([49a23e7](https://github.com/osrf/rocker/commit/49a23e70a3055898daba80fc35f1742dc01d9a09) by Tully Foote).
112 | - Add ability to preserve host user groups inside container ([b16136e](https://github.com/osrf/rocker/commit/b16136e589ace3519e4b5c85697e770b29a151fa) by Miguel Prada).
113 | - Add port and expose extensions with tests (#201) ([2770045](https://github.com/osrf/rocker/commit/27700451d0b1a0701d8fe5c80c64a52942b61ce0) by Will Baker).
114 | - Added note for linking Intel Xe cards with rocker (#190) ([3a2bf74](https://github.com/osrf/rocker/commit/3a2bf74209f577f666857054095a57bbeb3c6c1d) by Zahi Kakish).
115 |
116 | ### Removed
117 |
118 | - Removed '-v' for rocker's version. Only --version should be available. (#205) ([67a4142](https://github.com/osrf/rocker/commit/67a414278c5940d587b9e3060eb7ab9e2e293510) by George Stavrinos).
119 |
120 | ## [v0.2.10](https://github.com/osrf/rocker/releases/tag/v0.2.10) - 2022-07-30
121 |
122 | [Compare with v0.2.9](https://github.com/osrf/rocker/compare/v0.2.9...v0.2.10)
123 |
124 | ## [v0.2.9](https://github.com/osrf/rocker/releases/tag/v0.2.9) - 2022-03-23
125 |
126 | [Compare with v0.2.8](https://github.com/osrf/rocker/compare/v0.2.8...v0.2.9)
127 |
128 | ## [v0.2.8](https://github.com/osrf/rocker/releases/tag/v0.2.8) - 2022-02-14
129 |
130 | [Compare with v0.2.7](https://github.com/osrf/rocker/compare/v0.2.7...v0.2.8)
131 |
132 | ## [v0.2.7](https://github.com/osrf/rocker/releases/tag/v0.2.7) - 2021-12-01
133 |
134 | [Compare with v0.2.6](https://github.com/osrf/rocker/compare/v0.2.6...v0.2.7)
135 |
136 | ### Added
137 |
138 | - 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).
139 |
140 | ## [v0.2.6](https://github.com/osrf/rocker/releases/tag/v0.2.6) - 2021-10-05
141 |
142 | [Compare with v0.2.5](https://github.com/osrf/rocker/compare/v0.2.5...v0.2.6)
143 |
144 | ## [v0.2.5](https://github.com/osrf/rocker/releases/tag/v0.2.5) - 2021-10-05
145 |
146 | [Compare with v0.2.4](https://github.com/osrf/rocker/compare/v0.2.4...v0.2.5)
147 |
148 | ### Added
149 |
150 | - Add an option to tag the image (#159) ([ef09014](https://github.com/osrf/rocker/commit/ef0901419dd99132bef5c1b797892927e63f5925) by Tully Foote).
151 |
152 | ## [v0.2.4](https://github.com/osrf/rocker/releases/tag/v0.2.4) - 2021-08-03
153 |
154 | [Compare with 0.2.3](https://github.com/osrf/rocker/compare/0.2.3...v0.2.4)
155 |
156 | ### Added
157 |
158 | - 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).
159 | - Add the privileged extension (#132) ([4214d73](https://github.com/osrf/rocker/commit/4214d73aeb3de235153905344d14ee18edc3c229) by Gaël Écorchard).
160 | - Add --nocleanup option (#111) ([5507dc5](https://github.com/osrf/rocker/commit/5507dc510c3e339beb3573e52dcc77d8585bb285) by Tim Redick).
161 |
162 | ### Fixed
163 |
164 | - Fix detector for the deprecated distro syntax (#156) ([2760838](https://github.com/osrf/rocker/commit/2760838dc4bc8c3bd436344ec8ab9225ad914696) by Tully Foote).
165 | - Fix os detection for non-root images (cont.) (#150) ([8ecaf53](https://github.com/osrf/rocker/commit/8ecaf530cc374b30be121eef7a324b8200788eea) by Miguel Prada).
166 |
167 | ### Removed
168 |
169 | - Remove intermediate containers and name os_detect images (#133) ([80602eb](https://github.com/osrf/rocker/commit/80602eb6868a9044279d88c13650828f654a32f9) by Daniel Stonier).
170 | - Remove redundant device additions to docker_args ([a759fe6](https://github.com/osrf/rocker/commit/a759fe6b241edf37b95d104f238eb6ef9fb3d70b) by Peter Polidoro).
171 |
172 | ## [0.2.3](https://github.com/osrf/rocker/releases/tag/0.2.3) - 2020-11-25
173 |
174 | [Compare with v0.2.2](https://github.com/osrf/rocker/compare/v0.2.2...0.2.3)
175 |
176 | ### Added
177 |
178 | - add x11 flags to examples (#104) ([ae7d3f6](https://github.com/osrf/rocker/commit/ae7d3f6edcd74cb74b345f005f401061b33bda3a) by Tully Foote).
179 | - 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).
180 | - Added --name argument ([019a543](https://github.com/osrf/rocker/commit/019a543afb759664751afe847162ab9ba09db889) by ahcorde).
181 |
182 | ### Fixed
183 |
184 | - Fix using rocker for non-root containers (#50) ([3585298](https://github.com/osrf/rocker/commit/35852984cc4554bbfcf55f3612fa17cc89d5d715) by Johannes Meyer).
185 |
186 | ## [v0.2.2](https://github.com/osrf/rocker/releases/tag/v0.2.2) - 2020-06-24
187 |
188 | [Compare with v0.2.1](https://github.com/osrf/rocker/compare/v0.2.1...v0.2.2)
189 |
190 | ## [v0.2.1](https://github.com/osrf/rocker/releases/tag/v0.2.1) - 2020-06-24
191 |
192 | [Compare with v0.2.0](https://github.com/osrf/rocker/compare/v0.2.0...v0.2.1)
193 |
194 | ### Added
195 |
196 | - add bullseye target too ([b9f407b](https://github.com/osrf/rocker/commit/b9f407ba5423c61bd9dd0a46f36d21cd7e795adf) by Tully Foote).
197 | - add focal as a target ([55b704f](https://github.com/osrf/rocker/commit/55b704f575dc5f6af1150e29fb18faf51cfa3336) by Tully Foote).
198 |
199 | ## [v0.2.0](https://github.com/osrf/rocker/releases/tag/v0.2.0) - 2020-05-08
200 |
201 | [Compare with v0.1.10](https://github.com/osrf/rocker/compare/v0.1.10...v0.2.0)
202 |
203 | ### Added
204 |
205 | - Add support for focal and buster (#83) ([a76acb2](https://github.com/osrf/rocker/commit/a76acb2aafb1c5d5609e923b42d8d2a351e28906) by Tully Foote).
206 | - Add support for --env-file option pass through (#82) ([135e73f](https://github.com/osrf/rocker/commit/135e73fc11cb1a6751cda6253efa26d543345e47) by Tully Foote).
207 | - Add an Extension Manager class (#77) ([413d25a](https://github.com/osrf/rocker/commit/413d25a7e578d4eab410eda99042cabb8dc8026b) by Tully Foote).
208 | - add codeowners so I get auto review requested (#74) ([15b604b](https://github.com/osrf/rocker/commit/15b604be5d1a940e4e242dbebff45d9298fcb787) by Tully Foote).
209 |
210 | ### Fixed
211 |
212 | - Fix documentation for User extension (#81) ([6e9ee9c](https://github.com/osrf/rocker/commit/6e9ee9c96aa2664b51974724377cd771c465dbbf) by Emerson Knapp).
213 | - Fix import paths (#76) ([d26004b](https://github.com/osrf/rocker/commit/d26004b2dcc919bf08ae7aeeef0e669f0a66a7f4) by Tully Foote).
214 |
215 | ## [v0.1.10](https://github.com/osrf/rocker/releases/tag/v0.1.10) - 2019-12-05
216 |
217 | [Compare with v0.1.9](https://github.com/osrf/rocker/compare/v0.1.9...v0.1.10)
218 |
219 | ## [v0.1.9](https://github.com/osrf/rocker/releases/tag/v0.1.9) - 2019-10-14
220 |
221 | [Compare with v0.1.8](https://github.com/osrf/rocker/compare/v0.1.8...v0.1.9)
222 |
223 | ### Added
224 |
225 | - Add verbosity outputs ([6246570](https://github.com/osrf/rocker/commit/6246570e1630592c9426e64c7477d53339edc568) by Tully Foote).
226 |
227 | ### Removed
228 |
229 | - Remove xvfb-run which started failing CI. (#69) ([d38927a](https://github.com/osrf/rocker/commit/d38927ab13ffe6ccf84bb9408261feb6a3df3231) by Tully Foote).
230 |
231 | ## [v0.1.8](https://github.com/osrf/rocker/releases/tag/v0.1.8) - 2019-09-18
232 |
233 | [Compare with v0.1.7](https://github.com/osrf/rocker/compare/v0.1.7...v0.1.8)
234 |
235 | ### Added
236 |
237 | - Add a version cli option (#65) ([28fc9ff](https://github.com/osrf/rocker/commit/28fc9ff1fab435ab71c1077ac6882380d38d66f1) by Tully Foote).
238 |
239 | ### Fixed
240 |
241 | - FIx the window resize (#66) ([7757df0](https://github.com/osrf/rocker/commit/7757df091bee54714368a2d08236f6ba9caf4bc5) by Tully Foote).
242 |
243 | ## [v0.1.7](https://github.com/osrf/rocker/releases/tag/v0.1.7) - 2019-09-17
244 |
245 | [Compare with v0.1.6](https://github.com/osrf/rocker/compare/v0.1.6...v0.1.7)
246 |
247 | ### Added
248 |
249 | - add --home (#64) ([733e29b](https://github.com/osrf/rocker/commit/733e29b7521a4037ba381d3d219cabaf3855a608) by Tully Foote).
250 | - Add sigwinch passthrough for xterm resizing (#57) ([efec0f9](https://github.com/osrf/rocker/commit/efec0f964a12b239f6c201498b2aa478fe56d83c) by Ruffin).
251 | - Add new extensions git and ssh (#58) ([5405f3f](https://github.com/osrf/rocker/commit/5405f3fc824b3948772c6b60efff4370954497d3) by Johannes Meyer).
252 |
253 | ### Fixed
254 |
255 | - Fix sudo with user extension for some usernames (#55) ([0bc20b8](https://github.com/osrf/rocker/commit/0bc20b84c72d16b5f81dda59283c6e242e203938) by Johannes Meyer).
256 |
257 | ## [v0.1.6](https://github.com/osrf/rocker/releases/tag/v0.1.6) - 2019-08-29
258 |
259 | [Compare with 0.1.5](https://github.com/osrf/rocker/compare/0.1.5...v0.1.6)
260 |
261 | ## [0.1.5](https://github.com/osrf/rocker/releases/tag/0.1.5) - 2019-08-28
262 |
263 | [Compare with 0.1.4](https://github.com/osrf/rocker/compare/0.1.4...0.1.5)
264 |
265 | ### Added
266 |
267 | - Add extension to pass custom environment variables (#51) ([e27cc0b](https://github.com/osrf/rocker/commit/e27cc0bb8d6677807a9c791470b023253569fedf) by Johannes Meyer).
268 | - Add Cosmic, Disco, and Eoan suites. ([daf23a7](https://github.com/osrf/rocker/commit/daf23a7ca4f10e557e1dcdf9edb854b83d2186bf) by Steven! Ragnarök).
269 |
270 | ### Fixed
271 |
272 | - Fix OS detection (fix #43) ([7a419f9](https://github.com/osrf/rocker/commit/7a419f91b0e8aa0fb0beae040844b4ed77e3f7a1) by Johannes Meyer).
273 |
274 | ### Removed
275 |
276 | - remove unused imports ([39118bb](https://github.com/osrf/rocker/commit/39118bb877553364f70e082be24aaf07559f7ef4) by Tully Foote).
277 |
278 | ## [0.1.4](https://github.com/osrf/rocker/releases/tag/0.1.4) - 2019-03-13
279 |
280 | [Compare with 0.1.3](https://github.com/osrf/rocker/compare/0.1.3...0.1.4)
281 |
282 | ### Added
283 |
284 | - add documentation about how to use an intel integrated graphics card ([f03299f](https://github.com/osrf/rocker/commit/f03299ffddeb3d050f1f16cb048e2a9983895d90) by Tully Foote).
285 | - Add No-Python2 flag ([101813f](https://github.com/osrf/rocker/commit/101813fa2fdf75d80e628ee0871d8cb238e11337) by Tully Foote).
286 | - adding coverage of nvidia ([6f8121f](https://github.com/osrf/rocker/commit/6f8121f7b5c5aad379708ea468c01b97671af159) by Tully Foote).
287 | - add coverage for extensions (#32) ([ac3afc5](https://github.com/osrf/rocker/commit/ac3afc5592771e45f866f22ba4357346f4353db4) by Tully Foote).
288 | - adding a few basic unit tests (#30) ([ea951b7](https://github.com/osrf/rocker/commit/ea951b77cfaae6fd6921555779bfb195c748402e) by Tully Foote).
289 | - Add codecov reports (#28) ([9ef5f36](https://github.com/osrf/rocker/commit/9ef5f361cdd94f1361a48c80deae48b5a58dea15) by Tully Foote).
290 | - adding basic travis (#27) ([b538350](https://github.com/osrf/rocker/commit/b538350fcef5cc08eba4ad39a0911127270f9a20) by Tully Foote).
291 | - Add unit tests for os_detection ([4b42a2d](https://github.com/osrf/rocker/commit/4b42a2de5d5dd9df7f8af6fed17c0080fdc4cbe8) by Tully Foote).
292 | - add dependencies to backport script ([0df081e](https://github.com/osrf/rocker/commit/0df081efe22e8c4853c4bc1b942d7a2fe89aba9a) by Tully Foote).
293 |
294 | ### Fixed
295 |
296 | - 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).
297 |
298 | ## [0.1.3](https://github.com/osrf/rocker/releases/tag/0.1.3) - 2019-01-10
299 |
300 | [Compare with 0.1.2](https://github.com/osrf/rocker/compare/0.1.2...0.1.3)
301 |
302 | ### Added
303 |
304 | - add comment about python3-distro ([e28b546](https://github.com/osrf/rocker/commit/e28b54661dcd2c3fbc08f677d228c2ce7e649e3e) by Tully Foote).
305 |
306 | ### Fixed
307 |
308 | - fix dependencies ([fafcab5](https://github.com/osrf/rocker/commit/fafcab54b24ec6cf0aa92b3adad3a16057717829) by Tully Foote).
309 |
310 | ## [0.1.2](https://github.com/osrf/rocker/releases/tag/0.1.2) - 2019-01-09
311 |
312 | [Compare with first commit](https://github.com/osrf/rocker/compare/abc236cbf234c8ac7bff30b865836aefb751dbef...0.1.2)
313 |
314 | ### Added
315 |
316 | - add command explicitly to argument parsing ([5e308a6](https://github.com/osrf/rocker/commit/5e308a61443c721f4a0de9129040f9898f538a04) by Tully Foote).
317 | - Add tests for extensions ([50ac127](https://github.com/osrf/rocker/commit/50ac1278a488049ae6ce4325356151e24f4df4b7) by Tully Foote).
318 | - add a few ignores core cleaner workspace diffs ([b498f03](https://github.com/osrf/rocker/commit/b498f031e50acff8c5e9d62c2e41365b289dd808) by Tully Foote).
319 | - Add stdeb.cfg ([034abf1](https://github.com/osrf/rocker/commit/034abf1e41ed0ea0c6c5abf2703377257a660e24) by Tully Foote).
320 | - add support for mounting devices, with a soft fail on not existing ([d3244ab](https://github.com/osrf/rocker/commit/d3244ab8f788bbc1e4355000e99176502d7f7d47) by Tully Foote).
321 | - adding copyright headers ([601a156](https://github.com/osrf/rocker/commit/601a1561ccb8bbb2107e85a8afce14f623c12a04) by Tully Foote).
322 | - 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).
323 | - add to the wishlist ([6e224e2](https://github.com/osrf/rocker/commit/6e224e2435cedb74b3c42e727bd388190610f46b) by Tully Foote).
324 | - add readme ([fa0d251](https://github.com/osrf/rocker/commit/fa0d251779bbf2add0842dcaefb40464451f3fd6) by Tully Foote).
325 | - add a readme ([9d6bd66](https://github.com/osrf/rocker/commit/9d6bd6636d858e2b2bac3cb8ae74f683cad311b1) by Tully Foote).
326 | - add parameter for disabling caching ([79a6beb](https://github.com/osrf/rocker/commit/79a6beb9301e0c446d9fe79d25d54067094c5c4d) by Tully Foote).
327 |
328 | ### Fixed
329 |
330 | - fix assert spacing ([02601da](https://github.com/osrf/rocker/commit/02601da2fa9413192fa96df32cb65bf8a07cb9c6) by Tully Foote).
331 | - fix network argument parsing ([d8b12af](https://github.com/osrf/rocker/commit/d8b12af21fe6ebf7bafdadf8e5f36e151ceb0ec6) by Tully Foote).
332 | - fix docker API usage for pull ([a749985](https://github.com/osrf/rocker/commit/a749985104855bc44a5615eb8e25aa63833bd27a) by Tully Foote).
333 |
334 | ### Removed
335 |
336 | - remove legacy test function ([4c1466a](https://github.com/osrf/rocker/commit/4c1466a217e4e150aa722836b3768abcd2efe425) by Tully Foote).
337 | - remove legacy comments ([38b5903](https://github.com/osrf/rocker/commit/38b59034eb373ff12619529383dc8812446890ad) by Tully Foote).
338 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rocker
2 |
3 | A tool to run docker images with customized local support injected for things like nvidia support. And user id specific files for cleaner mounting file permissions.
4 |
5 | ## Difference from docker-compose
6 |
7 | A common question about rocker is how is it different than `docker-compose`.
8 | `rocker` is designed to solve a similar but different problem than `docker-compose`.
9 | The primary goal of `rocker` is to support the use of Docker in use cases where the containers will be effected by the local environment.
10 | A primary example of this is setting up file permissions inside the container to match the users outside of the container so that mounted files inside the container have the same UID as the host.
11 | Doing this enables quickly going in and out of different containrs while leverating the same workspace on your host for testing on different platforms etc.
12 | This is done by dynamically generating overlays on the same core image after detecting the local conditions required.
13 |
14 | The secondary feature that `rocker` provides that docker-compose does not address is the ability to inject extra use case specific capabilities into a container before running.
15 | A common example is the ability to use NVIDIA drivers on a standard published image.
16 | `rocker` will take that standard published image and both inject the necessary drivers into the container which will match your host driver and simultaneously set the correct runtime flags.
17 | This is possible to do with docker-compose or straight docker.
18 | But the drawbacks are that you have to build and publish the combinatoric images of all possible drivers, and in addition you need to manually make sure to pass all the correct runtime arguments.
19 | This is especially true if you want to combine multiple possible additional features, such that the number of images starts scaling in a polynomic manner and maintenance of the number of images becomes unmanagable quickly.
20 | Whereas with `rocker` you can invoke your specific plugins and it will use multi-stage builds of docker images to customize the container for your specific use case, which lets you use official upstream docker images without requiring you to maintain a plethora of parallel built images.
21 |
22 |
23 |
24 | ## Known extensions
25 |
26 | Rocker supports extensions via entry points there are some built in but you can add your own.
27 |
28 | ### Integrated Extensions
29 |
30 | There are a number of integrated extensions here's some of the highlights.
31 | You can get full details on the extensions from the main `rocker --help` command.
32 |
33 | - x11 -- Enable the use of X11 inside the container via the host X instance.
34 | - nvidia -- Enable NVIDIA graphics cards for rendering
35 | - cuda -- Enable NVIDIA CUDA in the container
36 | - user -- Create a user inside the container with the same settings as the host and run commands inside the container as that user.
37 | - home -- Mount the user's home directory into the container
38 | - pulse -- Mount pulse audio into the container
39 | - ssh -- Pass through ssh access to the container.
40 |
41 | As well as access to many of the docker arguments as well such as `device`, `env`, `volume`, `name`, `network`, `ipc`, and `privileged`.
42 |
43 | ### Externally maintained extensions
44 |
45 | Here's a list of public repositories with extensions.
46 |
47 | - Off-your-rocker: https://github.com/sloretz/off-your-rocker
48 | - mp_rocker: https://github.com/miguelprada/mp_rocker
49 | - ghrocker: https://github.com/tfoote/ghrocker
50 | - novnc_rocker: https://github.com/tfoote/novnc-rocker
51 | - template_rocker: https://github.com/blooop/template_rocker
52 | - deps_rocker: https://github.com/blooop/deps_rocker
53 | - pixi_rocker: https://github.com/blooop/pixi_rocker
54 | - conda_rocker: https://github.com/blooop/conda_rocker
55 | - palanteer_rocker: https://github.com/blooop/palanteer_rocker
56 | - lazygit_rocker: https://github.com/blooop/lazygit_rocker
57 |
58 |
59 | ### Externally maintained rocker wrappers
60 |
61 | 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.
62 |
63 | - rockerc: https://github.com/blooop/rockerc wraps rocker to enable putting rocker commands into a yaml config file.
64 | - rockervsc: https://github.com/blooop/rockervsc wraps rocker so that a vscode instance attaches to the launched container.
65 |
66 | # Prerequisites
67 |
68 | This should work on most systems using with a recent docker version available.
69 |
70 | Docker installation instructions: https://docs.docker.com/install/
71 |
72 | ## NVIDIA settings
73 |
74 | For the NVIDIA option this has been tested on the following systems using nvidia docker2:
75 |
76 | | Ubuntu distribution | Linux Kernel | Nvidia drivers |
77 | | ------------------- | ------------ | ------------------------------------------------- |
78 | | 16.04 | 4.15 | nvidia-384 (works)
nvidia-340 (doesn't work) |
79 | | 18.04 | | nvidia-390 (works) |
80 | | 20.04 | 5.4.0 | nvidia-driver-460 (works) |
81 | | 22.04 | 5.13.0 | nvidia-driver-470 (works) |
82 |
83 | Install nvidia-docker 2: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker
84 |
85 | ### Additional Configuration for rootless mode
86 | For executing Docker as a non-root user, separate installation instructions are provided here: https://docs.docker.com/engine/security/rootless/
87 |
88 | After installing Rootless Docker, the nvidia-docker2 package can be installed as usual from the website above.
89 | Currently, [cgroups are not supported in rootless mode](https://github.com/moby/moby/issues/38729) so we need to change `no-cgroups` in */etc/nvidia-container-runtime/config.toml*
90 |
91 | ```shell
92 | [nvidia-container-cli]
93 | no-cgroups = true
94 | ```
95 |
96 | Note, that changing this setting will lead to a `Failed to initialize NVML: Unknown Error` if Docker is executed as root (noted [here](https://github.com/NVIDIA/nvidia-container-runtime/issues/85)).
97 |
98 | ## Intel integrated graphics support
99 |
100 | For [Intel integrated graphics support](https://www.intel.com/content/www/us/en/develop/documentation/get-started-with-ai-linux/top/using-containers/using-containers-with-the-command-line.html) you will need to mount the `/dev/dri` directory as follows:
101 |
102 | ```
103 | --devices /dev/dri
104 | ```
105 |
106 | # Installation
107 |
108 | ## Debians (Recommended)
109 | Debian packages are available from the ROS repositories. You can set them up in step one [here](http://wiki.ros.org/kinetic/Installation/Ubuntu) then come back.
110 |
111 | Then you can `sudo apt-get install python3-rocker`
112 |
113 | ## PIP
114 |
115 | If you're not on a Ubuntu or Debian platform.
116 |
117 | Rocker is available via pip you can install it via pip using
118 |
119 | `pip install rocker`
120 |
121 | ## Archlinux ([AUR](https://aur.archlinux.org/))
122 |
123 | Using any AUR helper, for example, with `paru`
124 |
125 | ```bash
126 | paru -S python-rocker
127 | ```
128 |
129 | or
130 |
131 | ```bash
132 | paru -S python-rocker-git
133 | ```
134 |
135 | ## Development
136 | To set things up in a virtual environment for isolation is a good way. If you don't already have it install python3's venv module.
137 |
138 | sudo apt-get install python3-venv
139 |
140 | Create a venv
141 |
142 | mkdir -p ~/rocker_venv
143 | python3 -m venv ~/rocker_venv
144 |
145 | Install rocker
146 |
147 | cd ~/rocker_venv
148 | . ~/rocker_venv/bin/activate
149 | pip install git+https://github.com/osrf/rocker.git
150 |
151 | For any new terminal re activate the venv before trying to use it.
152 |
153 | . ~/rocker_venv/bin/activate
154 |
155 | ### Testing
156 |
157 | To run tests install the 'test' extra and pytest-cov in the venv
158 |
159 | . ~/rocker_venv/bin/activate
160 | pip install -e .[test] pytest-cov
161 |
162 | Then you can run pytest.
163 |
164 | python3 -m pytest --cov=rocker
165 |
166 | Notes:
167 |
168 | - Make sure to use the python3 instance of pytest from inside the environment.
169 | - The tests include an nvidia test which assumes you're using a machine with an nvidia gpu. To skip them use `-m "not nvidia"`
170 |
171 |
172 | # Example usage
173 |
174 |
175 | ## Fly a drone
176 |
177 | Example usage with an iris
178 |
179 | rocker --nvidia --x11 --user --home --pull --pulse tfoote/drone_demo
180 |
181 | After the ekf converges,
182 |
183 | You can send a takeoff command and then click to command the vehicle to fly to a point on the map.
184 |
185 | ## ROS 2 rviz
186 |
187 | rocker --nvidia --x11 osrf/ros:crystal-desktop rviz2
188 |
189 |
190 | ## Generic gazebo
191 |
192 | On Xenial
193 |
194 | rocker --nvidia --x11 osrf/ros:kinetic-desktop-full gazebo
195 |
196 | On Bionic
197 |
198 | rocker --nvidia --x11 osrf/ros:melodic-desktop-full gazebo
199 |
200 | ## Volume mount
201 |
202 | ### For arguments with one element not colon separated
203 |
204 | `--volume` adds paths as docker volumes.
205 |
206 | **The last path must be terminated with two dashes `--`**.
207 |
208 | rocker --volume ~/.vimrc ~/.bashrc -- ubuntu:18.04
209 |
210 | The above example of the volume option will be expanded via absolute paths for `docker run` as follows:
211 |
212 | --volume /home//.vimrc:/home//.vimrc --volume /home//.bashrc:/home//.bashrc
213 |
214 | ### For arguments with colon separation
215 |
216 | It will process the same as `docker`'s `--volume` option, `rocker --volume` takes 3 fields.
217 | - 1st field: the path to the file or directory on the host machine.
218 | - 2nd field: (optional) the path where the file or directory is mounted in the container.
219 | - If only the 1st field is supplied, same value as the 1st field will be populated as the 2nd field.
220 | - 3rd field: (optional) bind propagation as `ro`, `z`, and `Z`. See [docs.docker.com](https://docs.docker.com/storage/bind-mounts/) for further detail.
221 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment:
2 | layout: "reach, diff, flags, files"
3 | behavior: default
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [tool:pytest]
2 | markers =
3 | # Tests which require a docker engine
4 | docker
5 | # Tests which require an NVIDIA GPU and drivers
6 | nvidia
7 | # Tests which require an X11 display of some kind
8 | x11
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | from setuptools import setup
5 |
6 | # importlib-metadata dependency can be removed when RHEL8 and other 3.6 based systems are not in support cycles
7 |
8 | install_requires = [
9 | 'empy',
10 | 'importlib-metadata; python_version < "3.8"',
11 | 'pexpect',
12 | 'packaging',
13 | 'urllib3',
14 | ]
15 |
16 | # docker API used to be in a package called `docker-py` before the 2.0 release
17 | docker_package = 'docker'
18 | try:
19 | import docker
20 | except ImportError:
21 | # Docker is not yet installed, pick library based on platform
22 | # Use old name if platform has pre-2.0 version
23 | if os.path.isfile('/etc/os-release'):
24 | with open('/etc/os-release') as fin:
25 | content = fin.read()
26 | if 'xenial' in content:
27 | docker_package = 'docker-py'
28 | else:
29 | # Docker is installed, pick library based on what we found
30 | ver = docker.__version__.split('.')
31 | if int(ver[0]) < 2:
32 | docker_package = 'docker-py'
33 |
34 | install_requires.append(docker_package)
35 |
36 | kwargs = {
37 | 'name': 'rocker',
38 | 'version': '0.2.19',
39 | 'packages': ['rocker'],
40 | 'package_dir': {'': 'src'},
41 | 'package_data': {'rocker': ['templates/*.em']},
42 | 'entry_points': {
43 | 'console_scripts': [
44 | 'rocker = rocker.cli:main',
45 | 'detect_docker_image_os = rocker.cli:detect_image_os',
46 | ],
47 | 'rocker.extensions': [
48 | 'cuda = rocker.nvidia_extension:Cuda',
49 | 'detach = rocker.extensions:Detach',
50 | 'devices = rocker.extensions:Devices',
51 | 'dev_helpers = rocker.extensions:DevHelpers',
52 | 'env = rocker.extensions:Environment',
53 | 'expose = rocker.extensions:Expose',
54 | 'git = rocker.git_extension:Git',
55 | 'group_add = rocker.extensions:GroupAdd',
56 | 'home = rocker.extensions:HomeDir',
57 | 'hostname = rocker.extensions:Hostname',
58 | 'ipc = rocker.extensions:Ipc',
59 | 'name = rocker.extensions:Name',
60 | 'network = rocker.extensions:Network',
61 | 'nvidia = rocker.nvidia_extension:Nvidia',
62 | 'port = rocker.extensions:Port',
63 | 'privileged = rocker.extensions:Privileged',
64 | 'pulse = rocker.extensions:PulseAudio',
65 | 'rmw = rocker.rmw_extension:RMW',
66 | 'shm_size = rocker.extensions:ShmSize',
67 | 'ssh = rocker.ssh_extension:Ssh',
68 | 'ulimit = rocker.ulimit_extension:Ulimit',
69 | 'user = rocker.extensions:User',
70 | 'volume = rocker.volume_extension:Volume',
71 | 'x11 = rocker.nvidia_extension:X11',
72 | ]
73 | },
74 | 'author': 'Tully Foote',
75 | 'author_email': 'tfoote@osrfoundation.org',
76 | 'keywords': ['Docker'],
77 | 'classifiers': [
78 | 'Programming Language :: Python',
79 | 'Programming Language :: Python :: 3',
80 | 'License :: OSI Approved :: Apache Software License'
81 | ],
82 | 'description': 'A tool to run docker containers with customized extras',
83 | 'long_description': 'A tool to run docker containers with customized extra added like nvidia gui support overlayed.',
84 | 'license': 'Apache License 2.0',
85 | 'python_requires': '>=3.0',
86 |
87 | 'install_requires': install_requires,
88 | 'extras_require': {
89 | 'test': [
90 | 'pytest'
91 | ]
92 | },
93 | 'url': 'https://github.com/osrf/rocker'
94 | }
95 |
96 | setup(**kwargs)
97 |
--------------------------------------------------------------------------------
/src/rocker/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osrf/rocker/ade980316020c2e3e82412081bb656c73bd85556/src/rocker/__init__.py
--------------------------------------------------------------------------------
/src/rocker/cli.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import argparse
16 | import os
17 | import sys
18 |
19 | from .core import DockerImageGenerator
20 | from .core import get_rocker_version
21 | from .core import RockerExtensionManager
22 | from .core import DependencyMissing
23 | from .core import ExtensionError
24 | from .core import OPERATIONS_DRY_RUN
25 | from .core import OPERATIONS_INTERACTIVE
26 | from .core import OPERATIONS_NON_INTERACTIVE
27 | from .core import OPERATION_MODES
28 |
29 | from .os_detector import detect_os
30 |
31 |
32 | def main():
33 |
34 | parser = argparse.ArgumentParser(
35 | description='A tool for running docker with extra options',
36 | formatter_class=argparse.ArgumentDefaultsHelpFormatter)
37 | parser.add_argument('image')
38 | parser.add_argument('command', nargs='*', default='')
39 | parser.add_argument('--noexecute', action='store_true', help='Deprecated') # TODO(tfoote) add when 3.13 is minimum supported, deprecated=True
40 | parser.add_argument('--nocache', action='store_true')
41 | parser.add_argument('--nocleanup', action='store_true', help='do not remove the docker container when stopped')
42 | 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
43 | parser.add_argument('--pull', action='store_true')
44 | parser.add_argument('--version', action='version',
45 | version='%(prog)s ' + get_rocker_version())
46 |
47 | try:
48 | extension_manager = RockerExtensionManager()
49 | default_args = {}
50 | extension_manager.extend_cli_parser(parser, default_args)
51 | except DependencyMissing as ex:
52 | # Catch errors if docker is missing or inaccessible.
53 | parser.error("DependencyMissing encountered: %s" % ex)
54 |
55 | args = parser.parse_args()
56 | args_dict = vars(args)
57 |
58 | if args.noexecute:
59 | from .core import OPERATIONS_DRY_RUN
60 | args_dict['mode'] = OPERATIONS_DRY_RUN
61 | print('DEPRECATION Warning: --noexecute is deprecated for --mode dry-run please switch your usage by December 2020')
62 |
63 | # validate_operating_mode
64 | operating_mode = args_dict.get('mode')
65 | # Don't try to be interactive if there's no tty
66 | if not os.isatty(sys.__stdin__.fileno()):
67 | if operating_mode == OPERATIONS_INTERACTIVE:
68 | parser.error("No tty detected cannot operate in interactive mode")
69 | elif not operating_mode:
70 | print("No tty detected for stdin defaulting mode to non-interactive")
71 | args_dict['mode'] = OPERATIONS_NON_INTERACTIVE
72 |
73 | # Check if detach extension is active and deconflict with interactive
74 | detach_active = args_dict.get('detach')
75 | operating_mode = args_dict.get('mode')
76 | if detach_active:
77 | if operating_mode == OPERATIONS_INTERACTIVE:
78 | parser.error("Command line option --mode=interactive and --detach are mutually exclusive")
79 | elif not operating_mode:
80 | print(f"Detach extension active, defaulting mode to {OPERATIONS_NON_INTERACTIVE}")
81 | args_dict['mode'] = OPERATIONS_NON_INTERACTIVE
82 | # TODO(tfoote) Deal with the case of dry-run + detach
83 | # Right now the printed results will include '-it'
84 | # But based on testing the --detach overrides -it in docker so it's ok.
85 |
86 | # Default to non-interactive if unset
87 | if args_dict.get('mode') not in OPERATION_MODES:
88 | print("Mode unset, defaulting to interactive")
89 | args_dict['mode'] = OPERATIONS_NON_INTERACTIVE
90 |
91 | try:
92 | active_extensions = extension_manager.get_active_extensions(args_dict)
93 | except ExtensionError as e:
94 | print(f"ERROR! {str(e)}")
95 | return 1
96 | print("Active extensions %s" % [e.get_name() for e in active_extensions])
97 |
98 | base_image = args.image
99 |
100 | dig = DockerImageGenerator(active_extensions, args_dict, base_image)
101 | exit_code = dig.build(**vars(args))
102 | if exit_code != 0:
103 | print("Build failed exiting")
104 | if not (args_dict['persist_image'] or args_dict.get('image_name')):
105 | dig.clear_image()
106 | return exit_code
107 | # Convert command into string
108 | args.command = ' '.join(args.command)
109 | result = dig.run(**args_dict)
110 | if not (args_dict['persist_image'] or args_dict.get('image_name')):
111 | print(f'Clearing Image: {dig.image_id}s\nTo not clean up use --persist-images')
112 | dig.clear_image()
113 | return result
114 |
115 |
116 | def detect_image_os():
117 | parser = argparse.ArgumentParser(description='Detect the os in an image')
118 | parser.add_argument('image')
119 | parser.add_argument('--verbose', action='store_true',
120 | help='Display verbose output of the process')
121 |
122 | args = parser.parse_args()
123 |
124 | results = detect_os(args.image, print if args.verbose else None)
125 | print(results)
126 | if results:
127 | return 0
128 | else:
129 | return 1
130 |
--------------------------------------------------------------------------------
/src/rocker/core.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from collections import OrderedDict
16 | from contextlib import nullcontext
17 | import io
18 | import os
19 | import pwd
20 | import re
21 | import sys
22 |
23 | # importlib-metadata dependency can be removed when RHEL8 and other 3.6 based systems are not in support cycles
24 | if sys.version_info >= (3, 8):
25 | import importlib.metadata as importlib_metadata
26 | else:
27 | import importlib_metadata
28 |
29 |
30 | import pkgutil
31 | from requests.exceptions import ConnectionError
32 | import shlex
33 | import subprocess
34 | import tempfile
35 |
36 | import docker
37 | import pexpect
38 |
39 | import fcntl
40 | from pathlib import Path
41 | import signal
42 | import struct
43 | import termios
44 | import typing
45 |
46 | SYS_STDOUT = sys.stdout
47 |
48 | OPERATIONS_DRY_RUN = 'dry-run'
49 | OPERATIONS_NON_INTERACTIVE = 'non-interactive'
50 | OPERATIONS_INTERACTIVE = 'interactive'
51 | OPERATION_MODES = [OPERATIONS_INTERACTIVE, OPERATIONS_NON_INTERACTIVE , OPERATIONS_DRY_RUN]
52 |
53 |
54 | class DependencyMissing(RuntimeError):
55 | pass
56 |
57 |
58 | class ExtensionError(RuntimeError):
59 | pass
60 |
61 |
62 | class RockerExtension(object):
63 | """The base class for Rocker extension points"""
64 |
65 | def precondition_environment(self, cliargs):
66 | """Modify the local environment such as setup tempfiles"""
67 | pass
68 |
69 | def validate_environment(self, cliargs):
70 | """ Check that the environment is something that can be used.
71 | This will check that we're on the right base OS and that the
72 | necessary resources are available, like hardware."""
73 | pass
74 |
75 | def invoke_after(self, cliargs) -> typing.Set[str]:
76 | """
77 | This extension should be loaded after the extensions in the returned
78 | set. These extensions are not required to be present, but if they are,
79 | they will be loaded before this extension.
80 | """
81 | return set()
82 |
83 | def required(self, cliargs) -> typing.Set[str]:
84 | """
85 | Ensures the specified extensions are present and combined with
86 | this extension. If the required extension should be loaded before
87 | this extension, it should also be added to the `invoke_after` set.
88 | """
89 | return set()
90 |
91 | def get_preamble(self, cliargs):
92 | return ''
93 |
94 | def get_snippet(self, cliargs):
95 | """ Get a dockerfile snippet to be executed as ROOT."""
96 | return ''
97 |
98 | def get_user_snippet(self, cliargs):
99 | """ Get a dockerfile snippet to be executed after switchingto the expected USER."""
100 | return ''
101 |
102 | def get_files(self, cliargs):
103 | """Get a dict of local filenames and content to write into them"""
104 | return {}
105 |
106 | @staticmethod
107 | def get_name():
108 | raise NotImplementedError
109 |
110 | def get_docker_args(self, cliargs):
111 | return ''
112 |
113 | @classmethod
114 | def check_args_for_activation(cls, cli_args):
115 | """ Returns true if the arguments indicate that this extension should be activated otherwise false.
116 | The default implementation looks for the extension name has any value.
117 | It is recommended to override this unless it's just a flag to enable the plugin."""
118 | return True if cli_args.get(cls.get_name()) else False
119 |
120 | @staticmethod
121 | def register_arguments(parser, defaults):
122 | raise NotImplementedError
123 |
124 |
125 | class RockerExtensionManager:
126 | def __init__(self):
127 | self.available_plugins = list_plugins()
128 |
129 | def extend_cli_parser(self, parser, default_args={}):
130 | for p in self.available_plugins.values():
131 | try:
132 | p.register_arguments(parser, default_args)
133 | except TypeError as ex:
134 | print("Extension %s doesn't support default arguments. Please extend it." % p.get_name())
135 | p.register_arguments(parser)
136 | parser.add_argument('--mode', choices=OPERATION_MODES,
137 | help="Choose mode of operation for rocker, default interactive unless detached.")
138 | parser.add_argument('--image-name', default=None,
139 | help='Tag the final image, useful with dry-run')
140 | parser.add_argument('--extension-blacklist', nargs='*',
141 | default=[],
142 | help='Prevent any of these extensions from being loaded.')
143 | parser.add_argument('--strict-extension-selection', action='store_true',
144 | help='When enabled, causes an error if required extensions are not explicitly '
145 | 'called out on the command line. Otherwise, the required extensions will '
146 | 'automatically be loaded if available.')
147 |
148 |
149 | def get_active_extensions(self, cli_args):
150 | """
151 | Checks for missing dependencies (specified by each extension's
152 | required() method) and additionally sorts them.
153 | """
154 | def sort_extensions(extensions, cli_args):
155 |
156 | def topological_sort(source: typing.Dict[str, typing.Set[str]]) -> typing.List[str]:
157 | """Perform a topological sort on names and dependencies and returns the sorted list of names."""
158 | names = set(source.keys())
159 | # prune optional dependencies if they are not present (at this point the required check has already occurred)
160 | pending = [(name, dependencies.intersection(names)) for name, dependencies in source.items()]
161 | emitted = []
162 | while pending:
163 | next_pending = []
164 | next_emitted = []
165 | for entry in pending:
166 | name, deps = entry
167 | deps.difference_update(emitted) # remove dependencies already emitted
168 | if deps: # still has dependencies? recheck during next pass
169 | next_pending.append(entry)
170 | else: # no more dependencies? time to emit
171 | yield name
172 | next_emitted.append(name) # remember what was emitted for difference_update()
173 | if not next_emitted:
174 | raise ExtensionError("Cyclic dependancy detected: %r" % (next_pending,))
175 | pending = next_pending
176 | emitted = next_emitted
177 |
178 | extension_graph = {name: cls.invoke_after(cli_args) for name, cls in sorted(extensions.items())}
179 | active_extension_list = [extensions[name] for name in topological_sort(extension_graph)]
180 | return active_extension_list
181 |
182 | active_extensions = {}
183 | find_reqs = set([name for name, cls in self.available_plugins.items() if cls.check_args_for_activation(cli_args)])
184 | while find_reqs:
185 | name = find_reqs.pop()
186 |
187 | if name in self.available_plugins.keys():
188 | if name not in cli_args['extension_blacklist']:
189 | ext = self.available_plugins[name]()
190 | active_extensions[name] = ext
191 | else:
192 | raise ExtensionError(f"Extension '{name}' is blacklisted.")
193 | else:
194 | raise ExtensionError(f"Extension '{name}' not found. Is it installed?")
195 |
196 | # add additional reqs for processing not already known about
197 | known_reqs = set(active_extensions.keys()).union(find_reqs)
198 | missing_reqs = ext.required(cli_args).difference(known_reqs)
199 | if missing_reqs:
200 | if cli_args['strict_extension_selection']:
201 | raise ExtensionError(f"Extension '{name}' is missing required extension(s) {list(missing_reqs)}")
202 | else:
203 | print(f"Adding implicilty required extension(s) {list(missing_reqs)} required by extension '{name}'")
204 | find_reqs = find_reqs.union(missing_reqs)
205 |
206 | return sort_extensions(active_extensions, cli_args)
207 |
208 | def get_docker_client():
209 | """Simple helper function for pre 2.0 imports"""
210 | try:
211 | try:
212 | docker_client = docker.from_env().api
213 | except AttributeError:
214 | # docker-py pre 2.0
215 | docker_client = docker.Client()
216 | # Validate that the server is available
217 | docker_client.ping()
218 | return docker_client
219 | except (docker.errors.DockerException, docker.errors.APIError, ConnectionError) as ex:
220 | raise DependencyMissing('Docker Client failed to connect to docker daemon.'
221 | ' Please verify that docker is installed and running.'
222 | ' As well as that you have permission to access the docker daemon.'
223 | ' This is usually by being a member of the docker group.'
224 | ' The underlying error was:\n"""\n%s\n"""\n' % ex)
225 |
226 | def get_user_name():
227 | userinfo = pwd.getpwuid(os.getuid())
228 | return getattr(userinfo, 'pw_' + 'name')
229 |
230 | def docker_build(docker_client = None, output_callback = None, **kwargs):
231 | image_id = None
232 |
233 | if not docker_client:
234 | docker_client = get_docker_client()
235 | kwargs['decode'] = True
236 | for line in docker_client.build(**kwargs):
237 | output = line.get('stream', '').rstrip()
238 | if not output:
239 | # print("non stream data", line)
240 | continue
241 | if output_callback is not None:
242 | output_callback(output)
243 |
244 | match = re.match(r'Successfully built ([a-z0-9]{12})', output)
245 | if match:
246 | image_id = match.group(1)
247 |
248 | if image_id:
249 | return image_id
250 | else:
251 | print("no more output and success not detected")
252 | return None
253 |
254 | def docker_remove_image(
255 | image_id,
256 | docker_client = None,
257 | fail_on_error = False,
258 | force = False,
259 | **kwargs):
260 |
261 | if not docker_client:
262 | docker_client = get_docker_client()
263 |
264 | try:
265 | docker_client.remove_image(image_id, force=force)
266 | except docker.errors.APIError as ex:
267 | ## removing the image can fail if there's child images
268 | if fail_on_error:
269 | return False
270 | return True
271 |
272 | class SIGWINCHPassthrough(object):
273 | def __init__ (self, process):
274 | self.process = process
275 | self.active = os.isatty(sys.__stdout__.fileno())
276 |
277 | def set_window_size(self):
278 | s = struct.pack("HHHH", 0, 0, 0, 0)
279 | try:
280 | a = struct.unpack('hhhh', fcntl.ioctl(SYS_STDOUT.fileno(),
281 | termios.TIOCGWINSZ , s))
282 | except (io.UnsupportedOperation, AttributeError) as ex:
283 | # We're not interacting with a real stdout, don't do the resize
284 | # This happens when we're in something like unit tests.
285 | return
286 | if not self.process.closed:
287 | self.process.setwinsize(a[0],a[1])
288 |
289 |
290 | def __enter__(self):
291 | # Short circuit if not a tty
292 | if not self.active:
293 | return self
294 | # Expected function prototype for signal handling
295 | # ignoring unused arguments
296 | def sigwinch_passthrough (sig, data):
297 | self.set_window_size()
298 |
299 | signal.signal(signal.SIGWINCH, sigwinch_passthrough)
300 |
301 | # Initially set the window size since it may not be default sized
302 | self.set_window_size()
303 | return self
304 |
305 | # Clean up signal handler before returning.
306 | def __exit__(self, exc_type, exc_value, traceback):
307 | if not self.active:
308 | return
309 | # This was causing hangs and resolved as referenced
310 | # here: https://github.com/pexpect/pexpect/issues/465
311 | signal.signal(signal.SIGWINCH, signal.SIG_DFL)
312 |
313 | class DockerImageGenerator(object):
314 | def __init__(self, active_extensions, cliargs, base_image):
315 | self.built = False
316 | self.cliargs = cliargs
317 | self.cliargs['base_image'] = base_image # inject base image into arguments for use
318 | self.active_extensions = active_extensions
319 |
320 | self.dockerfile = generate_dockerfile(active_extensions, self.cliargs, base_image)
321 | self.image_id = None
322 |
323 | def build(self, **kwargs):
324 | with tempfile.TemporaryDirectory() as td:
325 | df = os.path.join(td, 'Dockerfile')
326 | print("Writing dockerfile to %s" % df)
327 | with open(df, 'w') as fh:
328 | fh.write(self.dockerfile)
329 | print('vvvvvv')
330 | print(self.dockerfile)
331 | print('^^^^^^')
332 | write_files(self.active_extensions, self.cliargs, td)
333 | arguments = {}
334 | arguments['path'] = td
335 | arguments['rm'] = True
336 | arguments['nocache'] = kwargs.get('nocache', False)
337 | arguments['pull'] = kwargs.get('pull', False)
338 | image_name = kwargs.get('image_name', None)
339 | if image_name:
340 | print(f"Running docker tag {self.image_id} {image_name}")
341 | arguments['tag'] = image_name
342 | print("Building docker file with arguments: ", arguments)
343 | try:
344 | self.image_id = docker_build(
345 | **arguments,
346 | output_callback=lambda output: print("building > %s" % output)
347 | )
348 | if self.image_id:
349 | self.built = True
350 | return 0
351 | else:
352 | return 2
353 |
354 | except docker.errors.APIError as ex:
355 | print("Docker build failed\n", ex)
356 | return 1
357 |
358 | def get_operating_mode(self, args):
359 | operating_mode = args.get('mode')
360 | # Default to non-interactive if unset
361 | if operating_mode not in OPERATION_MODES:
362 | operating_mode = OPERATIONS_NON_INTERACTIVE
363 | return operating_mode
364 |
365 | def generate_docker_cmd(self, command='', **kwargs):
366 | docker_args = ''
367 |
368 | for e in self.active_extensions:
369 | docker_args += e.get_docker_args(self.cliargs)
370 |
371 | image_name = kwargs.get('image_name', None)
372 | if image_name:
373 | image = image_name
374 | else:
375 | image = self.image_id
376 | cmd = "docker run"
377 | if(not kwargs.get('nocleanup')):
378 | # remove container only if --nocleanup is not present
379 | cmd += " --rm"
380 |
381 | operating_mode = self.get_operating_mode(kwargs)
382 | if operating_mode != OPERATIONS_NON_INTERACTIVE:
383 | # only disable for OPERATIONS_NON_INTERACTIVE
384 | cmd += " -it"
385 | cmd += "%(docker_args)s %(image)s %(command)s" % locals()
386 | return cmd
387 |
388 | def run(self, command='', **kwargs):
389 | if not self.built:
390 | print("Cannot run if build has not passed.")
391 | return 1
392 |
393 | for e in self.active_extensions:
394 | try:
395 | e.precondition_environment(self.cliargs)
396 | except subprocess.CalledProcessError as ex:
397 | print("ERROR! Failed to precondition for extension [%s] with error: %s\ndeactivating" % (e.get_name(), ex))
398 | return 1
399 |
400 | cmd = self.generate_docker_cmd(command, **kwargs)
401 | operating_mode = self.get_operating_mode(kwargs)
402 | console_output_file = kwargs.get('console_output_file')
403 |
404 | # $DOCKER_OPTS \
405 | if operating_mode == OPERATIONS_DRY_RUN:
406 | print("Run this command: \n\n\n")
407 | print(cmd)
408 | return 0
409 | elif operating_mode == OPERATIONS_NON_INTERACTIVE:
410 | try:
411 | with open(console_output_file, 'a') if console_output_file else nullcontext() as consoleout_fh:
412 | if console_output_file:
413 | print(f"Logging output to file {console_output_file}")
414 | print("Executing command: ")
415 | print(cmd)
416 | p = subprocess.run(shlex.split(cmd), check=True, stdout=consoleout_fh if console_output_file else None, stderr=subprocess.STDOUT)
417 | return p.returncode
418 | except subprocess.CalledProcessError as ex:
419 | print("Non-interactive Docker run failed\n", ex)
420 | return ex.returncode
421 | else:
422 | try:
423 | print("Executing command: ")
424 | print(cmd)
425 | p = pexpect.spawn(cmd)
426 | with SIGWINCHPassthrough(p):
427 | p.interact()
428 | p.close(force=True)
429 | return p.exitstatus
430 | except pexpect.ExceptionPexpect as ex:
431 | print("Docker run failed\n", ex)
432 | return ex.returncode
433 |
434 | def clear_image(self):
435 | if self.image_id:
436 | if not docker_remove_image(self.image_id):
437 | print(f'Failed to clear image {self.image_id} it likely has child images.')
438 | self.image_id = None
439 | self.built = False
440 |
441 | def write_files(extensions, args_dict, target_directory):
442 | all_files = {}
443 | for active_extension in extensions:
444 | for file_path, contents in active_extension.get_files(args_dict).items():
445 | if os.path.isabs(file_path):
446 | print('WARNING!! Path %s from extension %s is absolute'
447 | 'and cannot be written out, skipping' % (file_path, active_extension.get_name()))
448 | continue
449 | full_path = os.path.join(target_directory, file_path)
450 | if Path(target_directory).resolve() not in Path(full_path).resolve().parents:
451 | print('WARNING!! Path %s from extension %s is outside target directory'
452 | 'and cannot be written out, skipping' % (file_path, active_extension.get_name()))
453 | continue
454 | Path(os.path.dirname(full_path)).mkdir(exist_ok=True, parents=True)
455 | mode = 'wb' if isinstance(contents, bytes) else 'w' # check to see if contents should be written as binary
456 | with open(full_path, mode) as fh:
457 | print('Writing to file %s' % full_path)
458 | fh.write(contents)
459 | return all_files
460 |
461 |
462 | def generate_dockerfile(extensions, args_dict, base_image):
463 | dockerfile_str = ''
464 | # Preamble snippets
465 | for el in extensions:
466 | dockerfile_str += '# Preamble from extension [%s]\n' % el.get_name()
467 | dockerfile_str += el.get_preamble(args_dict) + '\n'
468 | dockerfile_str += '\nFROM %s\n' % base_image
469 | # ROOT snippets
470 | dockerfile_str += 'USER root\n'
471 | for el in extensions:
472 | dockerfile_str += '# Snippet from extension [%s]\n' % el.get_name()
473 | dockerfile_str += el.get_snippet(args_dict) + '\n'
474 | # Set USER if user extension activated
475 | if 'user' in args_dict and args_dict['user']:
476 | if 'user_override_name' in args_dict and args_dict['user_override_name']:
477 | username = args_dict['user_override_name']
478 | else:
479 | username = get_user_name()
480 | dockerfile_str += f'USER {username}\n'
481 | # USER snippets
482 | for el in extensions:
483 | dockerfile_str += '# User Snippet from extension [%s]\n' % el.get_name()
484 | dockerfile_str += el.get_user_snippet(args_dict) + '\n'
485 | return dockerfile_str
486 |
487 |
488 | def list_entry_points():
489 | entry_points = importlib_metadata.entry_points()
490 | if hasattr(entry_points, 'select'):
491 | styles_groups = entry_points.select(group='flake8_import_order.styles')
492 | else:
493 | styles_groups = entry_points.get('flake8_import_order.styles', [])
494 |
495 | return styles_groups
496 |
497 | def list_plugins(extension_point='rocker.extensions'):
498 |
499 | all_entry_points = importlib_metadata.entry_points()
500 | if hasattr(all_entry_points, 'select'):
501 | rocker_extensions = all_entry_points.select(group=extension_point)
502 | else:
503 | rocker_extensions = all_entry_points.get(extension_point, [])
504 |
505 | unordered_plugins = {
506 | entry_point.name: entry_point.load()
507 | for entry_point in rocker_extensions
508 | }
509 | # Order plugins by extension point name for consistent ordering below
510 | plugin_names = list(unordered_plugins.keys())
511 | plugin_names.sort()
512 | return OrderedDict([(k, unordered_plugins[k]) for k in plugin_names])
513 |
514 |
515 | def get_rocker_version():
516 | return importlib_metadata.version('rocker')
517 |
--------------------------------------------------------------------------------
/src/rocker/em.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | import em
17 |
18 | def empy_expand(template, substitution_variables):
19 | """Indirection for empy version compatibility."""
20 | if em.__version__.startswith('3'):
21 | return em.expand(template, substitution_variables)
22 | else:
23 | return em.expand(template, globals=substitution_variables)
--------------------------------------------------------------------------------
/src/rocker/extensions.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import grp
16 | import os
17 | import docker
18 | import getpass
19 | import pwd
20 | import pkgutil
21 | from pathlib import Path
22 | from shlex import quote
23 | import subprocess
24 | import sys
25 |
26 | from .core import get_docker_client
27 | from .em import empy_expand
28 |
29 |
30 | def name_to_argument(name):
31 | return '--%s' % name.replace('_', '-')
32 |
33 | from .core import RockerExtension
34 |
35 | class Detach(RockerExtension):
36 | @staticmethod
37 | def get_name():
38 | return 'detach'
39 |
40 | def __init__(self):
41 | self.name = Detach.get_name()
42 |
43 | def get_docker_args(self, cliargs):
44 | args = ''
45 | detach = cliargs.get('detach', False)
46 | if detach:
47 | args += ' --detach'
48 | return args
49 |
50 | @staticmethod
51 | def register_arguments(parser, defaults):
52 | parser.add_argument(
53 | '-d',
54 | '--detach',
55 | action='store_true',
56 | default=defaults.get('detach', False),
57 | help='Run the container in the background.'
58 | )
59 |
60 |
61 | class Devices(RockerExtension):
62 | @staticmethod
63 | def get_name():
64 | return 'devices'
65 |
66 | def __init__(self):
67 | self.name = Devices.get_name()
68 |
69 | def get_preamble(self, cliargs):
70 | return ''
71 |
72 | def get_docker_args(self, cliargs):
73 | args = ''
74 | devices = cliargs.get('devices', None)
75 | for device in devices:
76 | if not os.path.exists(device):
77 | print("ERROR device %s doesn't exist. Skipping" % device)
78 | continue
79 | args += ' --device %s ' % device
80 | return args
81 |
82 | @staticmethod
83 | def register_arguments(parser, defaults):
84 | parser.add_argument('--devices',
85 | default=defaults.get('devices', None),
86 | nargs='*',
87 | help="Mount devices into the container.")
88 |
89 |
90 | class DevHelpers(RockerExtension):
91 | @staticmethod
92 | def get_name():
93 | return 'dev_helpers'
94 |
95 | def __init__(self):
96 | self._env_subs = None
97 | self.name = DevHelpers.get_name()
98 |
99 |
100 | def get_environment_subs(self):
101 | if not self._env_subs:
102 | self._env_subs = {}
103 | return self._env_subs
104 |
105 | def get_preamble(self, cliargs):
106 | return ''
107 |
108 | def get_snippet(self, cliargs):
109 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
110 | return empy_expand(snippet, self.get_environment_subs())
111 |
112 | @staticmethod
113 | def register_arguments(parser, defaults):
114 | parser.add_argument(name_to_argument(DevHelpers.get_name()),
115 | action='store_true',
116 | default=defaults.get('dev_helpers', None),
117 | help="add development tools emacs and byobu to your environment")
118 |
119 |
120 | class Expose(RockerExtension):
121 | @staticmethod
122 | def get_name():
123 | return 'expose'
124 |
125 | def __init__(self):
126 | self.name = Expose.get_name()
127 |
128 | def get_preamble(self, cliargs):
129 | return ''
130 |
131 | def get_docker_args(self, cliargs):
132 | args = ['']
133 | ports = cliargs.get('expose', [])
134 | for port in ports:
135 | args.append(' --expose {0}'.format(port))
136 | return ' '.join(args)
137 |
138 | @staticmethod
139 | def register_arguments(parser, defaults={}):
140 | parser.add_argument('--expose',
141 | default=defaults.get('expose', None),
142 | action='append',
143 | help="Exposes a port from the container to host machine.")
144 |
145 |
146 | class Hostname(RockerExtension):
147 | @staticmethod
148 | def get_name():
149 | return 'hostname'
150 |
151 | def __init__(self):
152 | self.name = Hostname.get_name()
153 |
154 | def get_preamble(self, cliargs):
155 | return ''
156 |
157 | def get_docker_args(self, cliargs):
158 | args = ''
159 | hostname = cliargs.get('hostname', None)
160 | if hostname:
161 | args += ' --hostname %s ' % hostname
162 | return args
163 |
164 | @staticmethod
165 | def register_arguments(parser, defaults):
166 | parser.add_argument('--hostname', default=defaults.get('hostname', ''),
167 | help='Hostname of the container.')
168 |
169 |
170 | class Ipc(RockerExtension):
171 | @staticmethod
172 | def get_name():
173 | return 'ipc'
174 | def __init__(self):
175 | self.name = Ipc.get_name()
176 |
177 | def get_preamble(self, cliargs):
178 | return ''
179 |
180 | def get_docker_args(self, cliargs):
181 | args = ''
182 | ipc = cliargs.get('ipc', None)
183 | args += ' --ipc %s ' % ipc
184 | return args
185 |
186 | @staticmethod
187 | def register_arguments(parser, defaults={}):
188 | parser.add_argument('--ipc', default=defaults.get('ipc', None),
189 | 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')
190 |
191 |
192 | class Name(RockerExtension):
193 | @staticmethod
194 | def get_name():
195 | return 'name'
196 |
197 | def __init__(self):
198 | self.name = Name.get_name()
199 |
200 | def get_preamble(self, cliargs):
201 | return ''
202 |
203 | def get_docker_args(self, cliargs):
204 | args = ''
205 | name = cliargs.get('name', None)
206 | if name:
207 | args += ' --name %s ' % name
208 | return args
209 |
210 | @staticmethod
211 | def register_arguments(parser, defaults):
212 | parser.add_argument('--name', default=defaults.get('name', ''),
213 | help='Name of the container.')
214 |
215 |
216 | class Network(RockerExtension):
217 | @staticmethod
218 | def get_name():
219 | return 'network'
220 |
221 | def __init__(self):
222 | self.name = Network.get_name()
223 |
224 | def get_preamble(self, cliargs):
225 | return ''
226 |
227 | def get_docker_args(self, cliargs):
228 | args = ''
229 | network = cliargs.get('network', None)
230 | args += ' --network %s ' % network
231 | return args
232 |
233 | @staticmethod
234 | def register_arguments(parser, defaults):
235 | client = get_docker_client()
236 | parser.add_argument('--network', choices=[n['Name'] for n in client.networks()],
237 | default=defaults.get('network', None),
238 | help="What network configuration to use.")
239 |
240 |
241 | class Port(RockerExtension):
242 | @staticmethod
243 | def get_name():
244 | return 'port'
245 |
246 | def __init__(self):
247 | self.name = Port.get_name()
248 |
249 | def get_preamble(self, cliargs):
250 | return ''
251 |
252 | def get_docker_args(self, cliargs):
253 | args = ['']
254 | ports = cliargs.get('port', [])
255 | for port in ports:
256 | args.append(' -p {0}'.format(port))
257 | return ' '.join(args)
258 |
259 | @staticmethod
260 | def register_arguments(parser, defaults):
261 | parser.add_argument('--port',
262 | default=defaults.get('port', None),
263 | action='append',
264 | help="Binds port from the container to host machine.")
265 |
266 |
267 | class PulseAudio(RockerExtension):
268 | @staticmethod
269 | def get_name():
270 | return 'pulse'
271 |
272 | def __init__(self):
273 | self._env_subs = None
274 | self.name = PulseAudio.get_name()
275 |
276 |
277 | def get_environment_subs(self):
278 | if not self._env_subs:
279 | self._env_subs = {}
280 | self._env_subs['user_id'] = os.getuid()
281 | self._env_subs['XDG_RUNTIME_DIR'] = os.getenv('XDG_RUNTIME_DIR')
282 | self._env_subs['audio_group_id'] = grp.getgrnam('audio').gr_gid
283 | return self._env_subs
284 |
285 | def get_preamble(self, cliargs):
286 | return ''
287 |
288 | def get_snippet(self, cliargs):
289 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
290 | return empy_expand(snippet, self.get_environment_subs())
291 |
292 | def get_docker_args(self, cliargs):
293 | args = ' -v /run/user/%(user_id)s/pulse:/run/user/%(user_id)s/pulse --device /dev/snd '\
294 | ' -e PULSE_SERVER=unix:%(XDG_RUNTIME_DIR)s/pulse/native -v %(XDG_RUNTIME_DIR)s/pulse/native:%(XDG_RUNTIME_DIR)s/pulse/native --group-add %(audio_group_id)s '
295 | return args % self.get_environment_subs()
296 |
297 | @staticmethod
298 | def register_arguments(parser, defaults):
299 | parser.add_argument(name_to_argument(PulseAudio.get_name()),
300 | action='store_true',
301 | default=defaults.get(PulseAudio.get_name(), None),
302 | help="mount pulse audio devices")
303 |
304 |
305 | class HomeDir(RockerExtension):
306 | @staticmethod
307 | def get_name():
308 | return 'home'
309 |
310 | def __init__(self):
311 | self.name = HomeDir.get_name()
312 |
313 | def get_docker_args(self, cliargs):
314 | return ' -v %s:%s ' % (Path.home(), Path.home())
315 |
316 | @staticmethod
317 | def register_arguments(parser, defaults):
318 | parser.add_argument(name_to_argument(HomeDir.get_name()),
319 | action='store_true',
320 | default=defaults.get(HomeDir.get_name(), None),
321 | help="mount the users home directory")
322 |
323 |
324 | class User(RockerExtension):
325 | @staticmethod
326 | def get_name():
327 | return 'user'
328 |
329 | def get_environment_subs(self):
330 | if not self._env_subs:
331 | user_vars = ['name', 'uid', 'gid', 'gecos','dir', 'shell']
332 | userinfo = pwd.getpwuid(os.getuid())
333 | self._env_subs = {
334 | k: getattr(userinfo, 'pw_' + k)
335 | for k in user_vars }
336 | return self._env_subs
337 |
338 | def __init__(self):
339 | self._env_subs = None
340 | self.name = User.get_name()
341 |
342 | def get_snippet(self, cliargs):
343 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
344 | substitutions = self.get_environment_subs()
345 | if 'user_override_name' in cliargs and cliargs['user_override_name']:
346 | substitutions['name'] = cliargs['user_override_name']
347 | substitutions['dir'] = os.path.join('/home/', cliargs['user_override_name'])
348 | substitutions['user_preserve_home'] = True if 'user_preserve_home' in cliargs and cliargs['user_preserve_home'] else False
349 | if 'user_preserve_groups' in cliargs and isinstance(cliargs['user_preserve_groups'], list):
350 | query_groups = cliargs['user_preserve_groups']
351 | all_groups = grp.getgrall()
352 | if query_groups:
353 | matched_groups = [g for g in all_groups if g.gr_name in query_groups]
354 | matched_group_names = [g.gr_name for g in matched_groups]
355 | unmatched_groups = [n for n in cliargs['user_preserve_groups'] if n not in matched_group_names]
356 | if unmatched_groups:
357 | print('Warning skipping groups %s because they do not exist on the host.' % unmatched_groups)
358 | substitutions['user_groups'] = ' '.join(['{};{}'.format(g.gr_name, g.gr_gid) for g in matched_groups])
359 | else:
360 | substitutions['user_groups'] = ' '.join(['{};{}'.format(g.gr_name, g.gr_gid) for g in all_groups if substitutions['name'] in g.gr_mem])
361 | else:
362 | substitutions['user_groups'] = ''
363 | substitutions['user_preserve_groups_permissive'] = True if 'user_preserve_groups_permissive' in cliargs and cliargs['user_preserve_groups_permissive'] else False
364 | substitutions['home_extension_active'] = True if 'home' in cliargs and cliargs['home'] else False
365 | if 'user_override_shell' in cliargs and cliargs['user_override_shell'] is not None:
366 | if cliargs['user_override_shell'] == '':
367 | substitutions['shell'] = None
368 | else:
369 | substitutions['shell'] = cliargs['user_override_shell']
370 | return empy_expand(snippet, substitutions)
371 |
372 | @staticmethod
373 | def register_arguments(parser, defaults):
374 | parser.add_argument(name_to_argument(User.get_name()),
375 | action='store_true',
376 | default=defaults.get('user', None),
377 | help="mount the current user's id and run as that user")
378 | parser.add_argument('--user-override-name',
379 | action='store',
380 | default=defaults.get('user-override-name', None),
381 | help="override the current user's name")
382 | parser.add_argument('--user-preserve-home',
383 | action='store_true',
384 | default=defaults.get('user-preserve-home', False),
385 | help="Do not delete home directory if it exists when making a new user.")
386 | parser.add_argument('--user-preserve-groups',
387 | action='store',
388 | nargs='*',
389 | default=defaults.get('user-preserve-groups', False),
390 | help="Assign user to same groups as he belongs in host. If arguments provided they are the explicit list of groups.")
391 | parser.add_argument('--user-preserve-groups-permissive',
392 | action='store_true',
393 | default=defaults.get('user-preserve-groups-permissive', False),
394 | help="If using user-preserve-groups allow failures in assignment."
395 | "This is important if the host and target have different rules. https://unix.stackexchange.com/a/11481/83370" )
396 | parser.add_argument('--user-override-shell',
397 | action='store',
398 | default=defaults.get('user-override-shell', None),
399 | help="Override the current user's shell. Set to empty string to use container default shell")
400 |
401 |
402 | class Environment(RockerExtension):
403 | @staticmethod
404 | def get_name():
405 | return 'env'
406 |
407 | def __init__(self):
408 | self.name = Environment.get_name()
409 |
410 | def get_snippet(self, cli_args):
411 | return ''
412 |
413 | def get_docker_args(self, cli_args):
414 | args = ['']
415 |
416 | if cli_args.get('env'):
417 | envs = [ x for sublist in cli_args['env'] for x in sublist]
418 | for env in envs:
419 | args.append('-e {0}'.format(quote(env)))
420 |
421 | if cli_args.get('env_file'):
422 | env_files = [ x for sublist in cli_args['env_file'] for x in sublist]
423 | for env_file in env_files:
424 | args.append('--env-file {0}'.format(quote(env_file)))
425 |
426 | return ' '.join(args)
427 |
428 | @staticmethod
429 | def register_arguments(parser, defaults):
430 | parser.add_argument('--env', '-e',
431 | metavar='NAME[=VALUE]',
432 | type=str,
433 | nargs='+',
434 | action='append',
435 | default=defaults.get(Environment.get_name(), []),
436 | help='set environment variables')
437 | parser.add_argument('--env-file',
438 | type=str,
439 | nargs=1,
440 | action='append',
441 | help='set environment variable via env-file')
442 |
443 | @classmethod
444 | def check_args_for_activation(cls, cli_args):
445 | """ Returns true if the arguments indicate that this extension should be activated otherwise false."""
446 | return True if cli_args.get('env') or cli_args.get('env_file') else False
447 |
448 |
449 | class Privileged(RockerExtension):
450 | """Add --privileged to docker arguments."""
451 | @staticmethod
452 | def get_name():
453 | return 'privileged'
454 |
455 | def __init__(self):
456 | self.name = Privileged.get_name()
457 |
458 | def get_snippet(self, cli_args):
459 | return ''
460 |
461 | def get_docker_args(self, cli_args):
462 | return ' --privileged'
463 |
464 | @staticmethod
465 | def register_arguments(parser, defaults):
466 | parser.add_argument(name_to_argument(Privileged.get_name()),
467 | action='store_true',
468 | default=defaults.get(Privileged.get_name(), None),
469 | help="give extended privileges to the container")
470 |
471 |
472 | class GroupAdd(RockerExtension):
473 | """Add additional groups to running container."""
474 | @staticmethod
475 | def get_name():
476 | return 'group_add'
477 |
478 | def __init__(self):
479 | self.name = GroupAdd.get_name()
480 |
481 | def get_preamble(self, cliargs):
482 | return ''
483 |
484 | def get_docker_args(self, cliargs):
485 | args = ['']
486 | groups = cliargs.get('group_add', [])
487 | for group in groups:
488 | args.append(' --group-add {0}'.format(group))
489 | return ' '.join(args)
490 |
491 | @staticmethod
492 | def register_arguments(parser, defaults):
493 | parser.add_argument(name_to_argument(GroupAdd.get_name()),
494 | default=defaults.get(GroupAdd.get_name(), None),
495 | action='append',
496 | help="Add additional groups to join.")
497 |
498 | class ShmSize(RockerExtension):
499 | @staticmethod
500 | def get_name():
501 | return 'shm_size'
502 |
503 | def __init__(self):
504 | self.name = ShmSize.get_name()
505 |
506 | def get_preamble(self, cliargs):
507 | return ''
508 |
509 | def get_docker_args(self, cliargs):
510 | args = ''
511 | shm_size = cliargs.get('shm_size', None)
512 | if shm_size:
513 | args += f' --shm-size {shm_size} '
514 | return args
515 |
516 | @staticmethod
517 | def register_arguments(parser, defaults={}):
518 | parser.add_argument('--shm-size',
519 | default=defaults.get('shm_size', None),
520 | help="Set the size of the shared memory for the container (e.g., 512m, 1g).")
--------------------------------------------------------------------------------
/src/rocker/git_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from argparse import ArgumentTypeError
16 | import getpass
17 | import os
18 | from rocker.extensions import RockerExtension
19 |
20 |
21 | class Git(RockerExtension):
22 |
23 | name = 'git'
24 |
25 | @classmethod
26 | def get_name(cls):
27 | return cls.name
28 |
29 |
30 | def get_docker_args(self, cli_args):
31 | args = ''
32 | # only parameterized for testing
33 | system_gitconfig = cli_args.get('git_config_path_system', '/etc/gitconfig')
34 | system_gitconfig_target = '/etc/gitconfig'
35 | user_gitconfig = cli_args.get('git_config_path', os.path.expanduser('~/.gitconfig'))
36 | user_gitconfig_target = '/root/.gitconfig'
37 | if 'user' in cli_args and cli_args['user']:
38 | username = getpass.getuser()
39 | if 'user_override_name' in cli_args and cli_args['user_override_name']:
40 | username = cli_args['user_override_name']
41 | user_gitconfig_target = '/home/%(username)s/.gitconfig' % locals()
42 | if os.path.exists(system_gitconfig):
43 | args += ' -v {system_gitconfig}:{system_gitconfig_target}:ro'.format(**locals())
44 | if os.path.exists(user_gitconfig):
45 | args += ' -v {user_gitconfig}:{user_gitconfig_target}:ro'.format(**locals())
46 | return args
47 |
48 | @staticmethod
49 | def register_arguments(parser, defaults):
50 | parser.add_argument('--git',
51 | action='store_true',
52 | default=defaults.get(Git.get_name(), None),
53 | help="Use the global Git settings from the host (/etc/gitconfig and ~/.gitconfig)")
54 | parser.add_argument('--git-config-path',
55 | action='store',
56 | default=defaults.get('git_config_path', os.path.expanduser('~/.gitconfig')),
57 | help="Override the path to the git config default: %s" % defaults.get('git_config_path', os.path.expanduser('~/.gitconfig')))
58 |
--------------------------------------------------------------------------------
/src/rocker/nvidia_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import getpass
17 | import tempfile
18 | from packaging.version import Version
19 | import pkgutil
20 | from pathlib import Path
21 | import subprocess
22 | import sys
23 |
24 | from .os_detector import detect_os
25 |
26 | from .extensions import name_to_argument
27 | from .core import get_docker_client
28 | from .core import RockerExtension
29 | from .em import empy_expand
30 |
31 | GLVND_VERSION_POLICY_LATEST_LTS='latest_lts'
32 |
33 | NVIDIA_GLVND_VALID_VERSIONS=['16.04', '18.04','20.04', '22.04', '24.04']
34 |
35 | def get_docker_version():
36 | docker_version_raw = get_docker_client().version()['Version']
37 | # Fix for version 17.09.0-ce
38 | return Version(docker_version_raw.split('-')[0])
39 |
40 | def glvnd_version_from_policy(image_version, policy):
41 | # Default policy GLVND_VERSION_POLICY_LATEST_LTS
42 | if not policy:
43 | policy = GLVND_VERSION_POLICY_LATEST_LTS
44 |
45 | if policy == GLVND_VERSION_POLICY_LATEST_LTS:
46 | if image_version in ['16.04', '16.10', '17.04', '17.10']:
47 | return '16.04'
48 | if image_version in ['18.04', '18.10', '19.04', '19.10']:
49 | return '18.04'
50 | if image_version in ['20.04', '20.10', '21.04', '21.10']:
51 | return '20.04'
52 | if image_version in ['22.04', '22.10', '23.04', '23.10']:
53 | return '22.04'
54 | # 24.04 is not available yet
55 | # if image_version in ['24.04', '24.10', '25.04', '25.10']:
56 | # return '24.04'
57 | return '22.04'
58 | return None
59 |
60 | class X11(RockerExtension):
61 | @staticmethod
62 | def get_name():
63 | return 'x11'
64 |
65 | def __init__(self):
66 | self.name = X11.get_name()
67 | self._env_subs = None
68 | self._xauth = None
69 |
70 | def get_docker_args(self, cliargs):
71 | assert self._xauth, 'xauth not initialized, get_docker_args must be called after precodition_environment'
72 | xauth = self._xauth.name
73 | return " -e DISPLAY -e TERM \
74 | -e QT_X11_NO_MITSHM=1 \
75 | -e XAUTHORITY=%(xauth)s -v %(xauth)s:%(xauth)s \
76 | -v /tmp/.X11-unix:/tmp/.X11-unix \
77 | -v /etc/localtime:/etc/localtime:ro " % locals()
78 |
79 | def precondition_environment(self, cliargs):
80 | self._xauth = tempfile.NamedTemporaryFile(prefix='.docker', suffix='.xauth', delete=not cliargs.get('nocleanup'))
81 | xauth = self._xauth.name
82 | display = os.getenv('DISPLAY')
83 | # Make sure processes in the container can connect to the x server
84 | # Necessary so gazebo can create a context for OpenGL rendering (even headless)
85 | if not os.path.exists(xauth): #if [ ! -f $XAUTH ]
86 | Path(xauth).touch()
87 | # print("touched %s" % xauth)
88 | cmd = 'xauth nlist %(display)s | sed -e \'s/^..../ffff/\' | xauth -f %(xauth)s nmerge -' % locals()
89 | # print("runnning %s" % cmd)
90 | try:
91 | subprocess.check_call(cmd, shell=True)
92 | except subprocess.CalledProcessError as ex:
93 | print("Failed setting up XAuthority with command %s" % cmd)
94 | raise ex
95 |
96 | @staticmethod
97 | def register_arguments(parser, defaults):
98 | parser.add_argument(name_to_argument(X11.get_name()),
99 | action='store_true',
100 | default=defaults.get(X11.get_name(), None),
101 | help="Enable x11")
102 |
103 |
104 | class Nvidia(RockerExtension):
105 | @staticmethod
106 | def get_name():
107 | return 'nvidia'
108 |
109 | def __init__(self):
110 | self._env_subs = None
111 | self.name = Nvidia.get_name()
112 | self.supported_distros = ['Ubuntu', 'Debian GNU/Linux']
113 | self.supported_versions = ['16.04', '18.04', '20.04', '10', '22.04', '24.04']
114 |
115 |
116 | def get_environment_subs(self, cliargs={}):
117 | if not self._env_subs:
118 | self._env_subs = {}
119 | self._env_subs['user_id'] = os.getuid()
120 | self._env_subs['username'] = getpass.getuser()
121 |
122 | # non static elements test every time
123 | detected_os = detect_os(cliargs['base_image'], print, nocache=cliargs.get('nocache', False))
124 | if detected_os is None:
125 | print("WARNING unable to detect os for base image '%s', maybe the base image does not exist" % cliargs['base_image'])
126 | sys.exit(1)
127 | dist, ver, codename = detected_os
128 |
129 | self._env_subs['image_distro_id'] = dist
130 | if self._env_subs['image_distro_id'] not in self.supported_distros:
131 | print("WARNING distro id %s not supported by Nvidia supported " % self._env_subs['image_distro_id'], self.supported_distros)
132 | sys.exit(1)
133 | self._env_subs['image_distro_version'] = ver
134 | if self._env_subs['image_distro_version'] not in self.supported_versions:
135 | print("WARNING distro %s version %s not in supported list by Nvidia supported versions" % (dist, ver), self.supported_versions)
136 | sys.exit(1)
137 | # TODO(tfoote) add a standard mechanism for checking preconditions and disabling plugins
138 | nvidia_glvnd_version = cliargs.get('nvidia_glvnd_version', None)
139 | if not nvidia_glvnd_version:
140 | nvidia_glvnd_version = glvnd_version_from_policy(ver, cliargs.get('nvidia_glvnd_policy', None) )
141 | self._env_subs['nvidia_glvnd_version'] = nvidia_glvnd_version
142 |
143 | return self._env_subs
144 |
145 | def get_preamble(self, cliargs):
146 | preamble = pkgutil.get_data('rocker', 'templates/%s_preamble.Dockerfile.em' % self.name).decode('utf-8')
147 | return empy_expand(preamble, self.get_environment_subs(cliargs))
148 |
149 | def get_snippet(self, cliargs):
150 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
151 | return empy_expand(snippet, self.get_environment_subs(cliargs))
152 |
153 | def get_docker_args(self, cliargs):
154 | force_flag = cliargs.get('nvidia', None)
155 | if force_flag == 'runtime':
156 | return " --runtime=nvidia"
157 | if force_flag == 'gpus':
158 | return " --gpus all"
159 | if get_docker_version() >= Version("19.03"):
160 | return " --gpus all"
161 | return " --runtime=nvidia"
162 |
163 | @staticmethod
164 | def register_arguments(parser, defaults):
165 | parser.add_argument(name_to_argument(Nvidia.get_name()),
166 | choices=['auto', 'runtime', 'gpus'],
167 | nargs='?',
168 | const='auto',
169 | default=defaults.get(Nvidia.get_name(), None),
170 | help="Enable nvidia. Default behavior is to pick flag based on docker version.")
171 | parser.add_argument('--nvidia-glvnd-version',
172 | choices=NVIDIA_GLVND_VALID_VERSIONS,
173 | default=defaults.get('nvidia-glvnd-version', None),
174 | help="Explicitly select an nvidia glvnd version")
175 | parser.add_argument('--nvidia-glvnd-policy',
176 | choices=[GLVND_VERSION_POLICY_LATEST_LTS],
177 | default=defaults.get('nvidia-glvnd-policy', GLVND_VERSION_POLICY_LATEST_LTS),
178 | help="Set an nvidia glvnd version policy if version is unset")
179 |
180 | class Cuda(RockerExtension):
181 | @staticmethod
182 | def get_name():
183 | return 'cuda'
184 |
185 | def __init__(self):
186 | self._env_subs = None
187 | self.name = Cuda.get_name()
188 | self.supported_distros = ['Ubuntu', 'Debian GNU/Linux']
189 | self.supported_versions = ['20.04', '22.04', '24.04', '11', '12'] # Debian 11 and 12
190 |
191 | def get_environment_subs(self, cliargs={}):
192 | if not self._env_subs:
193 | self._env_subs = {}
194 | self._env_subs['user_id'] = os.getuid()
195 | self._env_subs['username'] = getpass.getuser()
196 |
197 | # non static elements test every time
198 | detected_os = detect_os(cliargs['base_image'], print, nocache=cliargs.get('nocache', False))
199 | if detected_os is None:
200 | print("WARNING unable to detect os for base image '%s', maybe the base image does not exist" % cliargs['base_image'])
201 | sys.exit(1)
202 | dist, ver, codename = detected_os
203 |
204 | self._env_subs['download_osstring'] = dist.split()[0].lower()
205 | self._env_subs['download_verstring'] = ver.replace('.', '')
206 | self._env_subs['download_keyid'] = '3bf863cc'
207 |
208 | self._env_subs['image_distro_id'] = dist
209 | if self._env_subs['image_distro_id'] not in self.supported_distros:
210 | print("WARNING distro id %s not supported by Cuda supported " % self._env_subs['image_distro_id'], self.supported_distros)
211 | sys.exit(1)
212 | self._env_subs['image_distro_version'] = ver
213 | if self._env_subs['image_distro_version'] not in self.supported_versions:
214 | print("WARNING distro %s version %s not in supported list by Nvidia supported versions" % (dist, ver), self.supported_versions)
215 | sys.exit(1)
216 | # TODO(tfoote) add a standard mechanism for checking preconditions and disabling plugins
217 |
218 | return self._env_subs
219 |
220 | def get_preamble(self, cliargs):
221 | return ''
222 | # preamble = pkgutil.get_data('rocker', 'templates/%s_preamble.Dockerfile.em' % self.name).decode('utf-8')
223 | # return empy_expand(preamble, self.get_environment_subs(cliargs))
224 |
225 | def get_snippet(self, cliargs):
226 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % self.name).decode('utf-8')
227 | return empy_expand(snippet, self.get_environment_subs(cliargs))
228 |
229 | def get_docker_args(self, cliargs):
230 | return ""
231 | # Runtime requires --nvidia option too
232 |
233 | @staticmethod
234 | def register_arguments(parser, defaults):
235 | parser.add_argument(name_to_argument(Cuda.get_name()),
236 | action='store_true',
237 | default=defaults.get('cuda', None),
238 | help="Install cuda and nvidia-cuda-dev into the container")
239 |
--------------------------------------------------------------------------------
/src/rocker/os_detector.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019-2022 Arm Ltd., Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import json
16 | import pexpect
17 |
18 | from io import BytesIO as StringIO
19 |
20 | from .core import docker_build, get_docker_client
21 |
22 |
23 | DETECTION_TEMPLATE="""
24 | FROM golang:1.19 as detector
25 |
26 | # For reliability, pin a distro-detect commit instead of targeting a branch.
27 | RUN git clone -q https://github.com/dekobon/distro-detect.git && \
28 | cd distro-detect && \
29 | git checkout -q 5f5b9c724b9d9a117732d2a4292e6288905734e1 && \
30 | CGO_ENABLED=0 go build .
31 |
32 | FROM %(image_name)s
33 |
34 | COPY --from=detector /go/distro-detect/distro-detect /tmp/detect_os
35 | ENTRYPOINT [ "/tmp/detect_os", "-format", "json-one-line" ]
36 | CMD [ "" ]
37 | """
38 |
39 | _detect_os_cache = dict()
40 |
41 | def detect_os(image_name, output_callback=None, nocache=False):
42 | # Do not rerun OS detection if there is already a cached result for the given image
43 | if image_name in _detect_os_cache:
44 | return _detect_os_cache[image_name]
45 |
46 | iof = StringIO((DETECTION_TEMPLATE % locals()).encode())
47 | tag_name = "rocker:" + f"os_detect_{image_name}".replace(':', '_').replace('/', '_')
48 | image_id = docker_build(
49 | fileobj=iof,
50 | output_callback=output_callback,
51 | nocache=nocache,
52 | forcerm=True, # Remove intermediate containers from RUN commands in DETECTION_TEMPLATE
53 | tag=tag_name
54 | )
55 | if not image_id:
56 | if output_callback:
57 | output_callback('Failed to build detector image')
58 | return None
59 |
60 | cmd="docker run -it --rm %s" % image_id
61 | if output_callback:
62 | output_callback("running, ", cmd)
63 | p = pexpect.spawn(cmd)
64 | output = p.read().decode()
65 | if output_callback:
66 | output_callback("output: ", output)
67 | p.terminate()
68 |
69 | # Clean up the image
70 | client = get_docker_client()
71 | client.remove_image(image=tag_name)
72 |
73 | if p.exitstatus == 0:
74 | try:
75 | detect_dict = json.loads(output.strip())
76 | except ValueError:
77 | if output_callback:
78 | output_callback('Failed to parse JSON')
79 | return None
80 |
81 | dist = detect_dict.get('name', '')
82 | os_release = detect_dict.get('os_release', {})
83 | ver = os_release.get('VERSION_ID', '')
84 | codename = os_release.get('VERSION_CODENAME', '')
85 |
86 | _detect_os_cache[image_name] = (dist, ver, codename)
87 | return _detect_os_cache[image_name]
88 | else:
89 | if output_callback:
90 | output_callback("/tmp/detect_os failed:")
91 | for l in output.splitlines():
92 | output_callback("> %s" % l)
93 | return None
94 |
--------------------------------------------------------------------------------
/src/rocker/rmw_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from argparse import ArgumentTypeError
16 | import os
17 | import pkgutil
18 |
19 | from .em import empy_expand
20 | from rocker.extensions import RockerExtension
21 | from rocker.extensions import name_to_argument
22 |
23 |
24 | class RMW(RockerExtension):
25 | rmw_map = {
26 | 'cyclonedds': ['ros-${ROS_DISTRO}-rmw-cyclonedds-cpp'],
27 | 'fastrtps' : ['ros-${ROS_DISTRO}-rmw-fastrtps-cpp'],
28 | 'zenoh' : ['ros-${ROS_DISTRO}-rmw-zenoh-cpp', 'ros-${ROS_DISTRO}-zenoh-vendor'],
29 | # Second dependency not 100% necessary but validates dual package detection in tests.
30 | # TODO(tfoote) Enable connext with license acceptance method
31 | # 'connextdds': ['ros-${ROS_DISTRO}-rmw-connextdds'],
32 | }
33 |
34 | @staticmethod
35 | def get_package_names(rmw_name):
36 | return RMW.rmw_map[rmw_name]
37 |
38 | @staticmethod
39 | def get_name():
40 | return 'rmw'
41 |
42 | def __init__(self):
43 | self._env_subs = None
44 | self.name = RMW.get_name()
45 |
46 | def get_docker_args(self, cli_args):
47 | rmw_config = cli_args.get('rmw')
48 | if not rmw_config:
49 | return '' # not active
50 | implementation = rmw_config[0]
51 | args = f' -e RMW_IMPLEMENTATION=rmw_{implementation}_cpp'
52 | return args #% self.get_environment_subs()
53 |
54 | def get_environment_subs(self):
55 | if not self._env_subs:
56 | self._env_subs = {}
57 | return self._env_subs
58 |
59 | def get_preamble(self, cliargs):
60 | return ''
61 |
62 | def get_snippet(self, cliargs):
63 | snippet = pkgutil.get_data('rocker', 'templates/%s_snippet.Dockerfile.em' % RMW.get_name()).decode('utf-8')
64 | data = self.get_environment_subs()
65 | # data['rosdistro'] = cliargs.get('rosdistro', 'rolling')
66 | rmw = cliargs.get('rmw', None)
67 | if rmw:
68 | rmw = rmw[0]
69 | else:
70 | return '' # rmw not active
71 | data['rmw'] = rmw
72 | data['packages'] = RMW.get_package_names(rmw)
73 | # data['rosdistro'] = 'rolling'
74 | return empy_expand(snippet, data)
75 |
76 | @staticmethod
77 | def register_arguments(parser, defaults):
78 | parser.add_argument(name_to_argument(RMW.get_name()),
79 | default=defaults.get('rmw', None),
80 | nargs=1,
81 | choices=RMW.rmw_map.keys(),
82 | help="Set the default RMW implementation")
83 |
84 | # parser.add_argument('rosdistro',
85 | # default=defaults.get('rosdistro', None),
86 | # help="Set the default rosdistro, else autodetect")
87 |
--------------------------------------------------------------------------------
/src/rocker/ssh_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from argparse import ArgumentTypeError
16 | import os
17 | import shlex
18 |
19 | from rocker.extensions import RockerExtension
20 |
21 |
22 | class Ssh(RockerExtension):
23 |
24 | name = 'ssh'
25 |
26 | @classmethod
27 | def get_name(cls):
28 | return cls.name
29 |
30 | def precondition_environment(self, cli_args):
31 | pass
32 |
33 | def validate_environment(self, cli_args):
34 | pass
35 |
36 | def get_preamble(self, cli_args):
37 | return ''
38 |
39 | def get_snippet(self, cli_args):
40 | return ''
41 |
42 | def get_docker_args(self, cli_args):
43 | args = ''
44 | if 'SSH_AUTH_SOCK' in os.environ:
45 | args += ' -e SSH_AUTH_SOCK -v ' + shlex.quote('{SSH_AUTH_SOCK}:{SSH_AUTH_SOCK}'.format(**os.environ))
46 | return args
47 |
48 | @staticmethod
49 | def register_arguments(parser, defaults):
50 | parser.add_argument('--ssh',
51 | action='store_true',
52 | default=defaults.get(Ssh.get_name(), None),
53 | help="Forward SSH agent into the container")
54 |
--------------------------------------------------------------------------------
/src/rocker/templates/Dockerfile.em:
--------------------------------------------------------------------------------
1 | TODO move logic here
2 |
3 | {{ for p in prefixes }}
4 |
5 | FROM {{base_image}}
6 |
7 | {{ for s in snippets }}
--------------------------------------------------------------------------------
/src/rocker/templates/bash_cmd_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | CMD /bin/bash
2 |
--------------------------------------------------------------------------------
/src/rocker/templates/cuda_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | # Installation instructions from NVIDIA:
2 | # https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Debian&target_version=11&target_type=deb_network
3 | # https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=22.04&target_type=deb_network
4 |
5 | # Keep the dockerfile non-interactive
6 | # TODO(tfoote) make this more generic/shared across instances
7 | ARG DEBIAN_FRONTEND=noninteractive
8 |
9 | # Prerequisites
10 | RUN apt-get update && apt-get install -y --no-install-recommends \
11 | wget software-properties-common gnupg2 \
12 | && rm -rf /var/lib/apt/lists/*
13 |
14 | # Enable contrib on debian to get required
15 | # https://packages.debian.org/bullseye/glx-alternative-nvidia
16 |
17 | RUN \
18 | wget -q https://developer.download.nvidia.com/compute/cuda/repos/@(download_osstring)@(download_verstring)/x86_64/cuda-keyring_1.1-1_all.deb && \
19 | dpkg -i cuda-keyring_1.1-1_all.deb && \
20 | rm cuda-keyring_1.1-1_all.deb && \
21 | \@[if download_osstring == 'debian']@
22 | add-apt-repository contrib && \
23 | \@[end if]@
24 | apt-get update && \
25 | apt-get -y install cuda-toolkit && \
26 | rm -rf /var/lib/apt/lists/*
27 |
28 | # File conflict problem with libnvidia-ml.so.1 and libcuda.so.1
29 | # https://github.com/NVIDIA/nvidia-docker/issues/1551
30 | RUN rm -rf /usr/lib/x86_64-linux-gnu/libnv*
31 | RUN rm -rf /usr/lib/x86_64-linux-gnu/libcuda*
32 |
33 | # TODO(tfoote) Add documentation of why these are required
34 | ENV PATH /usr/local/cuda/bin${PATH:+:${PATH}}
35 | ENV LD_LIBRARY_PATH /usr/local/cuda/lib64/stubs:/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
--------------------------------------------------------------------------------
/src/rocker/templates/dev_helpers_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | # workspace development helpers
2 | RUN apt-get update \
3 | && apt-get install -y \
4 | byobu \
5 | emacs \
6 | && apt-get clean
7 |
--------------------------------------------------------------------------------
/src/rocker/templates/nvidia_preamble.Dockerfile.em:
--------------------------------------------------------------------------------
1 | FROM nvidia/opengl:1.0-glvnd-devel-ubuntu@(nvidia_glvnd_version) as glvnd
2 |
--------------------------------------------------------------------------------
/src/rocker/templates/nvidia_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | @[if image_distro_version == '16.04']@
2 | # Open nvidia-docker2 GL support
3 | COPY --from=glvnd /usr/local/lib/x86_64-linux-gnu /usr/local/lib/x86_64-linux-gnu
4 | COPY --from=glvnd /usr/local/lib/i386-linux-gnu /usr/local/lib/i386-linux-gnu
5 | COPY --from=glvnd /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu
6 | COPY --from=glvnd /usr/lib/i386-linux-gnu /usr/lib/i386-linux-gnu
7 | # if the path is alreaady present don't fail because of being unable to append
8 | RUN ( echo '/usr/local/lib/x86_64-linux-gnu' >> /etc/ld.so.conf.d/glvnd.conf && ldconfig || grep -q /usr/local/lib/x86_64-linux-gnu /etc/ld.so.conf.d/glvnd.conf ) && \
9 | ( echo '/usr/local/lib/i386-linux-gnu' >> /etc/ld.so.conf.d/glvnd.conf && ldconfig || grep -q /usr/local/lib/i386-linux-gnu /etc/ld.so.conf.d/glvnd.conf )
10 | ENV LD_LIBRARY_PATH /usr/local/lib/x86_64-linux-gnu:/usr/local/lib/i386-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
11 |
12 | COPY --from=glvnd /usr/local/share/glvnd/egl_vendor.d/10_nvidia.json /usr/local/share/glvnd/egl_vendor.d/10_nvidia.json
13 | @[else]@
14 | RUN apt-get update && apt-get install -y --no-install-recommends \
15 | libglvnd0 \
16 | libgl1 \
17 | libglx0 \
18 | libegl1 \
19 | libgles2 \
20 | && rm -rf /var/lib/apt/lists/*
21 | COPY --from=glvnd /usr/share/glvnd/egl_vendor.d/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json
22 | @[end if]@
23 |
24 |
25 |
26 |
27 | ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all}
28 | ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:-all}
29 |
--------------------------------------------------------------------------------
/src/rocker/templates/pulse_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | RUN mkdir -p /etc/pulse
2 | RUN echo '\n\
3 | # Connect to the hosts server using the mounted UNIX socket\n\
4 | default-server = unix:/run/user/@(user_id)/pulse/native\n\
5 | \n\
6 | # Prevent a server running in the container\n\
7 | autospawn = no\n\
8 | daemon-binary = /bin/true\n\
9 | \n\
10 | # Prevent the use of shared memory\n\
11 | enable-shm = false\n\
12 | \n'\
13 | > /etc/pulse/client.conf
14 |
--------------------------------------------------------------------------------
/src/rocker/templates/rmw_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | # workspace development helpers
2 |
3 | # TODO(tfoote) This could be optimized to skip repeated apt updates.
4 | @[ if rmw ]@
5 | RUN \
6 | if [ -z "${ROS_DISTRO}" ]; then echo "ROS_DISTRO is unset cannot override RMW" ; exit 1 ; fi ;\
7 | @[for package in packages]@
8 | if ! dpkg -l @(packages) | grep -q ^ii ; then \
9 | apt-get update && \
10 | DEBIAN_FRONTENT=non-interactive apt-get install -qy --no-install-recommends\
11 | @(package) ;\
12 | else \
13 | echo "Found rmw package @(package) no need to install" ; \
14 | fi ; \
15 | @[end for]@
16 | apt-get clean ;\
17 | echo "Done detecting packages for rmw"
18 | @[ end if ]@
19 |
20 |
--------------------------------------------------------------------------------
/src/rocker/templates/user_snippet.Dockerfile.em:
--------------------------------------------------------------------------------
1 | # make sure sudo is installed to be able to give user sudo access in docker
2 | RUN if ! command -v sudo >/dev/null; then \
3 | apt-get update \
4 | && apt-get install -y sudo \
5 | && apt-get clean; \
6 | fi
7 |
8 | @[if name != 'root']@
9 | RUN existing_user_by_uid=`getent passwd "@(uid)" | cut -f1 -d: || true` && \
10 | if [ -n "${existing_user_by_uid}" ]; then userdel @('' if user_preserve_home else '-r') "${existing_user_by_uid}"; fi && \
11 | existing_user_by_name=`getent passwd "@(name)" | cut -f1 -d: || true` && \
12 | existing_user_uid=`getent passwd "@(name)" | cut -f3 -d: || true` && \
13 | if [ -n "${existing_user_by_name}" ]; then find / -uid ${existing_user_uid} -exec chown -h @(uid) {} + || true ; find / -gid ${existing_user_uid} -exec chgrp -h @(uid) {} + || true ; fi && \
14 | if [ -n "${existing_user_by_name}" ]; then userdel @('' if user_preserve_home else '-r') "${existing_user_by_name}"; fi && \
15 | existing_group_by_gid=`getent group "@(gid)" | cut -f1 -d: || true` && \
16 | if [ -z "${existing_group_by_gid}" ]; then \
17 | groupadd -g "@(gid)" "@name"; \
18 | fi && \
19 | useradd --no-log-init --no-create-home --uid "@(uid)" @(str('-s ' + shell) if shell else '') -c "@(gecos)" -g "@(gid)" -d "@(dir)" "@(name)" && \
20 | @[if user_groups != '']@
21 | user_groups="@(user_groups)" && \
22 | for groupinfo in ${user_groups}; do \
23 | existing_group_by_name=`getent group ${groupinfo%;*} || true`; \
24 | existing_group_by_gid=`getent group ${groupinfo#*;} || true`; \
25 | if [ -z "${existing_group_by_name}" ] && [ -z "${existing_group_by_gid}" ]; then \
26 | 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); \
27 | elif [ "${existing_group_by_name}" = "${existing_group_by_gid}" ]; then \
28 | 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); \
29 | fi; \
30 | done && \
31 | @[end if]@
32 | echo "@(name) ALL=NOPASSWD: ALL" >> /etc/sudoers.d/rocker
33 |
34 | @[if not home_extension_active ]@
35 | # Making sure a home directory exists if we haven't mounted the user's home directory explicitly
36 | RUN mkdir -p "$(dirname "@(dir)")" && mkhomedir_helper @(name)
37 | @[end if]@
38 | WORKDIR @(dir)
39 | @[else]@
40 | # Detected user is root, which already exists so not creating new user.
41 | @[end if]@
42 |
--------------------------------------------------------------------------------
/src/rocker/ulimit_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from argparse import ArgumentTypeError
16 | import re
17 | from rocker.extensions import RockerExtension, name_to_argument
18 |
19 |
20 | class Ulimit(RockerExtension):
21 | """
22 | A RockerExtension to handle ulimit settings for Docker containers.
23 |
24 | This extension allows specifying ulimit options in the format TYPE=SOFT_LIMIT[:HARD_LIMIT]
25 | and validates the format before passing them as Docker arguments.
26 | """
27 | EXPECTED_FORMAT = "TYPE=SOFT_LIMIT[:HARD_LIMIT]"
28 |
29 | @staticmethod
30 | def get_name():
31 | return 'ulimit'
32 |
33 | def get_docker_args(self, cliargs):
34 | args = ['']
35 | ulimits = [x for sublist in cliargs[Ulimit.get_name()] for x in sublist]
36 | for ulimit in ulimits:
37 | if self.arg_format_is_valid(ulimit):
38 | args.append(f"--ulimit {ulimit}")
39 | else:
40 | raise ArgumentTypeError(
41 | f"Error processing {Ulimit.get_name()} flag '{ulimit}': expected format"
42 | f" {Ulimit.EXPECTED_FORMAT}")
43 | return ' '.join(args)
44 |
45 | def arg_format_is_valid(self, arg: str):
46 | """
47 | Validate the format of the ulimit argument.
48 |
49 | Args:
50 | arg (str): The ulimit argument to validate.
51 |
52 | Returns:
53 | bool: True if the format is valid, False otherwise.
54 | """
55 | ulimit_format = r'(\w+)=(\w+)(:\w+)?$'
56 | match = re.match(ulimit_format, arg)
57 | return match is not None
58 |
59 | @staticmethod
60 | def register_arguments(parser, defaults):
61 | parser.add_argument(name_to_argument(Ulimit.get_name()),
62 | type=str,
63 | nargs='+',
64 | action='append',
65 | metavar=Ulimit.EXPECTED_FORMAT,
66 | default=defaults.get(Ulimit.get_name(), None),
67 | help='ulimit options to add into the container.')
68 |
--------------------------------------------------------------------------------
/src/rocker/volume_extension.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Open Source Robotics Foundation
2 |
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from argparse import ArgumentParser
16 | from argparse import ArgumentTypeError
17 | import os
18 | from rocker.extensions import RockerExtension
19 |
20 |
21 | class Volume(RockerExtension):
22 |
23 | ARG_DOCKER_VOLUME = "-v"
24 | ARG_ROCKER_VOLUME = "--volume"
25 | name = 'volume'
26 |
27 | @classmethod
28 | def get_name(cls):
29 | return cls.name
30 |
31 | def get_docker_args(self, cli_args):
32 | """
33 | @param cli_args: {'volume': [[%arg%]]}
34 | - 'volume' is fixed.
35 | - %arg% can be:
36 | - %path_host%: a path on the host. Same path will be populated in
37 | the container.
38 | - %path_host%:%path_cont%
39 | - %path_host%:%path_cont%:%option%
40 | """
41 | args = ['']
42 |
43 | # flatten cli_args['volume']
44 | volumes = [ x for sublist in cli_args[self.name] for x in sublist]
45 |
46 | for volume in volumes:
47 | elems = volume.split(':')
48 | host_dir = os.path.abspath(elems[0])
49 | if len(elems) == 1:
50 | args.append('{0} {1}:{1}'.format(self.ARG_DOCKER_VOLUME, host_dir))
51 | elif len(elems) == 2:
52 | container_dir = elems[1]
53 | args.append('{0} {1}:{2}'.format(self.ARG_DOCKER_VOLUME, host_dir, container_dir))
54 | elif len(elems) == 3:
55 | container_dir = elems[1]
56 | options = elems[2]
57 | args.append('{0} {1}:{2}:{3}'.format(self.ARG_DOCKER_VOLUME, host_dir, container_dir, options))
58 | else:
59 | raise ArgumentTypeError(
60 | '{} expects arguments in format HOST-DIR[:CONTAINER-DIR[:OPTIONS]]'.format(self.ARG_ROCKER_VOLUME))
61 |
62 | return ' '.join(args)
63 |
64 | @staticmethod
65 | def register_arguments(parser: ArgumentParser, defaults: dict):
66 | parser.add_argument(Volume.ARG_ROCKER_VOLUME,
67 | metavar='HOST-DIR[:CONTAINER-DIR[:OPTIONS]]',
68 | type=str,
69 | nargs='+',
70 | action='append',
71 | help='volume(s) to map into the container. The last path must be followed by two dashes "--"')
72 |
--------------------------------------------------------------------------------
/stdeb.cfg:
--------------------------------------------------------------------------------
1 | [rocker]
2 | Debian-Version: 100
3 | No-Python2:
4 | Depends3: python3-docker, python3-empy, python3-pexpect, python3-packaging
5 | Conflicts3: python-rocker
6 | Suite: focal jammy noble bookworm trixie
7 | X-Python3-Version: >= 3.2
8 |
--------------------------------------------------------------------------------
/test/test_core.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import argparse
19 | import em
20 | import os
21 | import pwd
22 | import pytest
23 | import unittest
24 |
25 | from itertools import chain
26 |
27 | from rocker.core import DockerImageGenerator
28 | from rocker.core import ExtensionError
29 | from rocker.core import list_plugins
30 | from rocker.core import get_docker_client
31 | from rocker.core import get_rocker_version
32 | from rocker.core import get_user_name
33 | from rocker.core import RockerExtension
34 | from rocker.core import RockerExtensionManager
35 |
36 | class RockerCoreTest(unittest.TestCase):
37 |
38 | def setUp(self):
39 | # Work around interference between empy Interpreter
40 | # stdout proxy and test runner. empy installs a proxy on stdout
41 | # to be able to capture the information.
42 | # And the test runner creates a new stdout object for each test.
43 | # This breaks empy as it assumes that the proxy has persistent
44 | # between instances of the Interpreter class
45 | # empy will error with the exception
46 | # "em.Error: interpreter stdout proxy lost"
47 | em.Interpreter._wasProxyInstalled = False
48 |
49 | def test_list_plugins(self):
50 | plugins_found = list_plugins()
51 | plugin_names = plugins_found.keys()
52 | self.assertTrue('nvidia' in plugin_names )
53 | self.assertTrue('pulse' in plugin_names )
54 | self.assertTrue('user' in plugin_names )
55 | self.assertTrue('home' in plugin_names )
56 |
57 | def test_get_rocker_version(self):
58 | v = get_rocker_version()
59 | parts = v.split('.')
60 | self.assertEqual(len(parts), 3)
61 | for p in parts:
62 | # Check that it can be cast to an int
63 | i = int(p)
64 |
65 | @pytest.mark.docker
66 | def test_run_before_build(self):
67 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
68 | self.assertEqual(dig.run('true'), 1)
69 | self.assertEqual(dig.build(), 0)
70 | self.assertEqual(dig.run('true'), 0)
71 | dig.clear_image()
72 |
73 | @pytest.mark.docker
74 | def test_return_code_no_extensions(self):
75 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
76 | self.assertEqual(dig.build(), 0)
77 | self.assertEqual(dig.run('true'), 0)
78 | self.assertEqual(dig.run('false'), 1)
79 | dig.clear_image()
80 |
81 | @pytest.mark.docker
82 | def test_return_code_multiple_extensions(self):
83 | plugins = list_plugins()
84 | desired_plugins = ['home', 'user']
85 | active_extensions = [e() for e in plugins.values() if e.get_name() in desired_plugins]
86 | dig = DockerImageGenerator(active_extensions, {}, 'ubuntu:bionic')
87 | self.assertEqual(dig.build(), 0)
88 | self.assertEqual(dig.run('true'), 0)
89 | self.assertEqual(dig.run('false'), 1)
90 | dig.clear_image()
91 |
92 | @pytest.mark.docker
93 | def test_noexecute(self):
94 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
95 | self.assertEqual(dig.build(), 0)
96 | self.assertEqual(dig.run('true', noexecute=True), 0)
97 | dig.clear_image()
98 |
99 | @pytest.mark.docker
100 | def test_dry_run(self):
101 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
102 | self.assertEqual(dig.build(), 0)
103 | self.assertEqual(dig.run('true', mode='dry-run'), 0)
104 | self.assertEqual(dig.run('false', mode='dry-run'), 0)
105 | dig.clear_image()
106 |
107 | @pytest.mark.docker
108 | def test_non_interactive(self):
109 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
110 | self.assertEqual(dig.build(), 0)
111 | self.assertEqual(dig.run('true', mode='non-interactive'), 0)
112 | self.assertEqual(dig.run('false', mode='non-interactive'), 1)
113 | dig.clear_image()
114 |
115 | @pytest.mark.docker
116 | def test_device(self):
117 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
118 | self.assertEqual(dig.build(), 0)
119 | self.assertEqual(dig.run('true', devices=['/dev/random']), 0)
120 | self.assertEqual(dig.run('true', devices=['/dev/does_not_exist']), 0)
121 | dig.clear_image()
122 |
123 | @pytest.mark.docker
124 | def test_network(self):
125 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
126 | self.assertEqual(dig.build(), 0)
127 | networks = ['bridge', 'host', 'none']
128 | for n in networks:
129 | self.assertEqual(dig.run('true', network=n), 0)
130 | dig.clear_image()
131 |
132 | @pytest.mark.docker
133 | def test_extension_manager(self):
134 | parser = argparse.ArgumentParser()
135 | extension_manager = RockerExtensionManager()
136 | default_args = {}
137 | extension_manager.extend_cli_parser(parser, default_args)
138 | help_str = parser.format_help()
139 | self.assertIn('--mode', help_str)
140 | self.assertIn('dry-run', help_str)
141 | self.assertIn('non-interactive', help_str)
142 | self.assertIn('--extension-blacklist', help_str)
143 |
144 | self.assertRaises(ExtensionError,
145 | extension_manager.get_active_extensions,
146 | {'user': True, 'ssh': True, 'extension_blacklist': ['ssh']})
147 |
148 | def test_strict_required_extensions(self):
149 | class Foo(RockerExtension):
150 | @classmethod
151 | def get_name(cls):
152 | return 'foo'
153 |
154 | class Bar(RockerExtension):
155 | @classmethod
156 | def get_name(cls):
157 | return 'bar'
158 |
159 | def required(self, cli_args):
160 | return {'foo'}
161 |
162 | extension_manager = RockerExtensionManager()
163 | extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
164 |
165 | correct_extensions_args = {'strict_extension_selection': True, 'bar': True, 'foo': True, 'extension_blacklist': []}
166 | extension_manager.get_active_extensions(correct_extensions_args)
167 |
168 | incorrect_extensions_args = {'strict_extension_selection': True, 'bar': True, 'extension_blacklist': []}
169 | self.assertRaises(ExtensionError,
170 | extension_manager.get_active_extensions, incorrect_extensions_args)
171 |
172 | def test_implicit_required_extensions(self):
173 | class Foo(RockerExtension):
174 | @classmethod
175 | def get_name(cls):
176 | return 'foo'
177 |
178 | class Bar(RockerExtension):
179 | @classmethod
180 | def get_name(cls):
181 | return 'bar'
182 |
183 | def required(self, cli_args):
184 | return {'foo'}
185 |
186 | extension_manager = RockerExtensionManager()
187 | extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
188 |
189 | implicit_extensions_args = {'strict_extension_selection': False, 'bar': True, 'extension_blacklist': []}
190 | active_extensions = extension_manager.get_active_extensions(implicit_extensions_args)
191 | self.assertEqual(len(active_extensions), 2)
192 | # required extensions are not ordered, just check to make sure they are both present
193 | if active_extensions[0].get_name() == 'foo':
194 | self.assertEqual(active_extensions[1].get_name(), 'bar')
195 | else:
196 | self.assertEqual(active_extensions[0].get_name(), 'bar')
197 | self.assertEqual(active_extensions[1].get_name(), 'foo')
198 |
199 | def test_extension_sorting(self):
200 | class Foo(RockerExtension):
201 | @classmethod
202 | def get_name(cls):
203 | return 'foo'
204 |
205 | class Bar(RockerExtension):
206 | @classmethod
207 | def get_name(cls):
208 | return 'bar'
209 |
210 | def invoke_after(self, cli_args):
211 | return {'foo', 'absent_extension'}
212 |
213 | extension_manager = RockerExtensionManager()
214 | extension_manager.available_plugins = {'foo': Foo, 'bar': Bar}
215 |
216 | args = {'bar': True, 'foo': True, 'extension_blacklist': []}
217 | active_extensions = extension_manager.get_active_extensions(args)
218 | self.assertEqual(active_extensions[0].get_name(), 'foo')
219 | self.assertEqual(active_extensions[1].get_name(), 'bar')
220 |
221 | def test_docker_cmd_interactive(self):
222 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
223 |
224 | self.assertNotIn('-it', dig.generate_docker_cmd(mode=''))
225 | self.assertIn('-it', dig.generate_docker_cmd(mode='dry-run'))
226 |
227 | # TODO(tfoote) mock this appropriately
228 | # google actions tests don't have a tty, local tests do
229 | # import os, sys
230 | # if os.isatty(sys.__stdin__.fileno()):
231 | # self.assertIn('-it', dig.generate_docker_cmd(mode='interactive'))
232 | # else:
233 | self.assertIn('-it', dig.generate_docker_cmd(mode='interactive'))
234 |
235 | self.assertNotIn('-it', dig.generate_docker_cmd(mode='non-interactive'))
236 |
237 | def test_docker_user_detection(self):
238 | userinfo = pwd.getpwuid(os.getuid())
239 | username_detected = getattr(userinfo, 'pw_' + 'name')
240 | self.assertEqual(username_detected, get_user_name())
241 |
242 | @pytest.mark.docker
243 | def test_docker_user_setting(self):
244 | parser = argparse.ArgumentParser()
245 | extension_manager = RockerExtensionManager()
246 | default_args = {}
247 | extension_manager.extend_cli_parser(parser, default_args)
248 | active_extensions = extension_manager.get_active_extensions({'user': True, 'extension_blacklist': ['ssh']})
249 | dig = DockerImageGenerator(active_extensions, {'user_override_name': 'foo'}, 'ubuntu:bionic')
250 |
251 | self.assertIn('USER root', dig.dockerfile)
252 | self.assertNotIn('USER foo', dig.dockerfile)
253 | dig = DockerImageGenerator(active_extensions, {'user': True, 'user_override_name': 'foo'}, 'ubuntu:bionic')
254 | self.assertIn('USER root', dig.dockerfile)
255 | self.assertIn('USER foo', dig.dockerfile)
256 |
257 | def test_docker_user_snippet(self):
258 |
259 | root_snippet_content = "RUN echo run as root"
260 | user_snippet_content = "RUN echo run as user"
261 |
262 | class UserSnippet(RockerExtension):
263 | def __init__(self):
264 | self.name = 'usersnippet'
265 |
266 | @classmethod
267 | def get_name(cls):
268 | return 'usersnippet'
269 |
270 | def get_snippet(self, cli_args):
271 | return root_snippet_content
272 |
273 |
274 | def get_user_snippet(self, cli_args):
275 | return user_snippet_content
276 |
277 | extension_manager = RockerExtensionManager()
278 | extension_manager.available_plugins = {'usersnippet': UserSnippet}
279 | active_extensions = extension_manager.get_active_extensions({'user': True, 'usersnippet': UserSnippet, 'extension_blacklist': ['ssh']})
280 | self.assertTrue(active_extensions)
281 |
282 | # No user snippet
283 | mock_cli_args = {'user': False, 'usersnippet': True, 'user_override_name': 'foo'}
284 | dig = DockerImageGenerator(active_extensions, mock_cli_args, 'ubuntu:bionic')
285 | self.assertIn('USER root', dig.dockerfile)
286 | self.assertNotIn('USER foo', dig.dockerfile)
287 |
288 | # User snippet added
289 | mock_cli_args = {'user': True, 'usersnippet': True, 'user_override_name': 'foo'}
290 | dig = DockerImageGenerator(active_extensions, mock_cli_args, 'ubuntu:bionic')
291 | self.assertIn(root_snippet_content, dig.dockerfile)
292 | self.assertIn('USER foo', dig.dockerfile)
293 | self.assertIn(user_snippet_content, dig.dockerfile)
294 |
295 | def test_docker_cmd_nocleanup(self):
296 | dig = DockerImageGenerator([], {}, 'ubuntu:bionic')
297 |
298 | self.assertIn('--rm', dig.generate_docker_cmd())
299 | self.assertIn('--rm', dig.generate_docker_cmd(mode='dry-run'))
300 | self.assertIn('--rm', dig.generate_docker_cmd(nocleanup=''))
301 |
302 | self.assertNotIn('--rm', dig.generate_docker_cmd(nocleanup='true'))
303 |
--------------------------------------------------------------------------------
/test/test_extension.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import argparse
19 | import em
20 | import getpass
21 | import os
22 | import unittest
23 | from pathlib import Path
24 | import pwd
25 | import pytest
26 | from io import BytesIO as StringIO
27 |
28 |
29 | from rocker.core import DockerImageGenerator
30 | from rocker.core import docker_build
31 | from rocker.core import list_plugins
32 | from rocker.extensions import name_to_argument
33 |
34 |
35 | def plugin_load_parser_correctly(plugin):
36 | """A helper function to test that the plugins at least
37 | register an option for their own name."""
38 | parser = argparse.ArgumentParser(description='test_parser')
39 | plugin.register_arguments(parser, {})
40 | argument_name = name_to_argument(plugin.get_name())
41 | for action in parser._actions:
42 | option_strings = getattr(action, 'option_strings', [])
43 | if argument_name in option_strings:
44 | return True
45 | return False
46 |
47 |
48 | class ExtensionsTest(unittest.TestCase):
49 | def test_name_to_argument(self):
50 | self.assertEqual(name_to_argument('asdf'), '--asdf')
51 | self.assertEqual(name_to_argument('as_df'), '--as-df')
52 | self.assertEqual(name_to_argument('as-df'), '--as-df')
53 |
54 |
55 | class DetachExtensionTest(unittest.TestCase):
56 |
57 | def setUp(self):
58 | # Work around interference between empy Interpreter
59 | # stdout proxy and test runner. empy installs a proxy on stdout
60 | # to be able to capture the information.
61 | # And the test runner creates a new stdout object for each test.
62 | # This breaks empy as it assumes that the proxy has persistent
63 | # between instances of the Interpreter class
64 | # empy will error with the exception
65 | # "em.Error: interpreter stdout proxy lost"
66 | em.Interpreter._wasProxyInstalled = False
67 |
68 | def test_detach_extension(self):
69 | plugins = list_plugins()
70 | detach_plugin = plugins['detach']
71 | self.assertEqual(detach_plugin.get_name(), 'detach')
72 |
73 | p = detach_plugin()
74 | self.assertTrue(plugin_load_parser_correctly(detach_plugin))
75 |
76 | mock_cliargs = {'detach': True}
77 | args = p.get_docker_args(mock_cliargs)
78 | self.assertTrue('--detach' in args)
79 |
80 | mock_cliargs = {'detach': False}
81 | args = p.get_docker_args(mock_cliargs)
82 | self.assertTrue('--detach' not in args)
83 |
84 | mock_cliargs = {}
85 | args = p.get_docker_args(mock_cliargs)
86 | self.assertTrue('--detach' not in args)
87 |
88 |
89 | class DevicesExtensionTest(unittest.TestCase):
90 |
91 | def setUp(self):
92 | # Work around interference between empy Interpreter
93 | # stdout proxy and test runner. empy installs a proxy on stdout
94 | # to be able to capture the information.
95 | # And the test runner creates a new stdout object for each test.
96 | # This breaks empy as it assumes that the proxy has persistent
97 | # between instances of the Interpreter class
98 | # empy will error with the exception
99 | # "em.Error: interpreter stdout proxy lost"
100 | em.Interpreter._wasProxyInstalled = False
101 |
102 | def test_devices_extension(self):
103 | plugins = list_plugins()
104 | devices_plugin = plugins['devices']
105 | self.assertEqual(devices_plugin.get_name(), 'devices')
106 |
107 | p = devices_plugin()
108 | self.assertTrue(plugin_load_parser_correctly(devices_plugin))
109 |
110 | mock_cliargs = {'devices': ['/dev/random']}
111 | self.assertEqual(p.get_snippet(mock_cliargs), '')
112 | self.assertEqual(p.get_preamble(mock_cliargs), '')
113 | args = p.get_docker_args(mock_cliargs)
114 | self.assertTrue('--device /dev/random' in args)
115 |
116 | # Check case for invalid device
117 | mock_cliargs = {'devices': ['/dev/does_not_exist']}
118 | self.assertEqual(p.get_snippet(mock_cliargs), '')
119 | self.assertEqual(p.get_preamble(mock_cliargs), '')
120 | args = p.get_docker_args(mock_cliargs)
121 | self.assertFalse('--device' in args)
122 |
123 |
124 | class HomeExtensionTest(unittest.TestCase):
125 |
126 | def setUp(self):
127 | # Work around interference between empy Interpreter
128 | # stdout proxy and test runner. empy installs a proxy on stdout
129 | # to be able to capture the information.
130 | # And the test runner creates a new stdout object for each test.
131 | # This breaks empy as it assumes that the proxy has persistent
132 | # between instances of the Interpreter class
133 | # empy will error with the exception
134 | # "em.Error: interpreter stdout proxy lost"
135 | em.Interpreter._wasProxyInstalled = False
136 |
137 | def test_home_extension(self):
138 | plugins = list_plugins()
139 | home_plugin = plugins['home']
140 | self.assertEqual(home_plugin.get_name(), 'home')
141 |
142 | p = home_plugin()
143 | self.assertTrue(plugin_load_parser_correctly(home_plugin))
144 |
145 | mock_cliargs = {}
146 | self.assertEqual(p.get_snippet(mock_cliargs), '')
147 | self.assertEqual(p.get_preamble(mock_cliargs), '')
148 | args = p.get_docker_args(mock_cliargs)
149 | self.assertTrue('-v %s:%s' % (Path.home(), Path.home()) in args)
150 |
151 |
152 | class IpcExtensionTest(unittest.TestCase):
153 |
154 | def setUp(self):
155 | # Work around interference between empy Interpreter
156 | # stdout proxy and test runner. empy installs a proxy on stdout
157 | # to be able to capture the information.
158 | # And the test runner creates a new stdout object for each test.
159 | # This breaks empy as it assumes that the proxy has persistent
160 | # between instances of the Interpreter class
161 | # empy will error with the exception
162 | # "em.Error: interpreter stdout proxy lost"
163 | em.Interpreter._wasProxyInstalled = False
164 |
165 | @pytest.mark.docker
166 | def test_ipc_extension(self):
167 | plugins = list_plugins()
168 | ipc_plugin = plugins['ipc']
169 | self.assertEqual(ipc_plugin.get_name(), 'ipc')
170 |
171 | p = ipc_plugin()
172 | self.assertTrue(plugin_load_parser_correctly(ipc_plugin))
173 |
174 | mock_cliargs = {'ipc': 'none'}
175 | self.assertEqual(p.get_snippet(mock_cliargs), '')
176 | self.assertEqual(p.get_preamble(mock_cliargs), '')
177 | args = p.get_docker_args(mock_cliargs)
178 | self.assertTrue('--ipc none' in args)
179 |
180 | mock_cliargs = {'ipc': 'host'}
181 | args = p.get_docker_args(mock_cliargs)
182 | self.assertTrue('--ipc host' in args)
183 |
184 |
185 | class NetworkExtensionTest(unittest.TestCase):
186 |
187 | def setUp(self):
188 | # Work around interference between empy Interpreter
189 | # stdout proxy and test runner. empy installs a proxy on stdout
190 | # to be able to capture the information.
191 | # And the test runner creates a new stdout object for each test.
192 | # This breaks empy as it assumes that the proxy has persistent
193 | # between instances of the Interpreter class
194 | # empy will error with the exception
195 | # "em.Error: interpreter stdout proxy lost"
196 | em.Interpreter._wasProxyInstalled = False
197 |
198 | @pytest.mark.docker
199 | def test_network_extension(self):
200 | plugins = list_plugins()
201 | network_plugin = plugins['network']
202 | self.assertEqual(network_plugin.get_name(), 'network')
203 |
204 | p = network_plugin()
205 | self.assertTrue(plugin_load_parser_correctly(network_plugin))
206 |
207 | mock_cliargs = {'network': 'none'}
208 | self.assertEqual(p.get_snippet(mock_cliargs), '')
209 | self.assertEqual(p.get_preamble(mock_cliargs), '')
210 | args = p.get_docker_args(mock_cliargs)
211 | self.assertTrue('--network none' in args)
212 |
213 | mock_cliargs = {'network': 'host'}
214 | args = p.get_docker_args(mock_cliargs)
215 | self.assertTrue('--network host' in args)
216 |
217 | class ExposeExtensionTest(unittest.TestCase):
218 |
219 | def setUp(self):
220 | # Work around interference between empy Interpreter
221 | # stdout proxy and test runner. empy installs a proxy on stdout
222 | # to be able to capture the information.
223 | # And the test runner creates a new stdout object for each test.
224 | # This breaks empy as it assumes that the proxy has persistent
225 | # between instances of the Interpreter class
226 | # empy will error with the exception
227 | # "em.Error: interpreter stdout proxy lost"
228 | em.Interpreter._wasProxyInstalled = False
229 |
230 | @pytest.mark.docker
231 | def test_expose_extension(self):
232 | plugins = list_plugins()
233 | expose_plugin = plugins['expose']
234 | self.assertEqual(expose_plugin.get_name(), 'expose')
235 |
236 | p = expose_plugin()
237 | self.assertTrue(plugin_load_parser_correctly(expose_plugin))
238 |
239 | mock_cliargs = {}
240 | self.assertEqual(p.get_snippet(mock_cliargs), '')
241 | self.assertEqual(p.get_preamble(mock_cliargs), '')
242 | args = p.get_docker_args(mock_cliargs)
243 | self.assertNotIn('--expose', args)
244 |
245 | mock_cliargs = {'expose': ['80', '8080']}
246 | args = p.get_docker_args(mock_cliargs)
247 | self.assertIn('--expose 80', args)
248 | self.assertIn('--expose 8080', args)
249 |
250 | class PortExtensionTest(unittest.TestCase):
251 |
252 | def setUp(self):
253 | # Work around interference between empy Interpreter
254 | # stdout proxy and test runner. empy installs a proxy on stdout
255 | # to be able to capture the information.
256 | # And the test runner creates a new stdout object for each test.
257 | # This breaks empy as it assumes that the proxy has persistent
258 | # between instances of the Interpreter class
259 | # empy will error with the exception
260 | # "em.Error: interpreter stdout proxy lost"
261 | em.Interpreter._wasProxyInstalled = False
262 |
263 | @pytest.mark.docker
264 | def test_port_extension(self):
265 | plugins = list_plugins()
266 | port_plugin = plugins['port']
267 | self.assertEqual(port_plugin.get_name(), 'port')
268 |
269 | p = port_plugin()
270 | self.assertTrue(plugin_load_parser_correctly(port_plugin))
271 |
272 | mock_cliargs = {}
273 | self.assertEqual(p.get_snippet(mock_cliargs), '')
274 | self.assertEqual(p.get_preamble(mock_cliargs), '')
275 | args = p.get_docker_args(mock_cliargs)
276 | self.assertNotIn('-p', args)
277 |
278 | mock_cliargs = {'port': ['80:8080', '81:8081']}
279 | args = p.get_docker_args(mock_cliargs)
280 | self.assertIn('-p 80:8080', args)
281 | self.assertIn('-p 81:8081', args)
282 |
283 | class NameExtensionTest(unittest.TestCase):
284 |
285 | def setUp(self):
286 | # Work around interference between empy Interpreter
287 | # stdout proxy and test runner. empy installs a proxy on stdout
288 | # to be able to capture the information.
289 | # And the test runner creates a new stdout object for each test.
290 | # This breaks empy as it assumes that the proxy has persistent
291 | # between instances of the Interpreter class
292 | # empy will error with the exception
293 | # "em.Error: interpreter stdout proxy lost"
294 | em.Interpreter._wasProxyInstalled = False
295 |
296 | def test_name_extension(self):
297 | plugins = list_plugins()
298 | name_plugin = plugins['name']
299 | self.assertEqual(name_plugin.get_name(), 'name')
300 |
301 | p = name_plugin()
302 | self.assertTrue(plugin_load_parser_correctly(name_plugin))
303 |
304 | mock_cliargs = {'name': 'none'}
305 | self.assertEqual(p.get_snippet(mock_cliargs), '')
306 | self.assertEqual(p.get_preamble(mock_cliargs), '')
307 | args = p.get_docker_args(mock_cliargs)
308 | self.assertTrue('--name none' in args)
309 |
310 | mock_cliargs = {'name': 'docker_name'}
311 | args = p.get_docker_args(mock_cliargs)
312 | self.assertTrue('--name docker_name' in args)
313 |
314 | class HostnameExtensionTest(unittest.TestCase):
315 |
316 | def setUp(self):
317 | # Work around interference between empy Interpreter
318 | # stdout proxy and test runner. empy installs a proxy on stdout
319 | # to be able to capture the information.
320 | # And the test runner creates a new stdout object for each test.
321 | # This breaks empy as it assumes that the proxy has persistent
322 | # between instances of the Interpreter class
323 | # empy will error with the exception
324 | # "em.Error: interpreter stdout proxy lost"
325 | em.Interpreter._wasProxyInstalled = False
326 |
327 | def test_name_extension(self):
328 | plugins = list_plugins()
329 | name_plugin = plugins['hostname']
330 | self.assertEqual(name_plugin.get_name(), 'hostname')
331 |
332 | p = name_plugin()
333 | self.assertTrue(plugin_load_parser_correctly(name_plugin))
334 |
335 | mock_cliargs = {'hostname': 'none'}
336 | self.assertEqual(p.get_snippet(mock_cliargs), '')
337 | self.assertEqual(p.get_preamble(mock_cliargs), '')
338 | args = p.get_docker_args(mock_cliargs)
339 | self.assertTrue('--hostname none' in args)
340 |
341 | mock_cliargs = {'hostname': 'docker-hostname'}
342 | args = p.get_docker_args(mock_cliargs)
343 | self.assertTrue('--hostname docker-hostname' in args)
344 |
345 |
346 | class PrivilegedExtensionTest(unittest.TestCase):
347 |
348 | def setUp(self):
349 | # Work around interference between empy Interpreter
350 | # stdout proxy and test runner. empy installs a proxy on stdout
351 | # to be able to capture the information.
352 | # And the test runner creates a new stdout object for each test.
353 | # This breaks empy as it assumes that the proxy has persistent
354 | # between instances of the Interpreter class
355 | # empy will error with the exception
356 | # "em.Error: interpreter stdout proxy lost"
357 | em.Interpreter._wasProxyInstalled = False
358 |
359 | def test_privileged_extension(self):
360 | plugins = list_plugins()
361 | print(plugins)
362 | privileged_plugin = plugins['privileged']
363 | self.assertEqual(privileged_plugin.get_name(), 'privileged')
364 |
365 | p = privileged_plugin()
366 | self.assertTrue(plugin_load_parser_correctly(privileged_plugin))
367 |
368 | mock_cliargs = {'privileged': True}
369 | self.assertEqual(p.get_snippet(mock_cliargs), '')
370 | self.assertEqual(p.get_preamble(mock_cliargs), '')
371 | args = p.get_docker_args(mock_cliargs)
372 | self.assertTrue('--privileged' in args)
373 |
374 |
375 | class UserExtensionTest(unittest.TestCase):
376 |
377 | def setUp(self):
378 | # Work around interference between empy Interpreter
379 | # stdout proxy and test runner. empy installs a proxy on stdout
380 | # to be able to capture the information.
381 | # And the test runner creates a new stdout object for each test.
382 | # This breaks empy as it assumes that the proxy has persistent
383 | # between instances of the Interpreter class
384 | # empy will error with the exception
385 | # "em.Error: interpreter stdout proxy lost"
386 | em.Interpreter._wasProxyInstalled = False
387 |
388 | def test_user_extension(self):
389 | plugins = list_plugins()
390 | user_plugin = plugins['user']
391 | self.assertEqual(user_plugin.get_name(), 'user')
392 |
393 | p = user_plugin()
394 | self.assertTrue(plugin_load_parser_correctly(user_plugin))
395 |
396 | env_subs = p.get_environment_subs()
397 | self.assertEqual(env_subs['gid'], os.getgid())
398 | self.assertEqual(env_subs['uid'], os.getuid())
399 | self.assertEqual(env_subs['name'], getpass.getuser())
400 | self.assertEqual(env_subs['dir'], str(Path.home()))
401 | self.assertEqual(env_subs['gecos'], pwd.getpwuid(os.getuid()).pw_gecos)
402 | self.assertEqual(env_subs['shell'], pwd.getpwuid(os.getuid()).pw_shell)
403 |
404 | mock_cliargs = {}
405 | snippet = p.get_snippet(mock_cliargs).splitlines()
406 |
407 | uid_line = [l for l in snippet if '--uid' in l][0]
408 | self.assertTrue(str(os.getuid()) in uid_line)
409 |
410 | self.assertEqual(p.get_preamble(mock_cliargs), '')
411 | self.assertEqual(p.get_docker_args(mock_cliargs), '')
412 |
413 | self.assertTrue('mkhomedir_helper' in p.get_snippet(mock_cliargs))
414 | home_active_cliargs = mock_cliargs
415 | home_active_cliargs['home'] = True
416 | self.assertFalse('mkhomedir_helper' in p.get_snippet(home_active_cliargs))
417 |
418 | user_override_active_cliargs = mock_cliargs
419 | user_override_active_cliargs['user_preserve_groups'] = []
420 | snippet_result = p.get_snippet(user_override_active_cliargs)
421 | self.assertTrue('usermod -aG' in snippet_result)
422 |
423 | user_override_active_cliargs = mock_cliargs
424 | user_override_active_cliargs['user_preserve_groups'] = ['cdrom', 'audio']
425 | snippet_result = p.get_snippet(user_override_active_cliargs)
426 | self.assertTrue('cdrom' in snippet_result)
427 | self.assertTrue('audio' in snippet_result)
428 |
429 | user_override_active_cliargs = mock_cliargs
430 | user_override_active_cliargs['user_preserve_groups'] = []
431 | user_override_active_cliargs['user_preserve_groups_permissive'] = True
432 | snippet_result = p.get_snippet(user_override_active_cliargs)
433 | self.assertTrue('usermod -aG' in snippet_result)
434 | self.assertTrue('user-preserve-group-permissive Enabled' in snippet_result)
435 |
436 | user_override_active_cliargs['user_override_name'] = 'testusername'
437 | snippet_result = p.get_snippet(user_override_active_cliargs)
438 | self.assertTrue('WORKDIR /home/testusername' in snippet_result)
439 | self.assertTrue('userdel -r' in snippet_result)
440 |
441 | user_override_active_cliargs['user_preserve_home'] = True
442 | snippet_result = p.get_snippet(user_override_active_cliargs)
443 | self.assertFalse('userdel -r' in snippet_result)
444 |
445 | snippet_result = p.get_snippet(user_override_active_cliargs)
446 | self.assertTrue(('-s ' + pwd.getpwuid(os.getuid()).pw_shell) in snippet_result)
447 |
448 | user_override_active_cliargs['user_override_shell'] = 'testshell'
449 | snippet_result = p.get_snippet(user_override_active_cliargs)
450 | self.assertTrue('-s testshell' in snippet_result)
451 |
452 | user_override_active_cliargs['user_override_shell'] = ''
453 | snippet_result = p.get_snippet(user_override_active_cliargs)
454 | self.assertFalse('-s' in snippet_result)
455 |
456 | @pytest.mark.docker
457 | def test_user_collisions(self):
458 | plugins = list_plugins()
459 | user_plugin = plugins['user']
460 | self.assertEqual(user_plugin.get_name(), 'user')
461 |
462 | uid = os.getuid()+1
463 | COLLIDING_UID_DOCKERFILE = f"""FROM ubuntu:jammy
464 | RUN useradd test -u{uid}
465 |
466 | """
467 | iof = StringIO(COLLIDING_UID_DOCKERFILE.encode())
468 | image_id = docker_build(
469 | fileobj=iof,
470 | #output_callback=output_callback,
471 | nocache=True,
472 | forcerm=True,
473 | tag="rocker:" + f"user_extension_test_uid_collision"
474 | )
475 | print(f'Image id is {image_id}')
476 | self.assertTrue(image_id, f"Image failed to build >>>{COLLIDING_UID_DOCKERFILE}<<<")
477 |
478 | # Test Colliding UID but not name
479 | build_args = {
480 | 'user': True,
481 | 'user_override_name': 'test2',
482 | 'user_preserve_home': True,
483 | # 'command': 'ls -l && touch /home/test2/home_directory_access_verification',
484 | 'command': 'touch /home/test2/testwrite',
485 | }
486 | dig = DockerImageGenerator([user_plugin()], build_args, image_id)
487 | exit_code = dig.build(**build_args)
488 | self.assertTrue(exit_code == 0, f"Build failed with exit code {exit_code}")
489 | run_exit_code = dig.run(**build_args)
490 | self.assertTrue(run_exit_code == 0, f"Run failed with exit code {run_exit_code}")
491 | dig.clear_image()
492 |
493 |
494 | # Test colliding UID and name
495 | build_args['user_override_name'] = 'test'
496 | build_args['command'] = 'touch /home/test/testwrite'
497 | dig = DockerImageGenerator([user_plugin()], build_args, image_id)
498 | exit_code = dig.build(**build_args)
499 | self.assertTrue(exit_code == 0, f"Build failed with exit code {exit_code}")
500 | run_exit_code = dig.run(**build_args)
501 | self.assertTrue(run_exit_code == 0, f"Run failed with exit code {run_exit_code}")
502 | dig.clear_image()
503 |
504 |
505 | class PulseExtensionTest(unittest.TestCase):
506 |
507 | def setUp(self):
508 | # Work around interference between empy Interpreter
509 | # stdout proxy and test runner. empy installs a proxy on stdout
510 | # to be able to capture the information.
511 | # And the test runner creates a new stdout object for each test.
512 | # This breaks empy as it assumes that the proxy has persistent
513 | # between instances of the Interpreter class
514 | # empy will error with the exception
515 | # "em.Error: interpreter stdout proxy lost"
516 | em.Interpreter._wasProxyInstalled = False
517 |
518 | def test_pulse_extension(self):
519 | plugins = list_plugins()
520 | pulse_plugin = plugins['pulse']
521 | self.assertEqual(pulse_plugin.get_name(), 'pulse')
522 |
523 | p = pulse_plugin()
524 | self.assertTrue(plugin_load_parser_correctly(pulse_plugin))
525 |
526 | mock_cliargs = {}
527 | snippet = p.get_snippet(mock_cliargs)
528 | #first line
529 | self.assertIn('RUN mkdir -p /etc/pulse', snippet)
530 | self.assertIn('default-server = unix:/run/user/', snippet) #skipping user id that's system dependent
531 | self.assertIn('autospawn = no', snippet)
532 | self.assertIn('daemon-binary = /bin/true', snippet)
533 | #last line
534 | self.assertIn('> /etc/pulse/client.conf', snippet)
535 | self.assertEqual(p.get_preamble(mock_cliargs), '')
536 | docker_args = p.get_docker_args(mock_cliargs)
537 | self.assertIn('-v /run/user/', docker_args)
538 | self.assertIn('/pulse:/run/user/', docker_args)
539 | self.assertIn('/pulse --device /dev/snd ', docker_args)
540 | self.assertIn(' -e PULSE_SERVER=unix', docker_args)
541 | self.assertIn('/pulse/native -v', docker_args)
542 | self.assertIn('/pulse/native:', docker_args)
543 | self.assertIn('/pulse/native --group-add', docker_args)
544 |
545 | EXPECTED_DEV_HELPERS_SNIPPET = """# workspace development helpers
546 | RUN apt-get update \\
547 | && apt-get install -y \\
548 | byobu \\
549 | emacs \\
550 | && apt-get clean
551 | """
552 |
553 | class DevHelpersExtensionTest(unittest.TestCase):
554 |
555 | def setUp(self):
556 | # Work around interference between empy Interpreter
557 | # stdout proxy and test runner. empy installs a proxy on stdout
558 | # to be able to capture the information.
559 | # And the test runner creates a new stdout object for each test.
560 | # This breaks empy as it assumes that the proxy has persistent
561 | # between instances of the Interpreter class
562 | # empy will error with the exception
563 | # "em.Error: interpreter stdout proxy lost"
564 | em.Interpreter._wasProxyInstalled = False
565 |
566 | def test_pulse_extension(self):
567 | plugins = list_plugins()
568 | dev_helper_plugin = plugins['dev_helpers']
569 | self.assertEqual(dev_helper_plugin.get_name(), 'dev_helpers')
570 |
571 | p = dev_helper_plugin()
572 | self.assertTrue(plugin_load_parser_correctly(dev_helper_plugin))
573 |
574 | mock_cliargs = {}
575 |
576 | self.assertEqual(p.get_snippet(mock_cliargs), EXPECTED_DEV_HELPERS_SNIPPET)
577 | self.assertEqual(p.get_preamble(mock_cliargs), '')
578 |
579 |
580 | class EnvExtensionTest(unittest.TestCase):
581 |
582 | def setUp(self):
583 | # Work around interference between empy Interpreter
584 | # stdout proxy and test runner. empy installs a proxy on stdout
585 | # to be able to capture the information.
586 | # And the test runner creates a new stdout object for each test.
587 | # This breaks empy as it assumes that the proxy has persistent
588 | # between instances of the Interpreter class
589 | # empy will error with the exception
590 | # "em.Error: interpreter stdout proxy lost"
591 | em.Interpreter._wasProxyInstalled = False
592 |
593 | def test_env_extension(self):
594 | plugins = list_plugins()
595 | env_plugin = plugins['env']
596 | self.assertEqual(env_plugin.get_name(), 'env')
597 |
598 | p = env_plugin()
599 | self.assertTrue(plugin_load_parser_correctly(env_plugin))
600 |
601 | mock_cliargs = {'env': [['ENVVARNAME=envvar_value', 'ENV2=val2'], ['ENV3=val3']]}
602 |
603 | self.assertEqual(p.get_snippet(mock_cliargs), '')
604 | self.assertEqual(p.get_preamble(mock_cliargs), '')
605 | self.assertEqual(p.get_docker_args(mock_cliargs), ' -e ENVVARNAME=envvar_value -e ENV2=val2 -e ENV3=val3')
606 |
607 | def test_env_file_extension(self):
608 | plugins = list_plugins()
609 | env_plugin = plugins['env']
610 | self.assertEqual(env_plugin.get_name(), 'env')
611 |
612 | p = env_plugin()
613 | self.assertTrue(plugin_load_parser_correctly(env_plugin))
614 |
615 | mock_cliargs = {'env_file': [['foo'], ['bar']]}
616 |
617 | self.assertEqual(p.get_snippet(mock_cliargs), '')
618 | self.assertEqual(p.get_preamble(mock_cliargs), '')
619 | self.assertEqual(p.get_docker_args(mock_cliargs), ' --env-file foo --env-file bar')
620 |
621 |
622 | class GroupAddExtensionTest(unittest.TestCase):
623 |
624 | def setUp(self):
625 | # Work around interference between empy Interpreter
626 | # stdout proxy and test runner. empy installs a proxy on stdout
627 | # to be able to capture the information.
628 | # And the test runner creates a new stdout object for each test.
629 | # This breaks empy as it assumes that the proxy has persistent
630 | # between instances of the Interpreter class
631 | # empy will error with the exception
632 | # "em.Error: interpreter stdout proxy lost"
633 | em.Interpreter._wasProxyInstalled = False
634 |
635 | @pytest.mark.docker
636 | def test_group_add_extension(self):
637 | plugins = list_plugins()
638 | group_add_plugin = plugins['group_add']
639 | self.assertEqual(group_add_plugin.get_name(), 'group_add')
640 |
641 | p = group_add_plugin()
642 | self.assertTrue(plugin_load_parser_correctly(group_add_plugin))
643 |
644 | mock_cliargs = {}
645 | self.assertEqual(p.get_snippet(mock_cliargs), '')
646 | self.assertEqual(p.get_preamble(mock_cliargs), '')
647 | args = p.get_docker_args(mock_cliargs)
648 | self.assertNotIn('--group_add', args)
649 |
650 | mock_cliargs = {'group_add': ['sudo', 'docker']}
651 | args = p.get_docker_args(mock_cliargs)
652 | self.assertIn('--group-add sudo', args)
653 | self.assertIn('--group-add docker', args)
654 |
655 | class ShmSizeExtensionTest(unittest.TestCase):
656 |
657 | def setUp(self):
658 | # Work around interference between empy Interpreter
659 | # stdout proxy and test runner. empy installs a proxy on stdout
660 | # to be able to capture the information.
661 | # And the test runner creates a new stdout object for each test.
662 | # This breaks empy as it assumes that the proxy has persistent
663 | # between instances of the Interpreter class
664 | # empy will error with the exception
665 | # "em.Error: interpreter stdout proxy lost"
666 | em.Interpreter._wasProxyInstalled = False
667 |
668 | @pytest.mark.docker
669 | def test_shm_size_extension(self):
670 | plugins = list_plugins()
671 | shm_size_plugin = plugins['shm_size']
672 | self.assertEqual(shm_size_plugin.get_name(), 'shm_size')
673 |
674 | p = shm_size_plugin()
675 | self.assertTrue(plugin_load_parser_correctly(shm_size_plugin))
676 |
677 | mock_cliargs = {}
678 | self.assertEqual(p.get_snippet(mock_cliargs), '')
679 | self.assertEqual(p.get_preamble(mock_cliargs), '')
680 | args = p.get_docker_args(mock_cliargs)
681 | self.assertNotIn('--shm-size', args)
682 |
683 | mock_cliargs = {'shm_size': '12g'}
684 | args = p.get_docker_args(mock_cliargs)
685 | self.assertIn('--shm-size 12g', args)
--------------------------------------------------------------------------------
/test/test_file_writing.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import argparse
19 | import em
20 | import getpass
21 | import os
22 | import unittest
23 | from pathlib import Path
24 | import pwd
25 | import shlex
26 | from tempfile import TemporaryDirectory
27 |
28 | from rocker.core import list_plugins
29 | from rocker.core import write_files
30 | from rocker.extensions import name_to_argument
31 | from rocker.extensions import RockerExtension
32 |
33 | from test_extension import plugin_load_parser_correctly
34 |
35 | class ExtensionsTest(unittest.TestCase):
36 | def test_name_to_argument(self):
37 | self.assertEqual(name_to_argument('asdf'), '--asdf')
38 | self.assertEqual(name_to_argument('as_df'), '--as-df')
39 | self.assertEqual(name_to_argument('as-df'), '--as-df')
40 |
41 | class TestFileInjection(RockerExtension):
42 |
43 | name = 'test_file_injection'
44 |
45 | @classmethod
46 | def get_name(cls):
47 | return cls.name
48 |
49 | def get_files(self, cliargs):
50 | all_files = {}
51 | all_files['test_file.txt'] = """The quick brown fox jumped over the lazy dog. %s""" % cliargs
52 | all_files['path/to/test_file.txt'] = """The quick brown fox jumped over the lazy dog. %s""" % cliargs
53 | all_files['test_file.bin'] = bytes("""The quick brown fox jumped over the lazy dog. %s""" % cliargs, 'utf-8')
54 | all_files['../outside/path/to/test_file.txt'] = """Path outside directory should be skipped"""
55 | all_files['/absolute.txt'] = """Absolute file path should be skipped"""
56 | return all_files
57 |
58 |
59 |
60 | @staticmethod
61 | def register_arguments(parser, defaults):
62 | parser.add_argument('--test-file-injection',
63 | action='store_true',
64 | default=defaults.get('test_file_injection', False),
65 | help="Enable test_file_injection extension")
66 |
67 |
68 | class FileInjectionExtensionTest(unittest.TestCase):
69 |
70 | def setUp(self):
71 | # Work around interference between empy Interpreter
72 | # stdout proxy and test runner. empy installs a proxy on stdout
73 | # to be able to capture the information.
74 | # And the test runner creates a new stdout object for each test.
75 | # This breaks empy as it assumes that the proxy has persistent
76 | # between instances of the Interpreter class
77 | # empy will error with the exception
78 | # "em.Error: interpreter stdout proxy lost"
79 | em.Interpreter._wasProxyInstalled = False
80 |
81 | def test_file_injection(self):
82 | extensions = [TestFileInjection()]
83 | mock_cliargs = {'test_key': 'test_value'}
84 |
85 | with TemporaryDirectory() as td:
86 | write_files(extensions, mock_cliargs, td)
87 |
88 | with open(os.path.join(td, 'test_file.txt'), 'r') as fh:
89 | content = fh.read()
90 | self.assertIn('quick brown', content)
91 | self.assertIn('test_key', content)
92 | self.assertIn('test_value', content)
93 |
94 | with open(os.path.join(td, 'path/to/test_file.txt'), 'r') as fh:
95 | content = fh.read()
96 | self.assertIn('quick brown', content)
97 | self.assertIn('test_key', content)
98 | self.assertIn('test_value', content)
99 |
100 | with open(os.path.join(td, 'test_file.bin'), 'r') as fh: # this particular binary file can be read in text mode
101 | content = fh.read()
102 | self.assertIn('quick brown', content)
103 | self.assertIn('test_key', content)
104 | self.assertIn('test_value', content)
105 |
106 | self.assertFalse(os.path.exists('../outside/path/to/test_file.txt'))
107 | self.assertFalse(os.path.exists('/absolute.txt'))
108 |
--------------------------------------------------------------------------------
/test/test_git_extension.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import argparse
19 | import em
20 | import getpass
21 | import os
22 | import unittest
23 | from pathlib import Path
24 | import pwd
25 | import tempfile
26 |
27 |
28 | from rocker.core import list_plugins
29 | from rocker.extensions import name_to_argument
30 |
31 | from test_extension import plugin_load_parser_correctly
32 |
33 | class ExtensionsTest(unittest.TestCase):
34 | def test_name_to_argument(self):
35 | self.assertEqual(name_to_argument('asdf'), '--asdf')
36 | self.assertEqual(name_to_argument('as_df'), '--as-df')
37 | self.assertEqual(name_to_argument('as-df'), '--as-df')
38 |
39 |
40 | class GitExtensionTest(unittest.TestCase):
41 |
42 | def setUp(self):
43 | # Work around interference between empy Interpreter
44 | # stdout proxy and test runner. empy installs a proxy on stdout
45 | # to be able to capture the information.
46 | # And the test runner creates a new stdout object for each test.
47 | # This breaks empy as it assumes that the proxy has persistent
48 | # between instances of the Interpreter class
49 | # empy will error with the exception
50 | # "em.Error: interpreter stdout proxy lost"
51 | em.Interpreter._wasProxyInstalled = False
52 |
53 | def test_git_extension(self):
54 | plugins = list_plugins()
55 | git_plugin = plugins['git']
56 | self.assertEqual(git_plugin.get_name(), 'git')
57 |
58 | p = git_plugin()
59 | self.assertTrue(plugin_load_parser_correctly(git_plugin))
60 |
61 |
62 | mock_cliargs = {}
63 | mock_config_file = tempfile.NamedTemporaryFile()
64 | mock_system_config_file = tempfile.NamedTemporaryFile()
65 | mock_cliargs['git_config_path'] = mock_config_file.name
66 | mock_cliargs['git_config_path_system'] = mock_system_config_file.name
67 | args = p.get_docker_args(mock_cliargs)
68 | system_gitconfig = mock_system_config_file.name
69 | system_gitconfig_target = '/etc/gitconfig'
70 | user_gitconfig = mock_config_file.name
71 | user_gitconfig_target = '/root/.gitconfig'
72 | self.assertIn('-v %s:%s' % (system_gitconfig, system_gitconfig_target), args)
73 | self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), args)
74 |
75 | # Test with user "enabled"
76 | mock_cliargs = {'user': True}
77 | mock_cliargs['git_config_path'] = mock_config_file.name
78 | user_args = p.get_docker_args(mock_cliargs)
79 | user_gitconfig_target = os.path.expanduser('~/.gitconfig')
80 | self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), user_args)
81 |
82 | # Test with an existing overridden user key, but with None value
83 | mock_cliargs['user_override_name'] = None
84 | user_args = p.get_docker_args(mock_cliargs)
85 | user_gitconfig_target = os.path.expanduser('~/.gitconfig')
86 | self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), user_args)
87 |
88 | # Test with overridden user
89 | mock_cliargs['user_override_name'] = 'testusername'
90 | user_args = p.get_docker_args(mock_cliargs)
91 | user_gitconfig_target = '/home/testusername/.gitconfig'
92 | self.assertIn('-v %s:%s' % (user_gitconfig, user_gitconfig_target), user_args)
93 |
94 | # Test non-extant files no generation
95 | mock_cliargs['git_config_path'] = '/path-does-not-exist'
96 | mock_cliargs['git_config_path_system'] = '/path-does-not-exist-either'
97 | user_args = p.get_docker_args(mock_cliargs)
98 | self.assertNotIn('-v', user_args)
99 |
--------------------------------------------------------------------------------
/test/test_nvidia.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import docker
19 | import em
20 | import unittest
21 | import pexpect
22 | import pytest
23 |
24 |
25 | from io import BytesIO as StringIO
26 | from packaging.version import Version
27 |
28 | from rocker.core import DockerImageGenerator
29 | from rocker.core import list_plugins
30 | from rocker.core import get_docker_client
31 | from rocker.nvidia_extension import get_docker_version
32 | from test_extension import plugin_load_parser_correctly
33 |
34 |
35 | @pytest.mark.docker
36 | class X11Test(unittest.TestCase):
37 | @classmethod
38 | def setUpClass(self):
39 | client = get_docker_client()
40 | self.dockerfile_tags = []
41 | for distro, distro_version in [('ubuntu', 'xenial'), ('ubuntu', 'bionic'), ('ubuntu', 'focal'), ('debian', 'buster')]:
42 | dockerfile = """
43 | FROM %(distro)s:%(distro_version)s
44 |
45 | RUN apt-get update && apt-get install x11-utils -y && apt-get clean
46 |
47 | CMD xdpyinfo
48 | """
49 | dockerfile_tag = 'testfixture_%s_x11_validate' % distro_version
50 | iof = StringIO((dockerfile % locals()).encode())
51 | im = client.build(fileobj = iof, tag=dockerfile_tag)
52 | for e in im:
53 | pass
54 | #print(e)
55 | self.dockerfile_tags.append(dockerfile_tag)
56 |
57 | def setUp(self):
58 | # Work around interference between empy Interpreter
59 | # stdout proxy and test runner. empy installs a proxy on stdout
60 | # to be able to capture the information.
61 | # And the test runner creates a new stdout object for each test.
62 | # This breaks empy as it assumes that the proxy has persistent
63 | # between instances of the Interpreter class
64 | # empy will error with the exception
65 | # "em.Error: interpreter stdout proxy lost"
66 | em.Interpreter._wasProxyInstalled = False
67 |
68 | def test_x11_extension_basic(self):
69 | plugins = list_plugins()
70 | x11_plugin = plugins['x11']
71 | self.assertEqual(x11_plugin.get_name(), 'x11')
72 | self.assertTrue(plugin_load_parser_correctly(x11_plugin))
73 |
74 | p = x11_plugin()
75 | mock_cliargs = {'base_image': 'ubuntu:xenial'}
76 |
77 | # Must be called before get_docker_args
78 | docker_args = p.precondition_environment(mock_cliargs)
79 |
80 | docker_args = p.get_docker_args(mock_cliargs)
81 | self.assertIn(' -e DISPLAY -e TERM', docker_args)
82 | self.assertIn(' -e QT_X11_NO_MITSHM=1', docker_args)
83 | self.assertIn(' -e XAUTHORITY=', docker_args)
84 | self.assertIn(' -v /tmp/.X11-unix:/tmp/.X11-unix ', docker_args)
85 | self.assertIn(' -v /etc/localtime:/etc/localtime:ro ', docker_args)
86 |
87 | def test_x11_extension_nocleanup(self):
88 | plugins = list_plugins()
89 | x11_plugin = plugins['x11']
90 | p = x11_plugin()
91 | mock_cliargs = {'base_image': 'ubuntu:xenial', 'nocleanup': True}
92 | docker_args = p.precondition_environment(mock_cliargs)
93 | # TODO(tfoote) do more to check that it doesn't actually clean up.
94 | # This is more of a smoke test
95 |
96 |
97 | def test_no_x11_xpdyinfo(self):
98 | for tag in self.dockerfile_tags:
99 | dig = DockerImageGenerator([], {}, tag)
100 | self.assertEqual(dig.build(), 0)
101 | self.assertNotEqual(dig.run(), 0)
102 |
103 | @pytest.mark.x11
104 | def test_x11_xpdyinfo(self):
105 | plugins = list_plugins()
106 | desired_plugins = ['x11']
107 | active_extensions = [e() for e in plugins.values() if e.get_name() in desired_plugins]
108 | for tag in self.dockerfile_tags:
109 | dig = DockerImageGenerator(active_extensions, {}, tag)
110 | self.assertEqual(dig.build(), 0)
111 | self.assertEqual(dig.run(), 0)
112 |
113 |
114 | @pytest.mark.docker
115 | class NvidiaTest(unittest.TestCase):
116 | @classmethod
117 | def setUpClass(self):
118 | client = get_docker_client()
119 | self.dockerfile_tags = []
120 | for distro_version in ['xenial', 'bionic']:
121 | dockerfile = """
122 | FROM ubuntu:%(distro_version)s
123 |
124 | RUN apt-get update && apt-get install glmark2 -y && apt-get clean
125 |
126 | CMD glmark2 --validate
127 | """
128 | dockerfile_tag = 'testfixture_%s_glmark2' % distro_version
129 | iof = StringIO((dockerfile % locals()).encode())
130 | im = client.build(fileobj = iof, tag=dockerfile_tag)
131 | for e in im:
132 | pass
133 | #print(e)
134 | self.dockerfile_tags.append(dockerfile_tag)
135 |
136 | def setUp(self):
137 | # Work around interference between empy Interpreter
138 | # stdout proxy and test runner. empy installs a proxy on stdout
139 | # to be able to capture the information.
140 | # And the test runner creates a new stdout object for each test.
141 | # This breaks empy as it assumes that the proxy has persistent
142 | # between instances of the Interpreter class
143 | # empy will error with the exception
144 | # "em.Error: interpreter stdout proxy lost"
145 | em.Interpreter._wasProxyInstalled = False
146 |
147 | def test_nvidia_extension_basic(self):
148 | plugins = list_plugins()
149 | nvidia_plugin = plugins['nvidia']
150 | self.assertEqual(nvidia_plugin.get_name(), 'nvidia')
151 | self.assertTrue(plugin_load_parser_correctly(nvidia_plugin))
152 |
153 | p = nvidia_plugin()
154 | mock_cliargs = {'base_image': 'ubuntu:xenial'}
155 | snippet = p.get_snippet(mock_cliargs)
156 |
157 | self.assertIn('COPY --from=glvnd /usr/local/lib/x86_64-linux-gnu /usr/local/lib/x86_64-linux-gnu', snippet)
158 | self.assertIn('COPY --from=glvnd /usr/local/lib/i386-linux-gnu /usr/local/lib/i386-linux-gnu', snippet)
159 | self.assertIn('ENV LD_LIBRARY_PATH /usr/local/lib/x86_64-linux-gnu:/usr/local/lib/i386-linux-gnu', snippet)
160 | self.assertIn('NVIDIA_VISIBLE_DEVICES', snippet)
161 | self.assertIn('NVIDIA_DRIVER_CAPABILITIES', snippet)
162 |
163 | mock_cliargs = {'base_image': 'ubuntu:bionic'}
164 | snippet = p.get_snippet(mock_cliargs)
165 | self.assertIn('RUN apt-get update && apt-get install -y --no-install-recommends', snippet)
166 | self.assertIn(' libglvnd0 ', snippet)
167 | self.assertIn(' libgles2 ', snippet)
168 | self.assertIn('COPY --from=glvnd /usr/share/glvnd/egl_vendor.d/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json', snippet)
169 |
170 | self.assertIn('NVIDIA_VISIBLE_DEVICES', snippet)
171 | self.assertIn('NVIDIA_DRIVER_CAPABILITIES', snippet)
172 |
173 |
174 | preamble = p.get_preamble(mock_cliargs)
175 | self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu18.04', preamble)
176 |
177 | mock_cliargs = {'base_image': 'ubuntu:jammy'}
178 | preamble = p.get_preamble(mock_cliargs)
179 | self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu22.04', preamble)
180 |
181 | mock_cliargs = {'base_image': 'ubuntu:jammy', 'nvidia_glvnd_version': '20.04'}
182 | preamble = p.get_preamble(mock_cliargs)
183 | self.assertIn('FROM nvidia/opengl:1.0-glvnd-devel-ubuntu20.04', preamble)
184 |
185 | docker_args = p.get_docker_args(mock_cliargs)
186 | #TODO(tfoote) restore with #37 self.assertIn(' -e DISPLAY -e TERM', docker_args)
187 | #TODO(tfoote) restore with #37 self.assertIn(' -e QT_X11_NO_MITSHM=1', docker_args)
188 | #TODO(tfoote) restore with #37 self.assertIn(' -e XAUTHORITY=', docker_args)
189 | #TODO(tfoote) restore with #37 self.assertIn(' -v /tmp/.X11-unix:/tmp/.X11-unix ', docker_args)
190 | #TODO(tfoote) restore with #37 self.assertIn(' -v /etc/localtime:/etc/localtime:ro ', docker_args)
191 | if get_docker_version() >= Version("19.03"):
192 | self.assertIn(' --gpus all', docker_args)
193 | else:
194 | self.assertIn(' --runtime=nvidia', docker_args)
195 |
196 | mock_cliargs = {'nvidia': 'auto'}
197 | docker_args = p.get_docker_args(mock_cliargs)
198 | if get_docker_version() >= Version("19.03"):
199 | self.assertIn(' --gpus all', docker_args)
200 | else:
201 | self.assertIn(' --runtime=nvidia', docker_args)
202 |
203 | mock_cliargs = {'nvidia': 'gpus'}
204 | docker_args = p.get_docker_args(mock_cliargs)
205 | self.assertIn(' --gpus all', docker_args)
206 |
207 | mock_cliargs = {'nvidia': 'runtime'}
208 | docker_args = p.get_docker_args(mock_cliargs)
209 | self.assertIn(' --runtime=nvidia', docker_args)
210 |
211 |
212 | def test_no_nvidia_glmark2(self):
213 | for tag in self.dockerfile_tags:
214 | dig = DockerImageGenerator([], {}, tag)
215 | self.assertEqual(dig.build(), 0)
216 | self.assertNotEqual(dig.run(), 0)
217 |
218 | @pytest.mark.nvidia
219 | @pytest.mark.x11
220 | def test_nvidia_glmark2(self):
221 | plugins = list_plugins()
222 | desired_plugins = ['x11', 'nvidia', 'user'] #TODO(Tfoote) encode the x11 dependency into the plugin and remove from test here
223 | active_extensions = [e() for e in plugins.values() if e.get_name() in desired_plugins]
224 | for tag in self.dockerfile_tags:
225 | dig = DockerImageGenerator(active_extensions, {}, tag)
226 | self.assertEqual(dig.build(), 0)
227 | self.assertEqual(dig.run(), 0)
228 |
229 | def test_nvidia_env_subs(self):
230 | plugins = list_plugins()
231 | nvidia_plugin = plugins['nvidia']
232 |
233 | p = nvidia_plugin()
234 |
235 | # base image doesn't exist
236 | mock_cliargs = {'base_image': 'ros:does-not-exist'}
237 | with self.assertRaises(SystemExit) as cm:
238 | p.get_environment_subs(mock_cliargs)
239 | self.assertEqual(cm.exception.code, 1)
240 |
241 | # unsupported version
242 | mock_cliargs = {'base_image': 'ubuntu:17.04'}
243 | with self.assertRaises(SystemExit) as cm:
244 | p.get_environment_subs(mock_cliargs)
245 | self.assertEqual(cm.exception.code, 1)
246 |
247 | # unsupported os
248 | mock_cliargs = {'base_image': 'fedora'}
249 | with self.assertRaises(SystemExit) as cm:
250 | p.get_environment_subs(mock_cliargs)
251 | self.assertEqual(cm.exception.code, 1)
252 |
253 | @pytest.mark.docker
254 | @pytest.mark.nvidia # Technically not needing nvidia but too resource intensive for main runs
255 | class CudaTest(unittest.TestCase):
256 | @classmethod
257 | def setUpClass(self):
258 | client = get_docker_client()
259 | self.dockerfile_tags = []
260 | for (distro_name, distro_version) in [
261 | ('ubuntu','focal'),
262 | ('ubuntu','jammy'),
263 | ('ubuntu','noble'),
264 | ('debian','bookworm'),
265 | ('debian','bullseye'),
266 | ]:
267 | dockerfile = """
268 | FROM %(distro_name)s:%(distro_version)s
269 | CMD dpkg -s cuda-toolkit
270 | """
271 | dockerfile_tag = 'testfixture_%s_cuda' % distro_version
272 | iof = StringIO((dockerfile % locals()).encode())
273 | im = client.build(fileobj = iof, tag=dockerfile_tag)
274 | for e in im:
275 | pass
276 | #print(e)
277 | self.dockerfile_tags.append(dockerfile_tag)
278 |
279 | def setUp(self):
280 | # Work around interference between empy Interpreter
281 | # stdout proxy and test runner. empy installs a proxy on stdout
282 | # to be able to capture the information.
283 | # And the test runner creates a new stdout object for each test.
284 | # This breaks empy as it assumes that the proxy has persistent
285 | # between instances of the Interpreter class
286 | # empy will error with the exception
287 | # "em.Error: interpreter stdout proxy lost"
288 | em.Interpreter._wasProxyInstalled = False
289 |
290 |
291 | def test_no_cuda(self):
292 | for tag in self.dockerfile_tags:
293 | dig = DockerImageGenerator([], {}, tag)
294 | self.assertEqual(dig.build(), 0)
295 | self.assertNotEqual(dig.run(), 0)
296 | dig.clear_image()
297 |
298 | def test_cuda_install(self):
299 | plugins = list_plugins()
300 | desired_plugins = ['cuda']
301 | active_extensions = [e() for e in plugins.values() if e.get_name() in desired_plugins]
302 | for tag in self.dockerfile_tags:
303 | dig = DockerImageGenerator(active_extensions, {}, tag)
304 | self.assertEqual(dig.build(), 0)
305 | self.assertEqual(dig.run(), 0)
306 | dig.clear_image()
307 |
308 | def test_cuda_env_subs(self):
309 | plugins = list_plugins()
310 | cuda_plugin = plugins['cuda']
311 |
312 | p = cuda_plugin()
313 |
314 | # base image doesn't exist
315 | mock_cliargs = {'base_image': 'ros:does-not-exist'}
316 | with self.assertRaises(SystemExit) as cm:
317 | p.get_environment_subs(mock_cliargs)
318 | self.assertEqual(cm.exception.code, 1)
319 |
320 | # unsupported version
321 | mock_cliargs = {'base_image': 'ubuntu:17.04'}
322 | with self.assertRaises(SystemExit) as cm:
323 | p.get_environment_subs(mock_cliargs)
324 | self.assertEqual(cm.exception.code, 1)
325 |
326 | # unsupported os
327 | mock_cliargs = {'base_image': 'fedora'}
328 | with self.assertRaises(SystemExit) as cm:
329 | p.get_environment_subs(mock_cliargs)
330 | self.assertEqual(cm.exception.code, 1)
331 |
--------------------------------------------------------------------------------
/test/test_os_detect.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import docker
19 | import pytest
20 | import unittest
21 |
22 |
23 | from rocker.os_detector import detect_os
24 |
25 | class RockerOSDetectorTest(unittest.TestCase):
26 |
27 | @pytest.mark.docker
28 | def test_ubuntu(self):
29 | result = detect_os("ubuntu:xenial")
30 | self.assertEqual(result[0], 'Ubuntu')
31 | self.assertEqual(result[1], '16.04')
32 |
33 | result = detect_os("ubuntu:bionic")
34 | self.assertEqual(result[0], 'Ubuntu')
35 | self.assertEqual(result[1], '18.04')
36 |
37 | # Cover verbose codepath
38 | result = detect_os("ubuntu:bionic", output_callback=print)
39 | self.assertEqual(result[0], 'Ubuntu')
40 | self.assertEqual(result[1], '18.04')
41 |
42 | @pytest.mark.docker
43 | def test_fedora(self):
44 | result = detect_os("fedora:29")
45 | self.assertEqual(result[0], 'Fedora')
46 | self.assertEqual(result[1], '29')
47 |
48 | @pytest.mark.docker
49 | def test_does_not_exist(self):
50 | result = detect_os("osrf/ros:does_not_exist")
51 | self.assertEqual(result, None)
52 |
53 | @pytest.mark.docker
54 | def test_cannot_detect_os(self):
55 | # Test with output callback too get coverage of error reporting
56 | result = detect_os("scratch", output_callback=print)
57 | self.assertEqual(result, None)
58 |
--------------------------------------------------------------------------------
/test/test_rmw_extension.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import em
19 | import os
20 | import unittest
21 | import pytest
22 |
23 |
24 | from rocker.core import DockerImageGenerator
25 | from rocker.core import list_plugins
26 |
27 | from test_extension import plugin_load_parser_correctly
28 |
29 | from rocker.rmw_extension import RMW
30 |
31 |
32 | class rmwExtensionTest(unittest.TestCase):
33 |
34 | def setUp(self):
35 | # Work around interference between empy Interpreter
36 | # stdout proxy and test runner. empy installs a proxy on stdout
37 | # to be able to capture the information.
38 | # And the test runner creates a new stdout object for each test.
39 | # This breaks empy as it assumes that the proxy has persistent
40 | # between instances of the Interpreter class
41 | # empy will error with the exception
42 | # "em.Error: interpreter stdout proxy lost"
43 | em.Interpreter._wasProxyInstalled = False
44 |
45 | def test_rmw_extension(self):
46 | plugins = list_plugins()
47 | rmw_plugin = plugins['rmw']
48 | self.assertEqual(rmw_plugin.get_name(), 'rmw')
49 |
50 | p = rmw_plugin()
51 | self.assertTrue(plugin_load_parser_correctly(rmw_plugin))
52 |
53 |
54 | mock_cliargs = {'rmw': ['cyclonedds']}
55 | self.assertEqual(p.get_preamble(mock_cliargs), '')
56 | args = p.get_docker_args(mock_cliargs)
57 | self.assertIn('-e RMW_IMPLEMENTATION=rmw_cyclonedds_cpp', args)
58 | snippet = p.get_snippet(mock_cliargs)
59 | self.assertIn('rmw-cyclonedds-cpp', snippet)
60 |
61 |
62 | #without it set
63 | mock_cliargs = {'rmw': None}
64 | args = p.get_docker_args(mock_cliargs)
65 | snippet = p.get_snippet(mock_cliargs)
66 | self.assertNotIn('RMW_IMPLEMENTATION', args)
67 | self.assertNotIn('rmw-cyclonedds-cpp', snippet)
68 |
69 |
70 | @pytest.mark.docker
71 | class rmwRuntimeExtensionTest(unittest.TestCase):
72 |
73 | def setUp(self):
74 | # Work around interference between empy Interpreter
75 | # stdout proxy and test runner. empy installs a proxy on stdout
76 | # to be able to capture the information.
77 | # And the test runner creates a new stdout object for each test.
78 | # This breaks empy as it assumes that the proxy has persistent
79 | # between instances of the Interpreter class
80 | # empy will error with the exception
81 | # "em.Error: interpreter stdout proxy lost"
82 | em.Interpreter._wasProxyInstalled = False
83 |
84 | def test_rmw_extension(self):
85 | plugins = list_plugins()
86 | rmw_plugin = plugins['rmw']
87 |
88 | rmws_to_test = RMW.rmw_map.keys()
89 |
90 | p = rmw_plugin()
91 | self.assertTrue(plugin_load_parser_correctly(rmw_plugin))
92 |
93 |
94 | for rmw_name in rmws_to_test:
95 | mock_cliargs = {'rmw': [rmw_name]}
96 | dig = DockerImageGenerator([rmw_plugin()], mock_cliargs, 'ros:rolling')
97 | self.assertEqual(dig.build(), 0, msg=f'dig.build for rmw {rmw_name} failed')
98 | self.assertEqual(dig.run(command=f'dpkg -l ros-rolling-rmw-{rmw_name}-cpp'), 0)
99 | self.assertIn(f'-e RMW_IMPLEMENTATION=rmw_{rmw_name}_cpp', dig.generate_docker_cmd('', mode='dry-run'))
100 | dig.clear_image()
101 |
--------------------------------------------------------------------------------
/test/test_ssh_extension.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import argparse
19 | import em
20 | import getpass
21 | import os
22 | import unittest
23 | from pathlib import Path
24 | import pwd
25 | import shlex
26 |
27 |
28 | from rocker.core import list_plugins
29 | from rocker.extensions import name_to_argument
30 |
31 | from test_extension import plugin_load_parser_correctly
32 |
33 | class ExtensionsTest(unittest.TestCase):
34 | def test_name_to_argument(self):
35 | self.assertEqual(name_to_argument('asdf'), '--asdf')
36 | self.assertEqual(name_to_argument('as_df'), '--as-df')
37 | self.assertEqual(name_to_argument('as-df'), '--as-df')
38 |
39 |
40 | class sshExtensionTest(unittest.TestCase):
41 |
42 | def setUp(self):
43 | # Work around interference between empy Interpreter
44 | # stdout proxy and test runner. empy installs a proxy on stdout
45 | # to be able to capture the information.
46 | # And the test runner creates a new stdout object for each test.
47 | # This breaks empy as it assumes that the proxy has persistent
48 | # between instances of the Interpreter class
49 | # empy will error with the exception
50 | # "em.Error: interpreter stdout proxy lost"
51 | em.Interpreter._wasProxyInstalled = False
52 |
53 | def test_ssh_extension(self):
54 | plugins = list_plugins()
55 | ssh_plugin = plugins['ssh']
56 | self.assertEqual(ssh_plugin.get_name(), 'ssh')
57 |
58 | p = ssh_plugin()
59 | self.assertTrue(plugin_load_parser_correctly(ssh_plugin))
60 |
61 |
62 | mock_cliargs = {}
63 | self.assertEqual(p.get_snippet(mock_cliargs), '')
64 | self.assertEqual(p.get_preamble(mock_cliargs), '')
65 | # with SSH_AUTH_SOCK set
66 | os.environ['SSH_AUTH_SOCK'] = 'foo'
67 | args = p.get_docker_args(mock_cliargs)
68 | self.assertIn('-e SSH_AUTH_SOCK -v ' + shlex.quote('{SSH_AUTH_SOCK}:{SSH_AUTH_SOCK}'.format(**os.environ)), args)
69 |
70 | #without it set
71 | del os.environ['SSH_AUTH_SOCK']
72 | args = p.get_docker_args(mock_cliargs)
73 | self.assertNotIn('SSH_AUTH_SOCK', args)
--------------------------------------------------------------------------------
/test/test_ulimit.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import unittest
19 | from argparse import ArgumentTypeError
20 |
21 | from rocker.ulimit_extension import Ulimit
22 |
23 |
24 | class UlimitTest(unittest.TestCase):
25 | """Unit tests for the Ulimit class."""
26 |
27 | def setUp(self):
28 | self._instance = Ulimit()
29 |
30 | def _is_arg_translation_ok(self, mock_cliargs, expected):
31 | is_ok = False
32 | message_string = ""
33 | try:
34 | docker_args = self._instance.get_docker_args(
35 | {self._instance.get_name(): [mock_cliargs]})
36 | is_ok = docker_args == expected
37 | message_string = f"Expected: '{expected}', got: '{docker_args}'"
38 | except ArgumentTypeError:
39 | message_string = "Incorrect argument format"
40 | return (is_ok, message_string)
41 |
42 | def test_args_single_soft(self):
43 | """Test single soft limit argument."""
44 | mock_cliargs = ["rtprio=99"]
45 | expected = " --ulimit rtprio=99"
46 | self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
47 |
48 | def test_args_multiple_soft(self):
49 | """Test multiple soft limit arguments."""
50 | mock_cliargs = ["rtprio=99", "memlock=102400"]
51 | expected = " --ulimit rtprio=99 --ulimit memlock=102400"
52 | self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
53 |
54 | def test_args_single_hard(self):
55 | """Test single hard limit argument."""
56 | mock_cliargs = ["nofile=1024:524288"]
57 | expected = " --ulimit nofile=1024:524288"
58 | self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
59 |
60 | def test_args_multiple_hard(self):
61 | """Test multiple hard limit arguments."""
62 | mock_cliargs = ["nofile=1024:524288", "rtprio=90:99"]
63 | expected = " --ulimit nofile=1024:524288 --ulimit rtprio=90:99"
64 | self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
65 |
66 | def test_args_multiple_mix(self):
67 | """Test multiple mixed limit arguments."""
68 | mock_cliargs = ["rtprio=99", "memlock=102400", "nofile=1024:524288"]
69 | expected = " --ulimit rtprio=99 --ulimit memlock=102400 --ulimit nofile=1024:524288"
70 | self.assertTrue(*self._is_arg_translation_ok(mock_cliargs, expected))
71 |
72 | def test_args_wrong_single_soft(self):
73 | """Test if single soft limit argument is wrong."""
74 | mock_cliargs = ["rtprio99"]
75 | expected = " --ulimit rtprio99"
76 | self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
77 |
78 | def test_args_wrong_multiple_soft(self):
79 | """Test if multiple soft limit arguments are wrong."""
80 | mock_cliargs = ["rtprio=99", "memlock102400"]
81 | expected = " --ulimit rtprio=99 --ulimit memlock=102400"
82 | self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
83 |
84 | def test_args_wrong_single_hard(self):
85 | """Test if single hard limit arguments are wrong."""
86 | mock_cliargs = ["nofile=1024:524288:"]
87 | expected = " --ulimit nofile=1024:524288"
88 | self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
89 |
90 | def test_args_wrong_multiple_hard(self):
91 | """Test if multiple hard limit arguments are wrong."""
92 | mock_cliargs = ["nofile1024524288", "rtprio=90:99"]
93 | expected = " --ulimit nofile=1024:524288 --ulimit rtprio=90:99"
94 | self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
95 |
96 | def test_args_wrong_multiple_mix(self):
97 | """Test if multiple mixed limit arguments are wrong."""
98 | mock_cliargs = ["rtprio=:", "memlock102400", "nofile1024:524288:"]
99 | expected = " --ulimit rtprio=99 --ulimit memlock=102400 --ulimit nofile=1024:524288"
100 | self.assertFalse(*self._is_arg_translation_ok(mock_cliargs, expected))
101 |
--------------------------------------------------------------------------------
/test/test_volume.py:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import os
19 | import unittest
20 |
21 | from rocker.core import list_plugins
22 | from rocker.volume_extension import Volume
23 |
24 |
25 | class VolumeTest(unittest.TestCase):
26 | def setUp(self):
27 | self._instance = Volume()
28 | self._curr_path = os.path.abspath(os.path.curdir)
29 | self._virtual_path = "/path/in/container"
30 |
31 | def _test_equals_args(self, mock_cliargs, expected):
32 | """
33 | @type mock_cliargs: { str: [[str]] }
34 | @type expected: [[str]]
35 | """
36 | print("DEBUG: 'mock_cliargs' {}\n\t'expected': {}".format(mock_cliargs, expected))
37 | docker_args = self._instance.get_docker_args(mock_cliargs)
38 | print("DEBUG: Resulted docker_args: {}".format(docker_args))
39 | for arg_expected in expected:
40 | # Whitespace at the beginning is needed.
41 | complete_expected = " {} {}".format(Volume.ARG_DOCKER_VOLUME, arg_expected[0])
42 | self.assertTrue(complete_expected in docker_args)
43 |
44 | def test_args_single(self):
45 | """Passing source path"""
46 | arg = [[self._curr_path]]
47 | expected = [['{}:{}'.format(self._curr_path, self._curr_path)]]
48 | mock_cliargs = {Volume.get_name(): arg}
49 | self._test_equals_args(mock_cliargs, expected)
50 |
51 | def test_args_twopaths(self):
52 | """Passing source path, dest path"""
53 | arg = ["{}:{}".format(self._curr_path, self._virtual_path)]
54 | mock_cliargs = {Volume.get_name(): [arg]}
55 | self._test_equals_args(mock_cliargs, arg)
56 |
57 | def test_args_twopaths_opt(self):
58 | """Passing source path, dest path, and Docker's volume option"""
59 | arg = ["{}:{}:ro".format(self._curr_path, self._virtual_path)]
60 | mock_cliargs = {Volume.get_name(): [arg]}
61 | self._test_equals_args(mock_cliargs, arg)
62 |
63 | def test_args_two_volumes(self):
64 | """Multiple volume points"""
65 | arg_first = ["{}:{}:ro".format(self._curr_path, self._virtual_path)]
66 | arg_second = ["/tmp:{}".format(os.path.join(self._virtual_path, "tmp"))]
67 | args = [arg_first, arg_second]
68 | mock_cliargs = {Volume.get_name(): args}
69 | self._test_equals_args(mock_cliargs, args)
70 |
--------------------------------------------------------------------------------
/wishlist.txt:
--------------------------------------------------------------------------------
1 | # Things that would be good to add
2 |
3 | Roll runtime options into expandability
4 | add support for prerequisite checks. (aka able to make sure the dockerfile is compatible)
5 | Add additional host detection options to extension points
6 | - Detect alternative base OS xenial vs bionic
7 | - Detect different kernels/nvidia drivers
8 |
9 | Add support for profiles
10 |
11 | Provide a non-command line API for use in tooling such as unit tests.
12 |
13 | Add a mechanism to declare dependencies
14 | - other plugins like home really wants user, and nvidia could require X11 and separate that out
15 | - required before, required after, and user required at the end(maybe keep as special case), snippets need ordering preambles don't
16 |
--------------------------------------------------------------------------------