From d369c5403bfd5aa5bff114d3dcc72e0158b73999 Mon Sep 17 00:00:00 2001 From: Davlet Panech Date: Thu, 19 May 2022 12:17:20 -0400 Subject: [PATCH] debian: new command: stx shell New command "stx shell", a replacement for "stx control enter" with better syntax & semantics: - Accepts an optional shell command to execute inside the container - Disables TTY emulaltion if current STDIN is not a terminal - Disables shell's interactive mode if a command is provided - Old form, "stx control enter" remains for compatibility, but prints a deprecation warning. TESTS ============================================== - Made sure "stx control enter" and "echo command | stx control enter" still work. - Manually tested "stx shell" with various parameters - Make sure "userenv" script is sourced only once at top level inside builder pod NOTE: this is a second version of an earlier (reverted) patch, d833981c70fcbcfce159f564d0757e86216f4b02, which had the following problem: in non-interactive mode, it defined BASH_ENV bwfore starting bash, which had unintended side-effects. Instead, the current patch prepends an explicit "source .../userenv" to the command being evaluated. Story: 2010055 Task: 45486 Signed-off-by: Davlet Panech Change-Id: I07592eef47bb8e7e0be654fc149a47ea241886b5 --- stx/lib/stx/config.py | 5 ++ stx/lib/stx/stx_control.py | 33 +--------- stx/lib/stx/stx_main.py | 23 +++++++ stx/lib/stx/stx_shell.py | 121 +++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 30 deletions(-) create mode 100644 stx/lib/stx/stx_shell.py diff --git a/stx/lib/stx/config.py b/stx/lib/stx/config.py index 54f6c3d3c..210a59e83 100644 --- a/stx/lib/stx/config.py +++ b/stx/lib/stx/config.py @@ -23,6 +23,8 @@ from stx import utils logger = logging.getLogger('STX-Config') utils.set_logger(logger) +ALL_CONTAINER_NAMES = ['builder', 'pkgbuilder', 'lat', 'docker', 'repomgr'] + def require_env(var): value = os.getenv(var) @@ -108,6 +110,9 @@ class Config: assert self.data return self.helm_cmd + def all_container_names(self): + return ALL_CONTAINER_NAMES + [] + @property def insecure_docker_reg_list(self): """List of insecure docker registries we are allowed to access""" diff --git a/stx/lib/stx/stx_control.py b/stx/lib/stx/stx_control.py index 6ce7a2b12..a2f05136e 100644 --- a/stx/lib/stx/stx_control.py +++ b/stx/lib/stx/stx_control.py @@ -23,6 +23,7 @@ import time from stx import helper # pylint: disable=E0611 from stx.k8s import KubeHelper +from stx import stx_shell from stx import utils # pylint: disable=E0611 helmchartdir = 'stx/stx-build-tools-chart/stx-builder' @@ -38,6 +39,7 @@ class HandleControlTask: self.logger = logging.getLogger('STX-Control') self.abs_helmchartdir = os.path.join(os.environ['PRJDIR'], helmchartdir) + self.shell = stx_shell.HandleShellTask(config) utils.set_logger(self.logger) def configurePulp(self): @@ -241,36 +243,7 @@ stx-pkgbuilder/configmap/') sys.exit(1) def handleEnterTask(self, args): - default_docker = 'builder' - container_list = ['builder', 'pkgbuilder', 'repomgr', 'lat', 'docker'] - prefix_exec_cmd = self.config.kubectl() + ' exec -ti ' - - if args.dockername: - if args.dockername not in container_list: - self.logger.error('Please input the correct docker name \ -argument. eg: %s \n', container_list) - sys.exit(1) - default_docker = args.dockername - - podname = self.k8s.get_pod_name(default_docker) - if podname: - if default_docker == 'builder': - cmd = prefix_exec_cmd + podname - cmd = cmd + ' -- bash -l -c \'runuser -u ${MYUNAME} -- bash \ ---rcfile /home/$MYUNAME/userenv\'' - elif default_docker == 'docker': - cmd = prefix_exec_cmd + podname + ' -- sh' - else: - cmd = prefix_exec_cmd + podname + ' -- bash' - self.logger.debug('Execute the enter command: %s', cmd) - # Return exit status to shell w/o raising an exception - # in case the user did "echo COMMAND ARGS | stx control enter" - ret = subprocess.call(cmd, shell=True) - sys.exit(ret) - else: - self.logger.error('Please ensure the docker container you want to \ -enter has been started!!!\n') - sys.exit(1) + self.shell.cmd_control_enter(args) def handleControl(self, args): diff --git a/stx/lib/stx/stx_main.py b/stx/lib/stx/stx_main.py index 605de4db2..a8fe605d0 100644 --- a/stx/lib/stx/stx_main.py +++ b/stx/lib/stx/stx_main.py @@ -20,6 +20,7 @@ from stx import stx_build # pylint: disable=E0611 from stx import stx_configparser # pylint: disable=E0611 from stx import stx_control # pylint: disable=E0611 from stx import stx_repomgr # pylint: disable=E0611 +from stx import stx_shell # pylint: disable=E0611 from stx import utils # pylint: disable=E0611 logger = logging.getLogger('STX') @@ -39,6 +40,7 @@ class CommandLine: self.handlecontrol = stx_control.HandleControlTask(self.config) self.handlebuild = stx_build.HandleBuildTask(self.config) self.handlerepomgr = stx_repomgr.HandleRepomgrTask(self.config) + self.handleshell = stx_shell.HandleShellTask(self.config) self.parser = self.parseCommandLine() def parseCommandLine(self): @@ -140,6 +142,26 @@ remove_repo|search_pkg|upload_pkg|delete_pkg ]') repo_subparser.add_argument('args', nargs=argparse.REMAINDER) repo_subparser.set_defaults(handle=self.handlerepomgr.handleCommand) + # stx shell + shell_subparser = subparsers.add_parser( + 'shell', + help='Run a shell command or start an interactive shell') + shell_subparser.add_argument( + '-c', '--command', + help='Shell snippet to execute inside a container. If omitted ' + + 'start a shell that reads commands from STDIN.') + shell_subparser.add_argument( + '--no-tty', + help="Disable terminal emulation for STDIN and start shell in " + + "non-interactive mode, even if STDIN is a TTY", + action='store_const', const=True) + shell_subparser.add_argument( + '--container', + metavar='builder|pkgbuilder|lat|repomgr|docker', + help='Container name (default: builder)') + shell_subparser.set_defaults(handle=self.handleshell.cmd_shell) + + # common args parser.add_argument('-d', '--debug', help='Enable debug output\n\n', action='store_const', const=logging.DEBUG, @@ -157,6 +179,7 @@ remove_repo|search_pkg|upload_pkg|delete_pkg ]') parser.add_argument('-v', '--version', help='Stx build tools version\n\n', action='version', version='%(prog)s 1.0.0') + return parser def parseArgs(self): diff --git a/stx/lib/stx/stx_shell.py b/stx/lib/stx/stx_shell.py new file mode 100644 index 000000000..a85718aae --- /dev/null +++ b/stx/lib/stx/stx_shell.py @@ -0,0 +1,121 @@ +# Copyright (c) 2021 Wind River Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import shlex +from stx.k8s import KubeHelper +from stx import utils # pylint: disable=E0611 +import subprocess +import sys + +# Logger can only be initialized once globally, not in class constructor. +# FIXME: review/fix utils.set_logger() +logger = logging.getLogger('STX-Shell') +utils.set_logger(logger) + + +def quote(wordlist): + if hasattr(wordlist, '__iter__') and not isinstance(wordlist, str): + return ' '.join([shlex.quote(w) for w in wordlist]) + return shlex.quote(wordlist) + + +class HandleShellTask: + + def __init__(self, config): + self.config = config + self.k8s = KubeHelper(config) + self.all_container_names = self.config.all_container_names() + self.logger = logger + + def __get_container_name(self, container): + if container not in self.config.all_container_names(): + raise NameError('Invalid container %s, expecting one of: %s' + % (container, self.all_container_names)) + name = self.k8s.get_pod_name(container) + if not name: + raise RuntimeError('Container "%s" is not running' % container) + return name + + def create_shell_command(self, container, command, no_tty): + kubectl_args = ['exec', '--stdin'] + + if not no_tty and sys.stdin.isatty(): + kubectl_args += ['--tty'] + + kubectl_args += [self.__get_container_name(container)] + + # builder + if container == 'builder': + + # This environment script is always required + req_env_file = '/home/$MYUNAME/userenv' + + user_cmd = 'runuser -u $MYUNAME -- ' + + # Shells have a concept of "interactive mode", when enabled: + # - define PS1 (prompt string, printed before every command) + # - source ~/.bashrc or --rcfile on startup + # - include "i" in "$-" shell var + # This mode is automatically enabled with either "bash -i", + # or if STDIN is a terminal. + + # No command given: interactive mode + source our own + # env file instead of ~/.bashrc + if command is None: + user_cmd += 'bash --rcfile %s -i' % req_env_file + # User provided a command to execute - shell may or may not run in + # interactive mode, depending on STDIN being a terminal. If it's + # not interactive, it won't source any rcfiles, so add an explicit + # "source FILENAME" in front of user command, and disable rcfiles + # to cover both situations. + else: + user_cmd += 'bash --norc -c ' + user_cmd += quote('source %s && { %s ; }' % + (req_env_file, command)) + + kubectl_args += ['--', 'bash', '-l', '-c', user_cmd] + + elif container == 'docker': + kubectl_args += ['--', 'sh', '-l'] + if command: + kubectl_args += ['-c', command] + else: + kubectl_args += ['--', 'bash', '-l'] + if command: + kubectl_args += ['-c', command] + + return self.config.kubectl() + ' ' + quote(kubectl_args) + + def _do_shell(self, args, no_tty, command, container_arg='container'): + container = getattr(args, container_arg) or 'builder' + if container not in self.all_container_names: + self.logger.error("--%s must be one of: %s", + container_arg, self.all_container_names) + sys.exit(1) + + shell_command = self.create_shell_command(container, command, no_tty) + self.logger.debug('Running command: %s', shell_command) + shell_status = subprocess.call(shell_command, shell=True) + sys.exit(shell_status) + + def cmd_shell(self, args): + self.logger.setLevel(args.loglevel) + self._do_shell(args, args.no_tty, args.command) + + def cmd_control_enter(self, args): + self.logger.setLevel(args.loglevel) + self.logger.warn("""This command is deprecated, please use "stx shell" instead""") + self._do_shell(args, False, None, 'dockername')