★ wanayoo — archive 1999 https://github.com/pre-commit/pre-commit/pull/1484Nouvelle recherche | Portail wanayoo
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect rootless mode #1484

Open
wants to merge 11 commits into
base: master
from
Open

Detect rootless mode #1484

wants to merge 11 commits into from

Conversation

@themr0c
Copy link

@themr0c themr0c commented Jun 2, 2020

fix #1243 - the -u option is not necessary on podman

@themr0c
Copy link
Author

@themr0c themr0c commented Jun 2, 2020

It's my first attempt to write a python patch ever. I apologize if I did it wrong.

@themr0c themr0c force-pushed the themr0c:fix-1243 branch from 94a2b54 to 7c16c3e Jun 2, 2020
@themr0c
Copy link
Author

@themr0c themr0c commented Jun 2, 2020

Build failing on coverage, I am afraid it is beyond my knowledge to fix this :/.

@themr0c themr0c changed the title fix #1243 - the -u option is not necessary on podman Remove the unnecessary -u option when running invoking podman-docker Jun 2, 2020
pre_commit/languages/docker.py Outdated Show resolved Hide resolved
@themr0c themr0c changed the title Remove the unnecessary -u option when running invoking podman-docker Remove the unnecessary -u option when running in rootless mode Jun 3, 2020
pre_commit/languages/docker.py Outdated Show resolved Hide resolved
@themr0c
Copy link
Author

@themr0c themr0c commented Jun 4, 2020

Now I am facing a coverage error: https://asottile.visualstudio.com/asottile/_build/results?buildId=3831&view=logs&j=291e3f77-befc-520d-9779-f5b46c027190&t=037f03e6-93d9-56f0-123e-44a4e724aa8c&l=73

I am clueless on how to fix it:

coverage report
Name                             Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------------------
pre_commit/languages/docker.py      64      3      8      1    92%   89->90, 90-92
pre_commit/languages/docker.py Outdated Show resolved Hide resolved
@themr0c
Copy link
Author

@themr0c themr0c commented Jun 4, 2020

I spent some time trying to understand how to write a test to make the build pass. I just don't understand how to get it. Too much to learn in one go. May I request some help (again)?

So far I understand that it should probably take the for of a block like that in tests/languages/docker_test.py.

Then I assume we have to mock the content of output to test the behaviour of the for ... if ... if.

Then I got lost in the pytest documentation.

def test_docker_system_info():
    <here be dragons>:
        assert  <here be dragons>
``
@webknjaz
Copy link

@webknjaz webknjaz commented Jun 4, 2020

@themr0c you can use a built-in fixture called monkeypatch (for this, just add monkeypatch as an argument of your test method) to mock subprocess.check_output with a fake function that returns a sample output of podman (multiline).

The simple one case test would be

_ROOTLESS_DOCKER_OUTPUT = """
some garbage
   rootless: true
      even more garbage
"""

def test_rootless_docker(monkeypatch):
    """Verify that only rootless docker/podman doesn't add args."""
    monkeypatch.setattr('subprocess.check_output', lambda cmd, text: _ROOTLESS_DOCKER_OUTPUT)
    assert docker.get_docker_user() == ()
@webknjaz
Copy link

@webknjaz webknjaz commented Jun 4, 2020

And going further this would create tests for 4 test cases (rootless docker, rootless podman, non-rootless docker and non-rootless podman):

import os

import pytest

_CURRENT_UID = os.getuid()
_CURRENT_GID = os.getgid()


@pytest.mark.parametrize(
    ('docker_sys_info', 'expected_args'),
    (
        pytest.param(
            """
            some garbage
               rootless: true
                  even more garbage
            """,
            (),
            id='rootless podman',
        ),
        pytest.param(
            """
            some garbage
               rootless: false
                  even more garbage
            """,
            (_CURRENT_UID, _CURRENT_GID),
            id='non-rootless podman',
        ),
        pytest.param(
            """
            some garbage
               rootless
                  even more garbage
            """,
            (),
            id='rootless docker',
        ),
        pytest.param(
            """
            some garbage
               nothing good
                  even more garbage
            """,
            (_CURRENT_UID, _CURRENT_GID),
            id='non-rootless docker',
        ),
    ),
)
def test_rootless_docker(docker_sys_info, expected_args, monkeypatch):
    """Verify that only rootless docker/podman doesn't add args."""
    monkeypatch.setattr('subprocess.check_output', lambda cmd, text: docker_sys_info)
    assert docker.get_docker_user() == expected_args
@asottile
Copy link
Member

@asottile asottile commented Jun 4, 2020

before going further please address my comments here

in particular:

  • this needs to not run every time
  • please don't use subprocess.check_output, there's a helper (already used throughout the rest of the module(s))

additionally:

  • please don't use monkeypatch, follow the style of the rest of the codebase (unittest.mock.patch.object)
  • if you're going to write tests targetting the docker output, please make them more like real ones (they would not be indented, they would not say "garbage", etc.)
@themr0c
Copy link
Author

@themr0c themr0c commented Jun 5, 2020

I am completely illiterate in writing python code, it seems I chose a task too complex for me as first attempt to contribute :/.

  • Is there some example somewhere where I could learn to use @functools.lru_cache(maxsize=1) by copy-pasting?
  • What does the replacement helper for subprocess.check_output look like?
  • I have read the other tests ans seen some unittest.mock.patch.object. I still don't understand how to write a proper test.
@webknjaz
Copy link

@webknjaz webknjaz commented Jun 5, 2020

@themr0c you literally can just import functools and paste @functools.lru_cache(maxsize=1) right before def get_docker_user(): ... (in the previous line). LRU cache only works during the same runtime session. So if you run the program (pre-commit) again, it'll not hit the cache and execute the function. I suppose Anthony wants the cached result to be preserved across pre-commit runs which is more complicated. Also, I'd factor out the logic of identifying the rootless install into a separate helper function and apply caching just to that function.

@asottile
Copy link
Member

@asottile asottile commented Jun 5, 2020

@themr0c you literally can just import functools and paste @functools.lru_cache(maxsize=1) right before def get_docker_user(): ... (in the previous line). LRU cache only works during the same runtime session. So if you run the program (pre-commit) again, it'll not hit the cache and execute the function. I suppose Anthony wants the cached result to be preserved across pre-commit runs which is more complicated. Also, I'd factor out the logic of identifying the rootless install into a separate helper function and apply caching just to that function.

in-process cache is fine -- there's currently no precedent for across-process caching in pre-commit, though I plan to do that eventually for virtualenv invalidation

@asottile
Copy link
Member

@asottile asottile commented Jun 5, 2020

I am completely illiterate in writing python code, it seems I chose a task too complex for me as first attempt to contribute :/.

you're doing great! we'll help you through it :)

* Is there some example somewhere where I could learn to use `@functools.lru_cache(maxsize=1)` by copy-pasting?

in this case it would just be importing functools and adding that decorator to the function that's being modified here

* What does the replacement helper for `subprocess.check_output` look like?

the helper is cmd_output and you can use it like cmd_output('docker', '--version') (it returns (retcode, stdout, stderr))

* I have read the other tests ans seen  some `unittest.mock.patch.object`. I still don't understand how to write a proper test.

taking the test above, you'd use

with mock.patch.object(docker, 'cmd_output', return_value=(0, ..., '')):
    ...

instead of the monkeypatch of check_output

@hroncok
Copy link

@hroncok hroncok commented Jun 11, 2020

Side note: We have a videocall scheduled with @themr0c for early next week (this week it didn't work out) and we'll go trough the requested changes together.


@functools.lru_cache(maxsize=1)
def docker_is_rootless() -> bool:
return _docker_is_rootless()

This comment has been minimized.

@hroncok

hroncok Jun 15, 2020

Note: We had to do this to be able to test this with multiple mocked outputs. The cached version made that very hard.

This comment has been minimized.

@webknjaz

webknjaz Jun 15, 2020

could also be something like docker_is_rootless = functools.lru_cache(maxsize=1)(_docker_is_rootless)

This comment has been minimized.

@hroncok

hroncok Jun 15, 2020

Could. Not sure if more or less readable.

This comment has been minimized.

@asottile

asottile Jun 15, 2020
Member

there's an example of testing the inner part of an lru_cache function in pre_commit/languages/node.py (tests/languages/node_test.py) -- basically, access the __wrapped__ attribute of the cached object in the tests, then you don't need this indirection

This comment has been minimized.

@webknjaz

webknjaz Jul 3, 2020

@themr0c @hroncok this will need to be addressed ^



@functools.lru_cache(maxsize=1)
def docker_is_rootless() -> bool:

This comment has been minimized.

@webknjaz

webknjaz Jun 15, 2020

This should probably have a PEP257-compliant docstring

This comment has been minimized.

@hroncok

hroncok Jun 15, 2020

To be fair, none of the other functions in this file have docstrings.

This comment has been minimized.

@webknjaz

webknjaz Jun 15, 2020

Fair enough. I guess it's just my personal habit kicking in before the linters :)

Copy link

@webknjaz webknjaz left a comment

I think you could also add a test calling docker.docker_is_rootless() twice and checking that cmd_output got called only once.

@hroncok
Copy link

@hroncok hroncok commented Jun 15, 2020

I think you could also add a test calling docker.docker_is_rootless() twice and checking that cmd_output got called only once.

This seems a bit too far fetched to me. Isn't it rather testing lru_cache?

Also, technically, other test might already called this.

@themr0c
Copy link
Author

@themr0c themr0c commented Jun 15, 2020

@asottile Is it satisfactory with the latest changes?
@hroncok A big thank you for the help!

@webknjaz
Copy link

@webknjaz webknjaz commented Jun 15, 2020

This seems a bit too far fetched to me. Isn't it rather testing lru_cache?

It's a regression test: when somebody decides to remove the decorator, the test should explode. It's up to you, of course, to skip adding it.

@themr0c themr0c requested a review from asottile Jun 15, 2020
# rootless docker has "rootless"
# rootless podman has "rootless: true"
if line.strip().startswith('rootless'):
if 'false' not in line:

This comment has been minimized.

@hroncok

hroncok Jun 15, 2020

Note: Technically, the case where false is present is not tested. However in practice, we haven't found a case like this, this check is present as precaution.

This comment has been minimized.

@webknjaz

webknjaz Jun 15, 2020

This would be useful as a code comment

This comment has been minimized.

@themr0c

themr0c Jun 15, 2020
Author

Is that what is causing the coverage error?

Name                             Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------------------
pre_commit/languages/docker.py      72      2     10      2    95%   89->91, 91, 101->102, 102

This comment has been minimized.

@webknjaz

webknjaz Jun 15, 2020

This is weird. If tests include example outputs containing rootless, they should also hit this condition check.

This comment has been minimized.

@webknjaz

webknjaz Jul 3, 2020

@asottile any ideas about this?

This comment has been minimized.

@asottile

asottile Jul 3, 2020
Member

there's no test for rootless: false so line 91 is never hit -- perhaps simpler would be if line.strip() == 'rootless: true': ...

This comment has been minimized.

@webknjaz

webknjaz Jul 3, 2020

That would probably not catch docker's behavior. Better to just improve the test matrix

@webknjaz
Copy link

@webknjaz webknjaz commented Jul 3, 2020

@themr0c I think this PR's title/description should be updated.

@themr0c themr0c changed the title Remove the unnecessary -u option when running in rootless mode Detect rootless mode Jul 3, 2020
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: true
''' # noqa

This comment has been minimized.

@webknjaz

webknjaz Jul 3, 2020

it's usually better to specify specific violation codes instead of ignoring everything

@int3l

This comment has been minimized.

Copy link

@int3l int3l commented on pre_commit/languages/docker.py in 7c16c3e Jul 10, 2020

A more durable detection for podman can be:

if b'podman' in subprocess.check_output(ver_cmd):
   ...
  1. 'podman' is the unique keyword here
  2. if the output doesn't start with the word 'podman' or the word 'version' changes in the future, this will not affect the check.
@int3l

This comment has been minimized.

Copy link

@int3l int3l commented on pre_commit/languages/docker.py in 7c16c3e Jul 10, 2020

For better readability and avoiding obscure bugs, it is recommended to use try/except block on a single statement and not on entire code blocks. A.k.a, in this case we as the programmers expect an exception from exactly one statement.
Example:

try:
    podman = subprocess.check_output(...)
except AttributeError:
   return ()
...

Also the try/except behavior will assume that we use rootless, even if this is not the case (a.k.a using python version that doesn't have check_output),

@asottile
Copy link
Member

@asottile asottile commented Jul 12, 2020

let me know if you'd like me to finish this one, I've finally gotten around to setting up podman to reproduce this

@hroncok
Copy link

@hroncok hroncok commented Jul 12, 2020

Depends on @themr0c. I am available to meet again and address the review comments, but if they prefer you to handle it, I don't mind.

@themr0c
Copy link
Author

@themr0c themr0c commented Jul 13, 2020

@asottile @hroncok If you manage to finish this one, I would be very happy. I am not sure I would find time during the next coming month.

@rkm
Copy link
Contributor

@rkm rkm commented Aug 23, 2020

I may be able to take a look at this as I use CentOS and have both docker and podman installed. Are the review comments still current?

@asottile
Copy link
Member

@asottile asottile commented Sep 6, 2020

looks like --userns=keep-id works slightly better on podman, no idea if this works well for docker though 🤔 https://stackoverflow.com/a/63767259/812183

@themr0c
Copy link
Author

@themr0c themr0c commented Sep 14, 2020

The user option is not necessary, podman does the correct mapping.

$ ls -ltr toto
ls: cannot access 'toto': No such file or directory
$ podman  run --rm -ti   -v $(pwd):/docs:Z antora/antora touch /docs/toto
/docs/tox.ini-rw-rw-r--    1 root     root          7966 Sep 14 11:22 /docs/tox.ini
$ ls -ltr toto
-rw-r--r--. 1 ffloreth ffloreth 0 14 sep 14:36 toto

On the contrary, specifying user and userns can lead to trouble:

$ podman  run --rm -ti   --userns=keep-id --user $(id -u):$(id -g)   -v $(pwd):/docs:Z antora/antora ls -ltr /docs/tox.ini
Error: requested user's UID 105971 is too large for the rootless user namespace

One of the main advantages of podman is to get rid of the -u option that you need when you run docker if you don't want to see your workspaces filled by files owned by root... So why insist on keeping some unnecessary complexity?

@asottile
Copy link
Member

@asottile asottile commented Sep 14, 2020

the default mapping gives too much permission and can create undeletable files outside:

$ podman run --rm -ti -v $PWD:/z:rw ubuntu:focal bash -c 'mkdir -p /z/1/2/3 && chown -R nobody /z/1'
$ rm -rf 1
rm: cannot remove '1/2/3': Permission denied
@themr0c
Copy link
Author

@themr0c themr0c commented Sep 14, 2020

Now I undertand better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

7 participants
You can’t perform that action at this time.