/// <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); }
/// <inheritdoc/> public override void Run(CommandLine commandLine) { if (HiveHelper.InToolContainer) { Console.Error.WriteLine("*** ERROR: [file] commands cannot be run inside a Docker container."); Program.Exit(1); } if (commandLine.Arguments.Length == 0 || commandLine.HasHelpOption) { Help(); Program.Exit(0); } // Parse the arguments. var command = commandLine.Arguments.ElementAtOrDefault(0); var source = string.Empty; var target = string.Empty; var passwordName = string.Empty; switch (command) { // These commands accept two parameters. case "create": case "edit": case "view": target = commandLine.Arguments.ElementAtOrDefault(1); passwordName = commandLine.Arguments.ElementAtOrDefault(2); if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine("*** ERROR: PATH argument is missing."); Program.Exit(1); } if (string.IsNullOrEmpty(passwordName)) { Console.Error.WriteLine("*** ERROR: PASSWORD-NAME argument is missing."); Program.Exit(1); } break; // These commands accept three parameters. case "encrypt": case "decrypt": source = commandLine.Arguments.ElementAtOrDefault(1); target = commandLine.Arguments.ElementAtOrDefault(2); passwordName = commandLine.Arguments.ElementAtOrDefault(3); if (string.IsNullOrEmpty(source)) { Console.Error.WriteLine("*** ERROR: SOURCE argument is missing."); Program.Exit(1); } if (string.IsNullOrEmpty(target)) { Console.Error.WriteLine("*** ERROR: TARGET argument is missing."); Program.Exit(1); } if (string.IsNullOrEmpty(passwordName)) { Console.Error.WriteLine("*** ERROR: PASSWORD-NAME argument is missing."); Program.Exit(1); } break; default: Console.Error.WriteLine($"*** ERROR: Unexpected [{command}] command."); Program.Exit(1); break; } var editor = commandLine.GetOption("--editor", "nano"); switch (editor.ToLowerInvariant()) { case "nano": Environment.SetEnvironmentVariable("EDITOR", "/bin/nano"); break; case "vim": Environment.SetEnvironmentVariable("EDITOR", "/usr/bin/vim"); break; case "vi": Environment.SetEnvironmentVariable("EDITOR", "/usr/bin/vi"); break; default: Console.Error.WriteLine($"*** ERROR: [--editor={editor}] does not specify a known editor. Specify one of: NANO, VIM, or VI."); Program.Exit(1); break; } // Ensure that the password file actually exists. Covenant.Assert(!string.IsNullOrEmpty(passwordName)); if (!File.Exists(Path.Combine(HiveHelper.GetAnsiblePasswordsFolder(), passwordName))) { Console.Error.WriteLine($"*** ERROR: Password file for [{passwordName}] does not exist."); Program.Exit(1); } // $note(jeff.lill): // // I tried to call [Program.ExecuteRecurse()] here to recurse into // the [neon vault -- COMMAND --vault-password-file=NAME PATH] commands // but it didn't work for [edit]. It looks like the command did run but // then gets stuck. I could have sworn that I had this working at one // point but I can't get it working again. I think the standard // I/O streams being redirect might be confusing Docker and Ansible, // since Ansible needs to access the Docker TTY. // // The [view] command was also a bit wonky. For example, two blank // lines in the encrypted file would be returned as only a single // blank line. // // The (not so bad) workaround is to simply recurse into // [Program.Main()]. It's a little sloppy but should be OK // (and will be faster to boot). I'm going to do this for // all of the commands. switch (command) { case "create": Program.Main( new string[] { "ansible", "vault", "--", "create", $"--vault-password-file={passwordName}", target }); break; case "decrypt": File.Copy(source, target, overwrite: true); Program.Main( new string[] { "ansible", "vault", "--", "decrypt", $"--vault-password-file={passwordName}", target }); break; case "edit": Program.Main( new string[] { "ansible", "vault", $"--editor={editor}", "--", "edit", $"--vault-password-file={passwordName}", target }); break; case "encrypt": File.Copy(source, target, overwrite: true); Program.Main( new string[] { "ansible", "vault", "--", "encrypt", $"--vault-password-file={passwordName}", target }); break; case "view": Program.Main( new string[] { "ansible", "vault", "--", "view", $"--vault-password-file={passwordName}", target }); break; default: Console.Error.WriteLine($"*** ERROR: Unexpected [{command}] command."); Program.Exit(1); break; } }
/// <inheritdoc/> public override void Run(CommandLine commandLine) { if (commandLine.Arguments.Length == 0 || commandLine.HasHelpOption) { Help(); Program.Exit(0); } string folder = commandLine.Arguments.First(); string path; switch (folder.ToLowerInvariant()) { case "ansible-roles": path = HiveHelper.GetAnsibleRolesFolder(); break; case "ansible-vault": path = HiveHelper.GetAnsiblePasswordsFolder(); break; case "logins": path = HiveHelper.GetLoginFolder(); break; case "setup": path = HiveHelper.GetVmTemplatesFolder(); break; case "temp": path = Program.HiveTempFolder; break; case "vpn": path = HiveHelper.GetVpnFolder(); break; default: Console.Error.WriteLine($"*** ERROR: Unexpected folder [{commandLine.Arguments.First()}]."); Program.Exit(1); return; } if (commandLine.HasOption("--open")) { if (NeonHelper.IsWindows) { Process.Start(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "explorer.exe"), $"\"{path}\""); } else if (NeonHelper.IsOSX) { throw new NotImplementedException("$todo(jeff.lill): Implement this for OSX."); } else { throw new NotSupportedException("[--open] option is not supported on this platform."); } } else { Console.Write(path); } }