/// <inheritdoc/> public override void Run(CommandLine commandLine) { if (commandLine.HasHelpOption) { Console.WriteLine(usage); Program.Exit(0); } if (commandLine.Arguments.Length < 2) { Console.WriteLine(usage); Program.Exit(1); } if (Environment.GetEnvironmentVariable("NEON_RUN_ENV") != null) { Console.Error.WriteLine("*** ERROR: [neon run ...] cannot be executed recursively."); Program.Exit(1); } var commandSplit = Program.CommandLine.Split(); var leftCommandLine = commandSplit.Left.Shift(1); var rightCommandLine = commandSplit.Right; if (rightCommandLine == null || rightCommandLine.Arguments.Length == 0) { Console.Error.WriteLine("*** ERROR: Expecting a [--] argument followed by a shell command."); Program.Exit(1); } var orgDirectory = Directory.GetCurrentDirectory(); var runFolder = Path.Combine(HiveHelper.GetRunFolder(), Guid.NewGuid().ToString("D")); var runEnvPath = Path.Combine(runFolder, "__runenv.txt"); var exitCode = 1; try { // Create the temporary run folder and make it the current directory. Directory.CreateDirectory(runFolder); // We need to load variables from any files specified on the command line, // decrypting them as required. var allVars = new Dictionary <string, string>(StringComparer.InvariantCultureIgnoreCase); if (leftCommandLine.Arguments.Length > 0) { bool askVaultPass = leftCommandLine.HasOption("--ask-vault-pass"); string tempPasswordPath = null; string passwordName = null; try { if (askVaultPass) { // Note that [--ask-vault-pass] takes presidence over [--vault-password-file]. var password = NeonHelper.ReadConsolePassword("Vault password: "******"D"); tempPasswordPath = Path.Combine(passwordsFolder, $"{guid}.tmp"); passwordName = Path.GetFileName(tempPasswordPath); File.WriteAllText(tempPasswordPath, password); } else { passwordName = leftCommandLine.GetOption("--vault-password-file"); } if (!string.IsNullOrEmpty(passwordName)) { AnsibleCommand.VerifyPassword(passwordName); } // Decrypt the variables files, add the variables to the environment // and also to the [allVars] dictionary which we'll use below to // create the run variables file. foreach (var varFile in leftCommandLine.Arguments) { var varContents = File.ReadAllText(varFile); if (varContents.StartsWith("$ANSIBLE_VAULT;")) { // The variable file is encrypted so we're going recursively invoke // the following command to decrypt it: // // neon ansible vault view -- --vault-password=NAME VARS-PATH // // This uses the password to decrypt the variables to STDOUT. if (string.IsNullOrEmpty(passwordName)) { Console.Error.WriteLine($"*** ERROR: [{varFile}] is encrypted. Use [--ask-vault-pass] or [--vault-password-file] to specify the password."); Program.Exit(1); } var result = Program.ExecuteRecurseCaptureStreams( new object[] { "ansible", "vault", "--", "view", $"--vault-password-file={passwordName}", varFile }); if (result.ExitCode != 0) { Console.Error.Write(result.AllText); Program.Exit(result.ExitCode); } varContents = NeonHelper.StripAnsibleWarnings(result.OutputText); } // [varContents] now holds the decrypted variables formatted as YAML. // We're going to parse this and set the appropriate environment // variables. // // Note that we're going to ignore variables with multi-line values. var yaml = new YamlStream(); var vars = new List <KeyValuePair <string, string> >(); try { yaml.Load(varContents); } catch (Exception e) { throw new HiveException($"Unable to parse YAML from decrypted [{varFile}]: {NeonHelper.ExceptionError(e)}", e); } if (yaml.Documents.FirstOrDefault() != null) { ParseYamlVariables(vars, (YamlMappingNode)yaml.Documents.First().RootNode); } foreach (var variable in vars) { if (variable.Value != null && variable.Value.Contains('\n')) { continue; // Ignore variables with multi-line values. } allVars[variable.Key] = variable.Value; Environment.SetEnvironmentVariable(variable.Key, variable.Value); } } } finally { if (tempPasswordPath != null && File.Exists(tempPasswordPath)) { File.Delete(tempPasswordPath); // Don't need this any more. } } } // We need to generate the NEON_RUN_ENV file defining the environment variables // loaded by the command. This file format is compatible with the Docker // [run] command's [--env-file=PATH] option and will be used by nested calls to // [neon] to pass these variables through to the tool container as required. Environment.SetEnvironmentVariable("NEON_RUN_ENV", runEnvPath); using (var runEnvWriter = new StreamWriter(runEnvPath, false, Encoding.UTF8)) { foreach (var item in allVars) { runEnvWriter.WriteLine($"{item.Key}={item.Value}"); } } // Execute the command in the appropriate shell for the current workstation. var sbCommand = new StringBuilder(); foreach (var arg in rightCommandLine.Items) { if (sbCommand.Length > 0) { sbCommand.Append(' '); } if (arg.Contains(' ')) { sbCommand.Append("\"" + arg + "\""); } else { sbCommand.Append(arg); } } exitCode = NeonHelper.ExecuteShell(sbCommand.ToString()); } finally { // Restore the current directory. Directory.SetCurrentDirectory(orgDirectory); // Cleanup if (Directory.Exists(runFolder)) { Directory.Delete(runFolder, true); } } Program.Exit(exitCode); }