From aa8be5dc94ecb9a62af8b07adb234d157eee486a Mon Sep 17 00:00:00 2001 From: Stephan Huber Date: Wed, 10 Feb 2021 22:28:20 +0100 Subject: [PATCH] Add new way of handling secrets via new replacement pattern Secrets can be declared in a secrets section in the root of a fabfile (similar to a question) and be used in any host-configuration as `%secret.MY_SECRET_NAME%. Phab will inject the actual value when the host configuration is requested. The actual value can be passed via environment variable of command line argument `--secret key=value` As a last ressort, the user is asked for the secret via shell. --- .editorconfig | 3 + src/Command/AboutCommand.php | 2 +- src/Command/AppCreateCommand.php | 2 +- src/Command/AppDestroyCommand.php | 2 +- src/Command/AppScaffoldCommand.php | 3 +- src/Command/AppUpdateCommand.php | 2 +- src/Command/BackupCommand.php | 2 +- src/Command/BaseCommand.php | 5 +- src/Command/BaseOptionsCommand.php | 22 ++- src/Command/CopyFromCommand.php | 2 +- src/Command/DeployCommand.php | 2 +- src/Command/DockerCommand.php | 2 +- src/Command/GetBackupCommand.php | 2 +- src/Command/GetFileCommand.php | 2 +- src/Command/GetFilesDumpCommand.php | 2 +- src/Command/GetSqlDumpCommand.php | 2 +- src/Command/InstallCommand.php | 2 +- src/Command/JiraCommand.php | 2 +- src/Command/K8sCommand.php | 2 +- src/Command/ListBackupsCommand.php | 2 +- src/Command/NotifyCommand.php | 2 +- src/Command/OutputCommand.php | 10 +- src/Command/PlatformCommand.php | 2 +- src/Command/PutFileCommand.php | 2 +- src/Command/ResetCommand.php | 2 +- src/Command/RestoreCommand.php | 2 +- src/Command/RestoreSqlFromFileCommand.php | 2 +- src/Command/ScaffoldCommand.php | 2 +- src/Command/ShellCommand.php | 2 +- src/Command/ShellCommandCommand.php | 2 +- .../SimpleExecutableInvocationCommand.php | 2 +- src/Command/StartRemoteAccessCommand.php | 2 +- src/Command/VariablePull.php | 2 +- src/Command/VariablePush.php | 2 +- src/Command/VersionCommand.php | 2 +- src/Command/WebhookCommand.php | 2 +- src/Command/WorkspaceCreateCommand.php | 2 +- src/Command/WorkspaceUpdateCommand.php | 3 +- src/Configuration/BlueprintConfiguration.php | 12 +- src/Configuration/BlueprintTemplate.php | 25 +++- src/Configuration/ConfigurationService.php | 42 +++++- src/Configuration/HostConfig.php | 5 + src/Exception/UnknownSecretException.php | 7 + src/Method/ArtifactsFtpMethod.php | 3 +- src/Method/TaskContext.php | 14 +- src/Utilities/PasswordManager.php | 111 +++++++++++++++- src/Utilities/PasswordManagerInterface.php | 13 +- src/Utilities/Utilities.php | 29 ++++ tests/ConfigurationServiceTest.php | 2 + tests/SecretsTest.php | 125 ++++++++++++++++++ tests/WebhookCommandTest.php | 18 +-- tests/assets/secret-tests/fabfile.yaml | 43 ++++++ .../shell-command-options-tests/fabfile.yaml | 3 + 53 files changed, 489 insertions(+), 73 deletions(-) create mode 100644 src/Exception/UnknownSecretException.php create mode 100644 tests/SecretsTest.php create mode 100644 tests/assets/secret-tests/fabfile.yaml diff --git a/.editorconfig b/.editorconfig index 4a492a38..28eed973 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.yaml] +indent_size = 2 + [composer.{json,lock}] indent_size = 4 diff --git a/src/Command/AboutCommand.php b/src/Command/AboutCommand.php index be638d53..a15aa025 100644 --- a/src/Command/AboutCommand.php +++ b/src/Command/AboutCommand.php @@ -48,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->write($output, $this->getDockerConfig()->raw(), 2); } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $this->getMethods()->runTask('about', $this->getHostConfig(), $context); } diff --git a/src/Command/AppCreateCommand.php b/src/Command/AppCreateCommand.php index 66c80748..fc74939c 100644 --- a/src/Command/AppCreateCommand.php +++ b/src/Command/AppCreateCommand.php @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $copy_from = $this->getConfiguration()->getHostConfig($copy_from); } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); $this->configuration->getMethodFactory()->runTask('appCheckExisting', $host_config, $context); diff --git a/src/Command/AppDestroyCommand.php b/src/Command/AppDestroyCommand.php index e714f78e..9048d615 100644 --- a/src/Command/AppDestroyCommand.php +++ b/src/Command/AppDestroyCommand.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); $this->configuration->getMethodFactory()->runTask('appCheckExisting', $host_config, $context); diff --git a/src/Command/AppScaffoldCommand.php b/src/Command/AppScaffoldCommand.php index ef31d948..5671932a 100644 --- a/src/Command/AppScaffoldCommand.php +++ b/src/Command/AppScaffoldCommand.php @@ -62,9 +62,10 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $context = $this->createContext($input, $output); + $url = $input->getArgument('scaffold-url'); $root_folder = empty($input->getOption('output')) ? getcwd() : $input->getOption('output'); - $context = $this->createContext($input, $output); $this->scaffold($url, $root_folder, $context, [], new Options()); return 0; diff --git a/src/Command/AppUpdateCommand.php b/src/Command/AppUpdateCommand.php index dce4101d..cc601a43 100644 --- a/src/Command/AppUpdateCommand.php +++ b/src/Command/AppUpdateCommand.php @@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); try { $this->getMethods()->runTask('appUpdate', $this->getHostConfig(), $context); diff --git a/src/Command/BackupCommand.php b/src/Command/BackupCommand.php index 58bdda2e..890bb724 100644 --- a/src/Command/BackupCommand.php +++ b/src/Command/BackupCommand.php @@ -46,7 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('what', array_map(function ($elem) { return trim(strtolower($elem)); }, $input->getArgument('what'))); diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index 94e9c61f..70cec607 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -12,6 +12,7 @@ use Phabalicious\Exception\ShellProviderNotFoundException; use Phabalicious\Exception\ValidationFailedException; use Phabalicious\Exception\MissingHostConfigException; +use Phabalicious\Method\TaskContext; use Phabalicious\ShellCompletion\FishShellCompletionContext; use Phabalicious\ShellProvider\ShellOptions; use Phabalicious\ShellProvider\ShellProviderInterface; @@ -35,6 +36,7 @@ abstract class BaseCommand extends BaseOptionsCommand private $dockerConfig; + protected function configure() { $default_conf = getenv('PHABALICIOUS_DEFAULT_CONFIG'); @@ -126,7 +128,8 @@ public function completeOptionValues($optionName, CompletionContext $context) */ protected function execute(InputInterface $input, OutputInterface $output) { - $io = new SymfonyStyle($input, $output); + $this->createContext($input, $output); + $io = $this->getContext()->io(); $this->checkAllRequiredOptionsAreNotEmpty($input); diff --git a/src/Command/BaseOptionsCommand.php b/src/Command/BaseOptionsCommand.php index aeafcffe..7de42fac 100644 --- a/src/Command/BaseOptionsCommand.php +++ b/src/Command/BaseOptionsCommand.php @@ -6,6 +6,7 @@ use Phabalicious\Method\MethodFactory; use Phabalicious\Method\ScriptMethod; use Phabalicious\Method\TaskContext; +use Phabalicious\Method\TaskContextInterface; use Phabalicious\Utilities\Utilities; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; @@ -26,6 +27,9 @@ abstract class BaseOptionsCommand extends Command implements CompletionAwareInte */ protected $methods; + /** @var \Phabalicious\Method\TaskContextInterface */ + private $context = null; + public function __construct(ConfigurationService $configuration, MethodFactory $method_factory, $name = null) { @@ -68,6 +72,13 @@ protected function configure() InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Pass optional arguments', [] + ) + ->addOption( + 'secret', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Pass optional secrets', + [] ); } @@ -171,7 +182,7 @@ protected function parseScriptArguments(array $defaults, $arguments_string) */ protected function createContext(InputInterface $input, OutputInterface $output, $default_arguments = []) { - $context = new TaskContext($this, $input, $output); + $context = $this->context ? $this->context : new TaskContext($this, $input, $output); $arguments = $this->parseScriptArguments($default_arguments, $input->getOption('arguments')); $context->set('variables', $arguments); $context->set('deployArguments', $arguments); @@ -180,6 +191,13 @@ protected function createContext(InputInterface $input, OutputInterface $output, $this->parseScriptArguments([], $input->getOption('arguments'))['arguments'] ?? [] ); - return $context; + $this->context = $context; + return $this->context; + } + + protected function getContext(): TaskContextInterface + { + assert($this->context); + return $this->context; } } diff --git a/src/Command/CopyFromCommand.php b/src/Command/CopyFromCommand.php index 2e47a389..b3ea8b3f 100644 --- a/src/Command/CopyFromCommand.php +++ b/src/Command/CopyFromCommand.php @@ -68,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $from = $this->configuration->getHostConfig($input->getArgument('from')); if (empty($from['supportsCopyFrom'])) { throw new \InvalidArgumentException('Source config does not support copy-from!'); diff --git a/src/Command/DeployCommand.php b/src/Command/DeployCommand.php index df128b3a..94eb6d0b 100644 --- a/src/Command/DeployCommand.php +++ b/src/Command/DeployCommand.php @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); // Override branch in config. $branch = $input->getArgument('branch'); diff --git a/src/Command/DockerCommand.php b/src/Command/DockerCommand.php index 56433155..e6897a73 100644 --- a/src/Command/DockerCommand.php +++ b/src/Command/DockerCommand.php @@ -68,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $docker_config = $this->getDockerConfig(); $context->set('docker_config', $docker_config); diff --git a/src/Command/GetBackupCommand.php b/src/Command/GetBackupCommand.php index 19c9df95..f3b56a84 100644 --- a/src/Command/GetBackupCommand.php +++ b/src/Command/GetBackupCommand.php @@ -57,7 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return trim(strtolower($elem)); }, $input->getArgument('what')); - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('what', $what); $hash = $input->getArgument('hash'); diff --git a/src/Command/GetFileCommand.php b/src/Command/GetFileCommand.php index 768a1a87..ad7d955e 100644 --- a/src/Command/GetFileCommand.php +++ b/src/Command/GetFileCommand.php @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $file = $input->getArgument('file'); - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('sourceFile', $file); $context->set('destFile', getcwd() . '/' . basename($file)); diff --git a/src/Command/GetFilesDumpCommand.php b/src/Command/GetFilesDumpCommand.php index 33bd4bcb..7b5bcebd 100644 --- a/src/Command/GetFilesDumpCommand.php +++ b/src/Command/GetFilesDumpCommand.php @@ -42,7 +42,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $this->getMethods()->runTask('getFilesDump', $this->getHostConfig(), $context); $to_copy = $context->getResult('files'); diff --git a/src/Command/GetSqlDumpCommand.php b/src/Command/GetSqlDumpCommand.php index 64ac2523..700dee0b 100644 --- a/src/Command/GetSqlDumpCommand.php +++ b/src/Command/GetSqlDumpCommand.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $this->getMethods()->runTask('getSQLDump', $this->getHostConfig(), $context); $to_copy = $context->getResult('files'); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index fff7600e..fe8b5dc8 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -53,7 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); if ($host_config['supportsInstalls'] == false) { diff --git a/src/Command/JiraCommand.php b/src/Command/JiraCommand.php index 23dfb34d..aca057f1 100644 --- a/src/Command/JiraCommand.php +++ b/src/Command/JiraCommand.php @@ -42,7 +42,7 @@ protected function execute(InputInterface $input, OutputInterface $output) { $this->checkAllRequiredOptionsAreNotEmpty($input); $this->readConfiguration($input); - $context = $this->createContext($input, $output); + $context = $this->getContext(); $jira_config = $this->configuration->getSetting('jira', []); $errors = new ValidationErrorBag(); diff --git a/src/Command/K8sCommand.php b/src/Command/K8sCommand.php index d84509ab..51a7bc0f 100644 --- a/src/Command/K8sCommand.php +++ b/src/Command/K8sCommand.php @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $subcommands = $input->getArgument('k8s'); if (!is_array($subcommands)) { $subcommands = [ $subcommands ]; diff --git a/src/Command/ListBackupsCommand.php b/src/Command/ListBackupsCommand.php index d5a63dd0..4557af8c 100644 --- a/src/Command/ListBackupsCommand.php +++ b/src/Command/ListBackupsCommand.php @@ -50,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $what = array_map(function ($elem) { return trim(strtolower($elem)); }, $input->getArgument('what')); diff --git a/src/Command/NotifyCommand.php b/src/Command/NotifyCommand.php index 35e29871..947479cd 100644 --- a/src/Command/NotifyCommand.php +++ b/src/Command/NotifyCommand.php @@ -60,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($result = parent::execute($input, $output)) { return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('message', $input->getArgument('message')); $context->set('channel', $input->getOption('channel')); diff --git a/src/Command/OutputCommand.php b/src/Command/OutputCommand.php index 23e9bec8..b88f533f 100644 --- a/src/Command/OutputCommand.php +++ b/src/Command/OutputCommand.php @@ -10,6 +10,7 @@ use Phabalicious\Exception\MissingHostConfigException; use Phabalicious\Exception\ShellProviderNotFoundException; use Phabalicious\Exception\ValidationFailedException; +use Phabalicious\Method\TaskContext; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -70,6 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->readConfiguration($input); $data = []; $title = ''; + $context = new TaskContext($this, $input, $output); if ($what == 'blueprint') { if (empty($blueprint)) { @@ -82,9 +84,13 @@ protected function execute(InputInterface $input, OutputInterface $output) ]; $title = 'Output of applied blueprint `' . $config . '`'; } elseif ($what == 'host') { - $data = $this->getConfiguration()->getHostConfig($config)->raw(); + if (!empty($blueprint)) { + $data = $this->getConfiguration()->getHostConfigFromBlueprint($config, $blueprint); + } else { + $data = $this->getConfiguration()->getHostConfig($config); + } $data = [ - $data['configName'] => $data + $data['configName'] => $data->raw(), ]; $title = 'Output of host-configuration `' . $config . '`'; } elseif ($what == 'docker') { diff --git a/src/Command/PlatformCommand.php b/src/Command/PlatformCommand.php index 98af443b..88d3ec4d 100644 --- a/src/Command/PlatformCommand.php +++ b/src/Command/PlatformCommand.php @@ -46,7 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('command', implode(' ', $input->getArgument('platform'))); try { diff --git a/src/Command/PutFileCommand.php b/src/Command/PutFileCommand.php index d0cc4bd1..cdb5e5d5 100644 --- a/src/Command/PutFileCommand.php +++ b/src/Command/PutFileCommand.php @@ -52,7 +52,7 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new \RuntimeException('Could not find file `' . $file . '`!'); } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('sourceFile', $file); $context->io()->comment('Putting file `' . $file . '` to `' . $this->getHostConfig()['configName']. '`'); diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php index a8201590..aa5f458a 100644 --- a/src/Command/ResetCommand.php +++ b/src/Command/ResetCommand.php @@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); try { $this->getMethods()->runTask('reset', $this->getHostConfig(), $context); diff --git a/src/Command/RestoreCommand.php b/src/Command/RestoreCommand.php index 312fb67d..5b3ddfa5 100644 --- a/src/Command/RestoreCommand.php +++ b/src/Command/RestoreCommand.php @@ -51,7 +51,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('what', array_map(function ($elem) { return trim(strtolower($elem)); }, $input->getArgument('what'))); diff --git a/src/Command/RestoreSqlFromFileCommand.php b/src/Command/RestoreSqlFromFileCommand.php index 030be607..b6cb6847 100644 --- a/src/Command/RestoreSqlFromFileCommand.php +++ b/src/Command/RestoreSqlFromFileCommand.php @@ -44,7 +44,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $file = $input->getArgument('file'); if (!file_exists($file)) { throw new \InvalidArgumentException('Could not find file at `' . $file . '`'); diff --git a/src/Command/ScaffoldCommand.php b/src/Command/ScaffoldCommand.php index 4c42b6d9..88f691ea 100644 --- a/src/Command/ScaffoldCommand.php +++ b/src/Command/ScaffoldCommand.php @@ -7,7 +7,6 @@ use Phabalicious\Exception\MismatchedVersionException; use Phabalicious\Exception\MissingScriptCallbackImplementation; use Phabalicious\Exception\ValidationFailedException; -use Phabalicious\Method\TaskContext; use Phabalicious\Scaffolder\Callbacks\TransformCallback; use Phabalicious\Scaffolder\Options; use Phabalicious\Utilities\PluginDiscovery; @@ -61,6 +60,7 @@ protected function configure() * @throws FabfileNotReadableException * @throws FailedShellCommandException * @throws MissingScriptCallbackImplementation + * @throws \Phabalicious\Exception\UnknownReplacementPatternException */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/src/Command/ShellCommand.php b/src/Command/ShellCommand.php index 8c3433a3..efce35ed 100644 --- a/src/Command/ShellCommand.php +++ b/src/Command/ShellCommand.php @@ -52,7 +52,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); // Allow methods to override the used shellProvider: diff --git a/src/Command/ShellCommandCommand.php b/src/Command/ShellCommandCommand.php index 3654d2b8..09e5d825 100644 --- a/src/Command/ShellCommandCommand.php +++ b/src/Command/ShellCommandCommand.php @@ -45,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); // Allow methods to override the used shellProvider: diff --git a/src/Command/SimpleExecutableInvocationCommand.php b/src/Command/SimpleExecutableInvocationCommand.php index 4846fce5..b1685727 100644 --- a/src/Command/SimpleExecutableInvocationCommand.php +++ b/src/Command/SimpleExecutableInvocationCommand.php @@ -69,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $arguments = $this->prepareArguments($input->getArgument('command-arguments')); $context->set('command', $arguments); diff --git a/src/Command/StartRemoteAccessCommand.php b/src/Command/StartRemoteAccessCommand.php index e9d50111..dd94c5cf 100644 --- a/src/Command/StartRemoteAccessCommand.php +++ b/src/Command/StartRemoteAccessCommand.php @@ -76,7 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $host_config = $this->getHostConfig(); $this->getMethods()->runTask('startRemoteAccess', $host_config, $context); diff --git a/src/Command/VariablePull.php b/src/Command/VariablePull.php index 10d8d950..d0efa89f 100644 --- a/src/Command/VariablePull.php +++ b/src/Command/VariablePull.php @@ -53,7 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('action', 'pull'); $filename = $input->getArgument('file'); diff --git a/src/Command/VariablePush.php b/src/Command/VariablePush.php index 42225478..fc04e555 100644 --- a/src/Command/VariablePush.php +++ b/src/Command/VariablePush.php @@ -50,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('action', 'push'); $filename = $input->getArgument('file'); diff --git a/src/Command/VersionCommand.php b/src/Command/VersionCommand.php index 0f878f73..ad644d38 100644 --- a/src/Command/VersionCommand.php +++ b/src/Command/VersionCommand.php @@ -40,7 +40,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return $result; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $this->getMethods()->runTask('version', $this->getHostConfig(), $context); diff --git a/src/Command/WebhookCommand.php b/src/Command/WebhookCommand.php index 2d912100..42c9a981 100644 --- a/src/Command/WebhookCommand.php +++ b/src/Command/WebhookCommand.php @@ -77,7 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $script_data = $script_data['script']; } - $context = $this->createContext($input, $output); + $context = $this->getContext(); $context->set('variables', $arguments); $context->set('webhook_name', $webhook_name); diff --git a/src/Command/WorkspaceCreateCommand.php b/src/Command/WorkspaceCreateCommand.php index 46c92ee4..21c08ec2 100644 --- a/src/Command/WorkspaceCreateCommand.php +++ b/src/Command/WorkspaceCreateCommand.php @@ -54,9 +54,9 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $context = $this->createContext($input, $output); $url = $this->scaffolder->getLocalScaffoldFile('mbb/mbb.yml'); $root_folder = empty($input->getOption('output')) ? getcwd() : $input->getOption('output'); - $context = $this->createContext($input, $output); $result = $this->scaffold($url, $root_folder, $context, [], new Options()); return $result->getExitCode(); diff --git a/src/Command/WorkspaceUpdateCommand.php b/src/Command/WorkspaceUpdateCommand.php index 41101c34..7376f8ea 100644 --- a/src/Command/WorkspaceUpdateCommand.php +++ b/src/Command/WorkspaceUpdateCommand.php @@ -39,12 +39,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $context = $this->createContext($input, $output); + $url = $this->scaffolder->getLocalScaffoldFile('mbb/mbb-update.yml'); $root_folder = $this->findRootFolder(getcwd()); if (!$root_folder) { throw new \InvalidArgumentException('Could not find multibasebox root folder!'); } - $context = $this->createContext($input, $output); $name = basename($root_folder); $root_folder = dirname($root_folder); diff --git a/src/Configuration/BlueprintConfiguration.php b/src/Configuration/BlueprintConfiguration.php index 2d4d6b5c..95bace66 100644 --- a/src/Configuration/BlueprintConfiguration.php +++ b/src/Configuration/BlueprintConfiguration.php @@ -20,18 +20,24 @@ public function __construct(ConfigurationService $service) { $this->configuration = $service; if ($data = $this->configuration->getSetting('blueprint', false)) { - $this->templates['default'] = new BlueprintTemplate($this->configuration, $data); + $this->templates['default'] = new BlueprintTemplate( + $this->configuration, + $data, + $this->configuration->getAllSettings() + ); } foreach ($this->configuration->getAllDockerConfigs() as $key => $data) { $data = $this->configuration->getDockerConfig($key); if (!empty($data['blueprint'])) { - $this->templates['docker:' . $key] = new BlueprintTemplate($this->configuration, $data['blueprint']); + $this->templates['docker:' . $key] = + new BlueprintTemplate($this->configuration, $data['blueprint'], $data->raw()); } } foreach ($this->configuration->getAllHostConfigs() as $key => $data) { if (!empty($data['blueprint'])) { - $this->templates['host:' . $key] = new BlueprintTemplate($this->configuration, $data['blueprint']); + $this->templates['host:' . $key] = + new BlueprintTemplate($this->configuration, $data['blueprint'], $data); } } } diff --git a/src/Configuration/BlueprintTemplate.php b/src/Configuration/BlueprintTemplate.php index e3cb548e..3941aed0 100644 --- a/src/Configuration/BlueprintTemplate.php +++ b/src/Configuration/BlueprintTemplate.php @@ -2,7 +2,10 @@ namespace Phabalicious\Configuration; +use Phabalicious\Exception\ValidationFailedException; use Phabalicious\Utilities\Utilities; +use Phabalicious\Validation\ValidationErrorBag; +use Phabalicious\Validation\ValidationService; class BlueprintTemplate { @@ -10,18 +13,26 @@ class BlueprintTemplate /** @var array */ private $template; + /** @var array */ + private $parent; + /** @var ConfigurationService */ private $configuration; /** * BlueprintTemplate constructor. + * * @param ConfigurationService $service * @param array $data + * @param array $parent + * + * @throws \Phabalicious\Exception\ValidationFailedException */ - public function __construct(ConfigurationService $service, $data) + public function __construct(ConfigurationService $service, array $data, array $parent) { $this->configuration = $service; $this->template = $data; + $this->parent = $parent; } public function expand($identifier) @@ -29,17 +40,25 @@ public function expand($identifier) $project_name = $this->configuration->getSetting('name', 'unknown'); $project_key = $this->configuration->getSetting('key', substr($project_name, 0, 3)); $identifier_wo_feature = str_replace('feature/', '', $identifier); + $identifier_wo_prefix = basename($identifier); + + $replacements = Utilities::expandVariables([ + 'template' => $this->template, + 'parent' => $this->parent + ]); - $replacements = []; $replacements['%identifier%'] = $identifier; $replacements['%slug%'] = Utilities::slugify($identifier); $replacements['%slug.with-hyphens%'] = Utilities::slugify($identifier, '-'); $replacements['%slug.without-feature%'] = Utilities::slugify($identifier_wo_feature); + $replacements['%slug.without-prefix%'] = Utilities::slugify($identifier_wo_prefix); + $replacements['%slug.with-hyphens.without-prefix%'] = Utilities::slugify($identifier_wo_prefix); $replacements['%slug.with-hyphens.without-feature%'] = Utilities::slugify($identifier_wo_feature, '-'); $replacements['%project-identifier%'] = $project_name; $replacements['%project-slug%'] = Utilities::slugify($project_name); $replacements['%project-slug.with-hypens%'] = Utilities::slugify($project_name, '-'); - $replacements['%project-key%'] = Utilities::slugify($project_key, ''); + $replacements['%project-slug.with-hyphens%'] = Utilities::slugify($project_name, '-'); + $replacements['%project-key%'] = Utilities::slugify($project_key); $replacements['%fabfilePath%'] = $this->configuration->getFabfilePath(); return Utilities::expandStrings($this->template, $replacements); diff --git a/src/Configuration/ConfigurationService.php b/src/Configuration/ConfigurationService.php index 81253ed3..1f4431e6 100644 --- a/src/Configuration/ConfigurationService.php +++ b/src/Configuration/ConfigurationService.php @@ -14,6 +14,8 @@ use Phabalicious\Method\MethodFactory; use Phabalicious\Method\TaskContextInterface; use Phabalicious\ShellProvider\ShellProviderFactory; +use Phabalicious\Utilities\PasswordManager; +use Phabalicious\Utilities\PasswordManagerInterface; use Phabalicious\Utilities\Utilities; use Phabalicious\Validation\ValidationErrorBag; use Phabalicious\Validation\ValidationService; @@ -54,6 +56,9 @@ class ConfigurationService private $skipCache = false; private $disallowDeepMergeForKeys = []; + /** @var \Phabalicious\Utilities\PasswordManagerInterface */ + private $passwordManager = null; + /** * @var bool */ @@ -521,6 +526,7 @@ public function getHostConfigFromBlueprint(string $blueprint, string $identifier { $cid = 'blueprint:' . $blueprint . ':' . $identifier; + if (!empty($this->cache[$cid])) { return $this->cache[$cid]; } @@ -538,6 +544,8 @@ public function getHostConfigFromBlueprint(string $blueprint, string $identifier $this->cache['host:' . $data['configName']] = $data; $this->cache[$cid] = $data; + + return $data; } @@ -567,9 +575,7 @@ private function validateHostConfig($config_name, $data) 'executables' => $this->getSetting('executables', []), 'supportsInstalls' => $type != HostType::PROD, 'supportsCopyFrom' => true, - 'backupBeforeDeploy' => in_array($type, [HostType::STAGE, HostType::PROD]) - ? true - : false, + 'backupBeforeDeploy' => in_array($type, [HostType::STAGE, HostType::PROD]), 'tmpFolder' => '/tmp', 'rootFolder' => $this->getFabfilePath(), ]; @@ -650,7 +656,13 @@ private function validateHostConfig($config_name, $data) } // Create host-config and return. - return new HostConfig($data, $shell_provider, $this); + $host_config = new HostConfig($data, $shell_provider, $this); + + if (!$this->getPasswordManager()) { + throw new \RuntimeException('No password manager found!'); + } + $this->getPasswordManager()->resolveSecrets($host_config); + return $host_config; } /** @@ -871,4 +883,26 @@ public function findScript(HostConfig $host_config, $script_name) } return $this->getSetting('scripts.' . $script_name, false); } + + /** + * @return \Phabalicious\Utilities\PasswordManagerInterface + */ + public function getPasswordManager(): PasswordManagerInterface + { + if (!$this->passwordManager) { + $this->passwordManager = new PasswordManager(); + } + return $this->passwordManager; + } + + /** + * @param \Phabalicious\Utilities\PasswordManagerInterface $passwordManager + * + * @return ConfigurationService + */ + public function setPasswordManager(PasswordManagerInterface $passwordManager): ConfigurationService + { + $this->passwordManager = $passwordManager; + return $this; + } } diff --git a/src/Configuration/HostConfig.php b/src/Configuration/HostConfig.php index b038561a..4e91ff1e 100644 --- a/src/Configuration/HostConfig.php +++ b/src/Configuration/HostConfig.php @@ -145,4 +145,9 @@ public function setProperty(string $key, string $value) { Utilities::setProperty($this->data, $key, $value); } + + public function setData($data) + { + $this->data = $data; + } } diff --git a/src/Exception/UnknownSecretException.php b/src/Exception/UnknownSecretException.php new file mode 100644 index 00000000..8f32458f --- /dev/null +++ b/src/Exception/UnknownSecretException.php @@ -0,0 +1,7 @@ +getPasswordManager()->getPasswordFor($ftp['host'], $ftp['port'], $ftp['user']); + $pw = $context->getPasswordManager(); + $ftp['password'] = $pw->getPasswordFor($pw->getKeyFromLogin($ftp['host'], $ftp['port'], $ftp['user'])); $host_config[self::PREFS_KEY] = $ftp; } diff --git a/src/Method/TaskContext.php b/src/Method/TaskContext.php index f2297295..4c401dcc 100644 --- a/src/Method/TaskContext.php +++ b/src/Method/TaskContext.php @@ -57,7 +57,7 @@ public function mergeAndSet(string $key, array $value) $stored_value = Utilities::mergeData($stored_value, $value); $this->set($key, $stored_value); } - + public function get(string $key, $default = null) { return isset($this->data[$key]) ? $this->data[$key] : $default; @@ -81,6 +81,10 @@ public function getOutput(): ?OutputInterface public function setConfigurationService(?ConfigurationService $service) { $this->configurationService = $service; + if ($this->configurationService) { + $this->configurationService->getPasswordManager() + ->setContext($this); + } } public function getConfigurationService(): ConfigurationService @@ -172,11 +176,7 @@ public function askQuestion(string $question) public function getPasswordManager() { - if (!$this->passwordManager) { - $this->passwordManager = new PasswordManager($this); - } - - return $this->passwordManager; + return $this->getConfigurationService()->getPasswordManager(); } /** @@ -189,7 +189,7 @@ public function io() } return $this->io; } - + public function setIo(SymfonyStyle $io) { $this->io = $io; diff --git a/src/Utilities/PasswordManager.php b/src/Utilities/PasswordManager.php index 7c38a4c4..30ade96e 100644 --- a/src/Utilities/PasswordManager.php +++ b/src/Utilities/PasswordManager.php @@ -3,7 +3,10 @@ namespace Phabalicious\Utilities; +use Phabalicious\Configuration\HostConfig; +use Phabalicious\Exception\UnknownSecretException; use Phabalicious\Method\TaskContextInterface; +use Phabalicious\Utilities\Questions\Question; use Symfony\Component\Yaml\Yaml; class PasswordManager implements PasswordManagerInterface @@ -13,29 +16,37 @@ class PasswordManager implements PasswordManagerInterface private $passwords; - public function __construct(TaskContextInterface $context) + private $questionFactory = null; + + public function __construct() { - $this->context = $context; $this->readPasswords(); } - public function getPasswordFor(string $host, int $port, string $user) + public function getPasswordFor(string $key) { - $key = $this->getKey($host, $port, $user); if (!empty($this->passwords[$key])) { return $this->passwords[$key]; } - $pw = $this->context->askQuestion(sprintf('Please provide a password for `%s@%s`: ', $user, $host)); + $pw = $this->context->askQuestion(sprintf('Please provide a secret for `%s`: ', $key)); $this->passwords[$key] = $pw; return $pw; } - private function getKey($host, $port, $user) + public function getKeyFromLogin($host, $port, $user): string { return sprintf('%s@%s:%s', $user, $host, $port); } + public function getQuestionFactory(): QuestionFactory + { + if (!$this->questionFactory) { + $this->questionFactory = new QuestionFactory(); + } + return $this->questionFactory; + } + private function readPasswords() { $file = getenv("HOME"). '/.phabalicious-credentials'; @@ -51,4 +62,92 @@ private function readPasswords() $this->passwords = $data; } + + /** + * @return TaskContextInterface + */ + public function getContext(): TaskContextInterface + { + return $this->context; + } + + /** + * @param TaskContextInterface $context + * + * @return PasswordManager + */ + public function setContext(TaskContextInterface $context): PasswordManagerInterface + { + $this->context = $context; + return $this; + } + + public function resolveSecrets(HostConfig $host_config) + { + $replacements = []; + $this->resolveSecretsImpl($host_config->raw(), $replacements); + $data = Utilities::expandStrings($host_config->raw(), $replacements); + $host_config->setData($data); + } + + private function resolveSecretsImpl(array $data, array &$replacements) + { + foreach ($data as $key => $value) { + if (is_array($value)) { + $this->resolveSecretsImpl($value, $replacements); + } elseif ($secret_key = $this->containsSecret($value)) { + $replacements['%secret.' . $secret_key . '%'] = $this->getSecret($secret_key); + } + } + } + + private function containsSecret(string $string) + { + $matches = []; + if (preg_match("/%secret\.(.*)%/", $string, $matches)) { + return $matches[1]; + } + return false; + } + + private function getSecret($secret) + { + $secrets = $this + ->getContext() + ->getConfigurationService() + ->getSetting('secrets', []); + if (!isset($secrets[$secret])) { + throw new UnknownSecretException("Could not find secret `$secret` in config!"); + } + + if (isset($this->passwords[$secret])) { + return $this->passwords[$secret]; + } + + $secret_data = $secrets[$secret]; + + $env_name = !empty($secret_data['env']) ? $secret_data['env'] : Utilities::toUpperSnakeCase($secret); + if ($value = getenv($env_name)) { + return $value; + } + + $args = $this->getContext()->getInput()->getOption('secret'); + foreach ($args as $p) { + [$key, $value] = explode('=', $p); + if ($key == $secret) { + return $value; + } + } + + // Still no match, ask for it! + + if (!$this->context) { + throw new \RuntimeException("Cant resolve secrets as no valid context is available!"); + } + + $pw = $this->getQuestionFactory()->askAndValidate($this->getContext()->io(), $secret_data, null); + + $this->passwords[$secret] = $pw; + return $pw; + } } diff --git a/src/Utilities/PasswordManagerInterface.php b/src/Utilities/PasswordManagerInterface.php index ecef435d..883f51dc 100644 --- a/src/Utilities/PasswordManagerInterface.php +++ b/src/Utilities/PasswordManagerInterface.php @@ -2,7 +2,18 @@ namespace Phabalicious\Utilities; +use Phabalicious\Configuration\HostConfig; +use Phabalicious\Method\TaskContextInterface; + interface PasswordManagerInterface { - public function getPasswordFor(string $host, int $port, string $user); + public function getContext(): TaskContextInterface; + + public function setContext(TaskContextInterface $context): PasswordManagerInterface; + + public function getKeyFromLogin($host, $port, $user); + + public function getPasswordFor(string $key); + + public function resolveSecrets(HostConfig $host_config); } diff --git a/src/Utilities/Utilities.php b/src/Utilities/Utilities.php index 577c35c8..088ca204 100644 --- a/src/Utilities/Utilities.php +++ b/src/Utilities/Utilities.php @@ -16,6 +16,16 @@ class Utilities const COMBINED_ARGUMENTS = 'combined'; const UNNAMED_ARGUMENTS = 'unnamedArguments'; + /** + * Merge two arrays, elements of override_data will replace elements in data. + * + * Plain arrays, which are not associcative will get replaced, instead of merged. + * + * @param array $data + * @param array $override_data + * + * @return array + */ public static function mergeData(array $data, array $override_data): array { $result = $data; @@ -38,6 +48,13 @@ public static function mergeData(array $data, array $override_data): array return $result; } + /** + * Expand variables. Will create an array with replacement strings as key and their value + * + * @param array $variables + * + * @return array + */ public static function expandVariables(array $variables): array { $result = []; @@ -51,6 +68,13 @@ public static function expandVariables(array $variables): array return $result; } + /** + * Implementation details for expandVariables. + * + * @param string $prefix + * @param array $variables + * @param array $result + */ private static function expandVariablesImpl(string $prefix, array $variables, array &$result) { foreach ($variables as $key => $value) { @@ -516,4 +540,9 @@ public static function camel2dashed($string) { return strtolower(preg_replace('/([A-Z])/', '-$1', $string)); } + + public static function toUpperSnakeCase($string) + { + return strtoUpper(str_replace('-', '_', self::camel2dashed($string))); + } } diff --git a/tests/ConfigurationServiceTest.php b/tests/ConfigurationServiceTest.php index aca79a27..01f7dfe3 100644 --- a/tests/ConfigurationServiceTest.php +++ b/tests/ConfigurationServiceTest.php @@ -8,6 +8,7 @@ use Phabalicious\Method\MethodFactory; use Phabalicious\Method\ScriptMethod; use Phabalicious\Method\SshMethod; +use Phabalicious\Utilities\PasswordManager; use Phabalicious\Utilities\TestableLogger; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; @@ -45,6 +46,7 @@ public function setUp() ->will($this->returnValue([])); $this->config->setMethodFactory($method_factory); + $this->config->setPasswordManager($this->getMockBuilder(PasswordManager::class)->getMock()); } public function testCustomFabfile() diff --git a/tests/SecretsTest.php b/tests/SecretsTest.php new file mode 100644 index 00000000..b30b20e1 --- /dev/null +++ b/tests/SecretsTest.php @@ -0,0 +1,125 @@ +application = new Application(); + $this->application->setVersion(Utilities::FALLBACK_VERSION); + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + $configuration = new ConfigurationService($this->application, $logger); + $method_factory = new MethodFactory($configuration, $logger); + $method_factory->addMethod(new LocalMethod($logger)); + $method_factory->addMethod(new ScriptMethod($logger)); + + $configuration->readConfiguration($this->getcwd() . '/assets/secret-tests/fabfile.yaml'); + + $this->application->add(new OutputCommand($configuration, $method_factory)); + } + + + public function testSecretsBlueprintAsArguments() + { + + $command = $this->application->find('output'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--blueprint' => 'test', + '--what' => 'host', + '--config' => 'testBlueprint', + '--secret' => [ 'mysql-password=top_Secret'] + )); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('top_Secret', $output); + $this->assertStringContainsString('123--top_Secret--321', $output); + $this->assertStringNotContainsString('%secret.mysql-password', $output); + } + + public function testSecretsAsArguments() + { + + $command = $this->application->find('output'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--what' => 'host', + '--config' => 'testHost', + '--secret' => [ 'mysql-password=top_Secret'] + )); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('top_Secret', $output); + $this->assertStringContainsString('123--top_Secret--321', $output); + $this->assertStringNotContainsString('%secret.mysql-password', $output); + } + + public function testSecretsAsCustomEnvironmentVar() + { + + putenv("MARIADB_PASSWORD=top_Secret"); + $command = $this->application->find('output'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--what' => 'host', + '--config' => 'testHost', + )); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('top_Secret', $output); + $this->assertStringContainsString('123--top_Secret--321', $output); + $this->assertStringNotContainsString('%secret.mysql-password', $output); + } + + public function testSecretsAsEnvironmentVar() + { + + putenv("SMTP_PASSWORD=top_Secret"); + $command = $this->application->find('output'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--what' => 'host', + '--config' => 'testEnv', + )); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('top_Secret', $output); + $this->assertStringContainsString('123--top_Secret--321', $output); + $this->assertStringNotContainsString('%secret.smtp-password', $output); + } + + public function testUnknownSecret() + { + + $this->expectException("Phabalicious\Exception\UnknownSecretException"); + $command = $this->application->find('output'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--what' => 'host', + '--config' => 'testUnknownSecret', + '--secret' => [ 'mysql-password=top_Secret'] + )); + + $output = $commandTester->getDisplay(); + } +} diff --git a/tests/WebhookCommandTest.php b/tests/WebhookCommandTest.php index 89d1e845..df96116d 100644 --- a/tests/WebhookCommandTest.php +++ b/tests/WebhookCommandTest.php @@ -77,10 +77,10 @@ public function testListWebhookCommand() $output = $commandTester->getDisplay(); - $this->assertContains('testGet', $output); - $this->assertContains('testDelete', $output); - $this->assertContains('testPost', $output); - $this->assertNotContains('defaults', $output); + $this->assertStringContainsString('testGet', $output); + $this->assertStringContainsString('testDelete', $output); + $this->assertStringContainsString('testPost', $output); + $this->assertStringNotContainsString('defaults', $output); } public function testWebhookWithArgumentsCommand() @@ -97,7 +97,7 @@ public function testWebhookWithArgumentsCommand() $output = $commandTester->getDisplay(); - $this->assertContains('"args":{"q":"hello-from-commandline"}', $output); + $this->assertStringContainsString('"args":{"q":"hello-from-commandline"}', $output); } public function testTaskSpecificWebhooks() @@ -111,9 +111,9 @@ public function testTaskSpecificWebhooks() $output = $commandTester->getDisplay(); - $this->assertContains('[test2Get]', $output); - $this->assertContains('[testArguments]', $output); - $this->assertContains('"args":{"q":"foo"}', $output); - $this->assertContains('factorial-screensaver', $output); + $this->assertStringContainsString('[test2Get]', $output); + $this->assertStringContainsString('[testArguments]', $output); + $this->assertStringContainsString('"args":{"q":"foo"}', $output); + $this->assertStringContainsString('factorial-screensaver', $output); } } diff --git a/tests/assets/secret-tests/fabfile.yaml b/tests/assets/secret-tests/fabfile.yaml new file mode 100644 index 00000000..adc6b575 --- /dev/null +++ b/tests/assets/secret-tests/fabfile.yaml @@ -0,0 +1,43 @@ +name: secret-tests + +secrets: + mysql-password: + question: What password is mysql using + env: MARIADB_PASSWORD + + smtp-password: + question: What password is mtp using + + +hosts: + base: + type: dev + needs: + - local + + testHost: + inheritsFrom: base + database: + password: "%secret.mysql-password%" + password_combined: "123--%secret.mysql-password%--321" + + testEnv: + inheritsFrom: base + smtp: + password: "%secret.smtp-password%" + password_combined: "123--%secret.smtp-password%--321" + + testUnknownSecret: + inheritsFrom: base + database: + password: "%secret.unknown-password%" + password_combined: "123--%secret.mysql-password%--321" + + testBlueprint: + inheritsFrom: base + blueprint: + inheritsFrom: testBlueprint + configName: "%identifier%" + database: + password: "%secret.mysql-password%" + password_combined: "123--%secret.mysql-password%--321" diff --git a/tests/assets/shell-command-options-tests/fabfile.yaml b/tests/assets/shell-command-options-tests/fabfile.yaml index 3e942293..164aa6f7 100644 --- a/tests/assets/shell-command-options-tests/fabfile.yaml +++ b/tests/assets/shell-command-options-tests/fabfile.yaml @@ -41,6 +41,9 @@ hosts: docker-exec-over-ssh-shell: inheritsFrom: docker-exec-shell shellProvider: docker-exec-over-ssh + shellProviderOptions: + - -i + - /home/stephan/multibasebox/projects/phabalicious/tests/assets/shell-command-options-tests/testruns user: stephan port: 22 host: localhost