Skip to content

Commit

Permalink
add detach plugin (#320)
Browse files Browse the repository at this point in the history
* add detach plugin
* Implementing conflict feedback and resolution for default operating mode for detach and no-tty present.

Signed-off-by: Tully Foote <[email protected]>
Co-authored-by: Tully Foote <[email protected]>
  • Loading branch information
YuqiHuai and tfoote authored Feb 19, 2025
1 parent 13b764c commit ade9803
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 11 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
],
'rocker.extensions': [
'cuda = rocker.nvidia_extension:Cuda',
'detach = rocker.extensions:Detach',
'devices = rocker.extensions:Devices',
'dev_helpers = rocker.extensions:DevHelpers',
'env = rocker.extensions:Environment',
Expand Down
34 changes: 33 additions & 1 deletion src/rocker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
from .core import RockerExtensionManager
from .core import DependencyMissing
from .core import ExtensionError
from .core import OPERATIONS_DRY_RUN
from .core import OPERATIONS_INTERACTIVE
from .core import OPERATIONS_NON_INTERACTIVE
from .core import OPERATION_MODES

from .os_detector import detect_os

Expand All @@ -32,7 +36,7 @@ def main():
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('image')
parser.add_argument('command', nargs='*', default='')
parser.add_argument('--noexecute', action='store_true', help='Deprecated')
parser.add_argument('--noexecute', action='store_true', help='Deprecated') # TODO(tfoote) add when 3.13 is minimum supported, deprecated=True
parser.add_argument('--nocache', action='store_true')
parser.add_argument('--nocleanup', action='store_true', help='do not remove the docker container when stopped')
parser.add_argument('--persist-image', action='store_true', help='do not remove the docker image when stopped', default=False) #TODO(tfoote) Add a name to it if persisting
Expand All @@ -56,6 +60,34 @@ def main():
args_dict['mode'] = OPERATIONS_DRY_RUN
print('DEPRECATION Warning: --noexecute is deprecated for --mode dry-run please switch your usage by December 2020')

# validate_operating_mode
operating_mode = args_dict.get('mode')
# Don't try to be interactive if there's no tty
if not os.isatty(sys.__stdin__.fileno()):
if operating_mode == OPERATIONS_INTERACTIVE:
parser.error("No tty detected cannot operate in interactive mode")
elif not operating_mode:
print("No tty detected for stdin defaulting mode to non-interactive")
args_dict['mode'] = OPERATIONS_NON_INTERACTIVE

# Check if detach extension is active and deconflict with interactive
detach_active = args_dict.get('detach')
operating_mode = args_dict.get('mode')
if detach_active:
if operating_mode == OPERATIONS_INTERACTIVE:
parser.error("Command line option --mode=interactive and --detach are mutually exclusive")
elif not operating_mode:
print(f"Detach extension active, defaulting mode to {OPERATIONS_NON_INTERACTIVE}")
args_dict['mode'] = OPERATIONS_NON_INTERACTIVE
# TODO(tfoote) Deal with the case of dry-run + detach
# Right now the printed results will include '-it'
# But based on testing the --detach overrides -it in docker so it's ok.

# Default to non-interactive if unset
if args_dict.get('mode') not in OPERATION_MODES:
print("Mode unset, defaulting to interactive")
args_dict['mode'] = OPERATIONS_NON_INTERACTIVE

try:
active_extensions = extension_manager.get_active_extensions(args_dict)
except ExtensionError as e:
Expand Down
6 changes: 1 addition & 5 deletions src/rocker/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,7 @@ def extend_cli_parser(self, parser, default_args={}):
print("Extension %s doesn't support default arguments. Please extend it." % p.get_name())
p.register_arguments(parser)
parser.add_argument('--mode', choices=OPERATION_MODES,
default=OPERATIONS_INTERACTIVE,
help="Choose mode of operation for rocker")
help="Choose mode of operation for rocker, default interactive unless detached.")
parser.add_argument('--image-name', default=None,
help='Tag the final image, useful with dry-run')
parser.add_argument('--extension-blacklist', nargs='*',
Expand Down Expand Up @@ -361,9 +360,6 @@ def get_operating_mode(self, args):
# Default to non-interactive if unset
if operating_mode not in OPERATION_MODES:
operating_mode = OPERATIONS_NON_INTERACTIVE
if operating_mode == OPERATIONS_INTERACTIVE and not os.isatty(sys.__stdin__.fileno()):
operating_mode = OPERATIONS_NON_INTERACTIVE
print("No tty detected for stdin forcing non-interactive")
return operating_mode

def generate_docker_cmd(self, command='', **kwargs):
Expand Down
26 changes: 26 additions & 0 deletions src/rocker/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ def name_to_argument(name):

from .core import RockerExtension

class Detach(RockerExtension):
@staticmethod
def get_name():
return 'detach'

def __init__(self):
self.name = Detach.get_name()

def get_docker_args(self, cliargs):
args = ''
detach = cliargs.get('detach', False)
if detach:
args += ' --detach'
return args

@staticmethod
def register_arguments(parser, defaults):
parser.add_argument(
'-d',
'--detach',
action='store_true',
default=defaults.get('detach', False),
help='Run the container in the background.'
)


class Devices(RockerExtension):
@staticmethod
def get_name():
Expand Down
10 changes: 5 additions & 5 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ def test_docker_cmd_interactive(self):

# TODO(tfoote) mock this appropriately
# google actions tests don't have a tty, local tests do
import os, sys
if os.isatty(sys.__stdin__.fileno()):
self.assertIn('-it', dig.generate_docker_cmd(mode='interactive'))
else:
self.assertNotIn('-it', dig.generate_docker_cmd(mode='interactive'))
# import os, sys
# if os.isatty(sys.__stdin__.fileno()):
# self.assertIn('-it', dig.generate_docker_cmd(mode='interactive'))
# else:
self.assertIn('-it', dig.generate_docker_cmd(mode='interactive'))

self.assertNotIn('-it', dig.generate_docker_cmd(mode='non-interactive'))

Expand Down
34 changes: 34 additions & 0 deletions test/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,40 @@ def test_name_to_argument(self):
self.assertEqual(name_to_argument('as-df'), '--as-df')


class DetachExtensionTest(unittest.TestCase):

def setUp(self):
# Work around interference between empy Interpreter
# stdout proxy and test runner. empy installs a proxy on stdout
# to be able to capture the information.
# And the test runner creates a new stdout object for each test.
# This breaks empy as it assumes that the proxy has persistent
# between instances of the Interpreter class
# empy will error with the exception
# "em.Error: interpreter stdout proxy lost"
em.Interpreter._wasProxyInstalled = False

def test_detach_extension(self):
plugins = list_plugins()
detach_plugin = plugins['detach']
self.assertEqual(detach_plugin.get_name(), 'detach')

p = detach_plugin()
self.assertTrue(plugin_load_parser_correctly(detach_plugin))

mock_cliargs = {'detach': True}
args = p.get_docker_args(mock_cliargs)
self.assertTrue('--detach' in args)

mock_cliargs = {'detach': False}
args = p.get_docker_args(mock_cliargs)
self.assertTrue('--detach' not in args)

mock_cliargs = {}
args = p.get_docker_args(mock_cliargs)
self.assertTrue('--detach' not in args)


class DevicesExtensionTest(unittest.TestCase):

def setUp(self):
Expand Down

0 comments on commit ade9803

Please sign in to comment.