Skip to content

Commit

Permalink
#278 #408 added support for stdin parameters + allowed to disabled pa…
Browse files Browse the repository at this point in the history
…ssing parameters as arguments/env variables
  • Loading branch information
bugy committed Mar 17, 2023
1 parent a4c38f7 commit c30ff9c
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 11 deletions.
6 changes: 1 addition & 5 deletions samples/scripts/parameterized.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,4 @@ echo 'Environment variables:'
echo 'Req_Text='"$Req_Text"
printenv | grep -P '^(PARAM_|EXECUTION)'

trap -- '' SIGINT SIGTERM
while true; do
date +%F_%T
sleep 1
done
sleep 3
4 changes: 4 additions & 0 deletions src/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
FILE_TYPE_FILE = 'file'
FILE_TYPE_DIR = 'dir'
SHARED_ACCESS_TYPE_ALL = "ALL_USERS"

PASS_AS_ARGUMENT = 'argument'
PASS_AS_ENV_VAR = 'env_variable'
PASS_AS_STDIN = 'stdin'
83 changes: 81 additions & 2 deletions src/execution/executor.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logging
import re
import sys
from typing import List

from execution import process_popen, process_base
from model import model_helper
from model.model_helper import read_bool
from utils import file_utils, process_utils, os_utils
from model.parameter_config import ParameterModel
from react.observable import ObservableBase
from utils import file_utils, process_utils, os_utils, string_utils
from utils.env_utils import EnvVariables
from utils.transliteration import transliterate

Expand Down Expand Up @@ -106,6 +109,8 @@ def start(self, execution_id):
output_stream = process_wrapper.output_stream.time_buffered(TIME_BUFFER_MS, _concat_output)
self.raw_output_stream = output_stream.replay()

send_stdin_parameters(self.config.parameters, parameter_values, self.raw_output_stream, process_wrapper)

if self.secure_replacements:
self.protected_output_stream = output_stream \
.map(self.__replace_secure_variables) \
Expand Down Expand Up @@ -216,6 +221,9 @@ def build_command_args(param_values, config):
name = parameter.name
option_name = parameter.param

if not parameter.pass_as.pass_as_argument():
continue

if name in param_values:
value = param_values[name]

Expand Down Expand Up @@ -270,7 +278,7 @@ def _to_env_name(key):
return 'PARAM_' + replaced.upper()


def _build_env_variables(parameter_values, parameters, execution_id):
def _build_env_variables(parameter_values, parameters: List[ParameterModel], execution_id):
result = {}
excluded = []
for param_name, value in parameter_values.items():
Expand All @@ -283,6 +291,9 @@ def _build_env_variables(parameter_values, parameters, execution_id):

parameter = found_parameters[0]

if not parameter.pass_as.pass_as_env_variable():
continue

env_var = parameter.env_var
if env_var is None:
env_var = _to_env_name(param_name)
Expand Down Expand Up @@ -315,6 +326,36 @@ def _concat_output(output_chunks):
return [''.join(output_chunks)]


def send_stdin_parameters(
parameters: List[ParameterModel],
parameter_values,
raw_output_stream: ObservableBase,
process_wrapper):
for parameter in parameters:
if not parameter.pass_as.pass_as_stdin():
continue

value = parameter_values.get(parameter.name)
if value is None:
continue

if parameter.no_value:
if read_bool(value):
value = 'true'
else:
value = 'false'

if isinstance(value, list):
value = ','.join(string_utils.values_to_string(value))

if not parameter.stdin_expected_text:
process_wrapper.write_to_input(value)
else:
raw_output_stream.subscribe(_ExpectedTextListener(
parameter.stdin_expected_text,
lambda closed_value=value: process_wrapper.write_to_input(closed_value)))


class _Value:
def __init__(self, user_value, mapped_script_value, script_arg, secure_value=None):
self.user_value = user_value
Expand All @@ -332,3 +373,41 @@ def __str__(self) -> str:
return str(self.secure_value)

return str(self.script_arg)


class _ExpectedTextListener:
def __init__(self, expected_text, callback):
self.expected_text = expected_text
self.callback = callback

self.buffer = ''
self.first_char = expected_text[0]
self.value_sent = False

def on_next(self, output):
if self.value_sent:
return

full_text = self.buffer + output

while True:
start_index = full_text.find(self.first_char)
if start_index < 0:
self.buffer = ''
return

full_text = full_text[start_index:]

if len(full_text) < len(self.expected_text):
self.buffer = full_text
return

if full_text.startswith(self.expected_text):
self.callback()
self.value_sent = True
return

full_text = full_text[1:]

def on_close(self):
pass
3 changes: 3 additions & 0 deletions src/execution/process_popen.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def start_execution(self, command, working_directory):
errors='replace')

def write_to_input(self, value):
if self.is_finished():
return

input_value = value
if not value.endswith("\n"):
input_value += "\n"
Expand Down
3 changes: 3 additions & 0 deletions src/execution/process_pty.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def start_execution(self, command, working_directory):
fcntl.fcntl(self.pty_master, fcntl.F_SETFL, os.O_NONBLOCK)

def write_to_input(self, value):
if self.is_finished():
return

input_value = value
if not input_value.endswith("\n"):
input_value += "\n"
Expand Down
36 changes: 35 additions & 1 deletion src/model/parameter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ipaddress import ip_address, IPv4Address, IPv6Address

from config.constants import PARAM_TYPE_SERVER_FILE, FILE_TYPE_FILE, PARAM_TYPE_MULTISELECT, FILE_TYPE_DIR, \
PARAM_TYPE_EDITABLE_LIST
PARAM_TYPE_EDITABLE_LIST, PASS_AS_ARGUMENT, PASS_AS_ENV_VAR, PASS_AS_STDIN
from config.script.list_values import ConstValuesProvider, ScriptValuesProvider, EmptyValuesProvider, \
DependantScriptValuesProvider, NoneValuesProvider, FilesProvider
from model import model_helper
Expand Down Expand Up @@ -59,6 +59,8 @@ def __init__(self, parameter_config, username, audit_name,
self._process_invoker = process_invoker

self.name = parameter_config.get('name')
self.pass_as: PassAsConfiguration = _read_pass_as(parameter_config, self.name)
self.stdin_expected_text = parameter_config.get('stdin_expected_text')

self._original_config = parameter_config
self._parameter_values = other_param_values
Expand Down Expand Up @@ -511,6 +513,20 @@ def update_default(_, new):
update_default(None, template_property.value)


class PassAsConfiguration:
def __init__(self, configured_option) -> None:
self._configured_option = configured_option

def pass_as_argument(self):
return (self._configured_option is None) or (self._configured_option == PASS_AS_ARGUMENT)

def pass_as_env_variable(self):
return (self._configured_option is None) or (self._configured_option == PASS_AS_ENV_VAR)

def pass_as_stdin(self):
return self._configured_option == PASS_AS_STDIN


def _resolve_file_dir(config, key):
raw_value = config.get(key)
if not raw_value:
Expand Down Expand Up @@ -589,3 +605,21 @@ def get_order(key):

sorted_config = OrderedDict(sorted(param_config.items(), key=lambda item: get_order(item[0])))
return sorted_config


def _read_pass_as(parameter_config, param_name):
default_value = PassAsConfiguration(None)

pass_as = parameter_config.get('pass_as')
if is_empty(pass_as):
return default_value

pass_as = pass_as.lower().strip()

allowed_values = [PASS_AS_ARGUMENT, PASS_AS_ENV_VAR, PASS_AS_STDIN]
if pass_as not in allowed_values:
LOGGER.warning(f'Unknown pass_as value "{pass_as}" for parameter {param_name}. '
f'Should be one of: {allowed_values}')
return default_value

return PassAsConfiguration(pass_as)
72 changes: 72 additions & 0 deletions src/tests/executor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tests import test_utils
from tests.test_utils import _MockProcessWrapper, create_config_model, create_script_param_config, \
create_parameter_model, assert_contains_sub_dict
from utils import string_utils

BUFFER_FLUSH_WAIT_TIME = (executor.TIME_BUFFER_MS * 1.5) / 1000.0

Expand Down Expand Up @@ -128,6 +129,77 @@ def test_start_with_multiple_values_when_one_not_exist(self):
process_wrapper.all_env_variables,
{'PARAM_ID': '918273', 'PARAM_VERBOSE': 'true', 'EXECUTION_ID': '123'})

def test_pass_as(self):
config = create_config_model(
'config_x',
script_command='bash -c \'sleep 0.1 && echo ">$0< >$1< >$PARAM_P1< >$PARAM_P2< >$PARAM_P3<"\'',
parameters=[
create_script_param_config('p1', pass_as='argument'),
create_script_param_config('p2', pass_as='env_variable'),
create_script_param_config('p3', pass_as='stdin'),
])

executor._process_creator = create_process_wrapper
self.create_executor(config, {'p1': 'abc', 'p2': 'def', 'p3': 'xyz'})
self.executor.start(123)

data = read_until_closed(self.executor.get_raw_output_stream(), 200)
output = ''.join(data)

self.assertEqual('xyz\n>abc< >< >< >def< ><\n', output)

def test_pass_as_stdin(self):
file_path = test_utils.create_file('my_script.sh', text='''
sleep 0.1
echo -n 'ababa'
sleep 0.1
echo -n 'baba'
sleep 0.1
echo -n 'bcdef'
sleep 0.1
echo 'abc'
sleep 0.1
read input1
read input2
read input3
read input4
read input5
read input6
sleep 0.1
echo "inputs: '$input1' '$input2' '$input3' '$input4' '$input5' '$input6'"
''')

config = create_config_model(
'config_x',
script_command='bash ' + file_path,
parameters=[
create_script_param_config('p1', pass_as='stdin'),
create_script_param_config('p2', pass_as='stdin', stdin_expected_text='abc'),
create_script_param_config('p3', pass_as='stdin'),
create_script_param_config('p4', pass_as='stdin'),
create_script_param_config('p5', pass_as='stdin', no_value=True),
create_script_param_config('p6', pass_as='stdin', no_value=True),
create_script_param_config('p7', pass_as='stdin', stdin_expected_text='b'),
])

executor._process_creator = create_process_wrapper
self.create_executor(config, {'p1': 'xxx', 'p2': 'yyy', 'p3': [1, 3, 7], 'p5': True, 'p6': False, 'p7': 'zzz'})
self.executor.start(123)

data = read_until_closed(self.executor.get_raw_output_stream(), 1000)
output = ''.join(data)

self.assertEqual(string_utils.dedent('''
xxx
1,3,7
true
false
ababazzz
bababcdefyyy
abc
inputs: 'xxx' '1,3,7' 'true' 'false' 'zzz' 'yyy'
'''), output)

def create_executor(self, config, parameter_values):
self.executor = ScriptExecutor(config, parameter_values, test_utils.env_variables)

Expand Down
31 changes: 31 additions & 0 deletions src/tests/parameter_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,37 @@ def test_normalize_multiselect_when_none(self):
self.assertEqual([], parameter.normalize_user_value(None))


class TestPassAsValue(unittest.TestCase):
@parameterized.expand([
('argument', True, False, False),
('env_variable', False, True, False),
('EnV_VariablE', False, True, False),
('stdin', False, False, True),
('STDIN ', False, False, True),
(None, True, True, False),
('unknown value', True, True, False),
])
def test_pass_as(self, config, pass_as_arg, pass_as_env, pass_as_stdin):
parameter = create_parameter_model('param', pass_as=config)

pass_as = parameter.pass_as
actual_value = (pass_as.pass_as_argument(), pass_as.pass_as_env_variable(), pass_as.pass_as_stdin())

self.assertEqual((pass_as_arg, pass_as_env, pass_as_stdin), actual_value)


class TestStdinExpectedText(unittest.TestCase):

@parameterized.expand([
('some text\nabc', 'some text\nabc'),
(None, None),
])
def test_stdin_expected_text(self, config, expected_value):
parameter = create_parameter_model('param', stdin_expected_text=config)

self.assertEqual(expected_value, parameter.stdin_expected_text)


class GetSortedParamConfig(unittest.TestCase):
def test_get_sorted_when_3_fields(self):
config = get_sorted_config({'type': 'int', 'name': 'Param X', 'required': True})
Expand Down
Loading

0 comments on commit c30ff9c

Please sign in to comment.