public void CopyFolder() { using (var tempFolder = new TempFolder()) { var sourceFolder = Path.Combine(tempFolder.Path, "source"); var sourceSubFolder = Path.Combine(sourceFolder, "subfolder"); Directory.CreateDirectory(sourceFolder); File.WriteAllText(Path.Combine(sourceFolder, "test1.txt"), "test1"); File.WriteAllText(Path.Combine(sourceFolder, "test2.txt"), "test2"); Directory.CreateDirectory(sourceSubFolder); File.WriteAllText(Path.Combine(sourceSubFolder, "test3.txt"), "test3"); var targetFolder = Path.Combine(tempFolder.Path, "target"); NeonHelper.CopyFolder(sourceFolder, targetFolder); Assert.True(Directory.Exists(targetFolder)); Assert.True(Directory.Exists(Path.Combine(targetFolder, "subfolder"))); Assert.Equal("test1", File.ReadAllText(Path.Combine(targetFolder, "test1.txt"))); Assert.Equal("test2", File.ReadAllText(Path.Combine(targetFolder, "test2.txt"))); Assert.Equal("test3", File.ReadAllText(Path.Combine(targetFolder, "subfolder", "test3.txt"))); } }
/// <summary> /// Implements the <b>publish-folder</b> command. /// </summary> /// <param name="commandLine">The command line.</param> public static void PublishFolder(CommandLine commandLine) { commandLine = commandLine.Shift(1); if (commandLine.Arguments.Length != 2) { Console.WriteLine(usage); Program.Exit(1); } var sourceFolder = commandLine.Arguments[0]; var targetFolder = commandLine.Arguments[1]; Console.WriteLine($"neon-build publish-folder: {sourceFolder} --> {targetFolder}"); if (!Directory.Exists(sourceFolder)) { Console.Error.WriteLine($"*** ERROR: [SOURCE-FOLDER={sourceFolder}] does not exist!"); Program.Exit(1); } if (Directory.Exists(targetFolder)) { Directory.Delete(targetFolder, recursive: true); } NeonHelper.CopyFolder(sourceFolder, targetFolder); }
/// <inheritdoc/> public override async Task RunAsync(CommandLine commandLine) { if (commandLine.HasHelpOption || commandLine.Arguments.Length == 0) { Console.WriteLine(usage); Program.Exit(0); } var sourceFolder = commandLine.Arguments.ElementAtOrDefault(0); var outputPath = commandLine.Arguments.ElementAtOrDefault(1); var sbGccArgs = new StringBuilder(); if (string.IsNullOrEmpty(sourceFolder) || string.IsNullOrEmpty(outputPath)) { Console.WriteLine(usage); Program.Exit(1); } foreach (var arg in commandLine.Arguments.Skip(2)) { sbGccArgs.AppendWithSeparator(arg); } // We're going to build this within the distro at [/tmp/wsl-util/GUID] by // recursively copying the contents of SOURCE-FOLDER to this directory, // running GCC to build the thing, passing [*.c] to include all of the C // source files and generating the binary as [output.bin] with the folder. // // We're also going to clear the [/tmp/wsl-util] folder first to ensure that we don't // accumulate any old build files over time and we'll also ensure that // [gcc] is installed. var defaultDistro = Wsl2Proxy.GetDefault(); if (string.IsNullOrEmpty(defaultDistro)) { Console.Error.WriteLine("*** ERROR: There is no default WSL2 distro."); Program.Exit(1); } var distro = new Wsl2Proxy(defaultDistro); if (!distro.IsDebian) { Console.Error.WriteLine($"*** ERROR: Your default WSL2 distro [{distro.Name}] is running: {distro.OSRelease["ID"]}/{distro.OSRelease["ID_LIKE"]}"); Console.Error.WriteLine($" The CRI-O build requires an Debian/Ubuntu based distribution."); Program.Exit(1); } var linuxUtilFolder = LinuxPath.Combine("/", "tmp", "wsl-util"); var linuxBuildFolder = LinuxPath.Combine(linuxUtilFolder, Guid.NewGuid().ToString("d")); var linuxOutputPath = LinuxPath.Combine(linuxBuildFolder, "output.bin"); var windowsUtilFolder = distro.ToWindowsPath(linuxUtilFolder); var windowsBuildFolder = distro.ToWindowsPath(linuxBuildFolder); try { // Delete the [/tmp/wsl-util] folder on Linux and the copy the // source from the Windows side into a fresh distro folder. NeonHelper.DeleteFolder(windowsUtilFolder); NeonHelper.CopyFolder(sourceFolder, windowsBuildFolder); // Install [safe-apt-get] if it's not already present. We're using this // because it's possible that projects build in parallel and it's possible // that multiple GCC commands could also be running in parallel. var linuxSafeAptGetPath = "/usr/bin/safe-apt-get"; var windowsSafeAptGetPath = distro.ToWindowsPath(linuxSafeAptGetPath); if (!File.Exists(windowsSafeAptGetPath)) { var resources = Assembly.GetExecutingAssembly().GetResourceFileSystem("WslUtil.Resources"); var toolScript = resources.GetFile("/safe-apt-get.sh").ReadAllText(); // Note that we need to escape all "$" characters in the script // so the upload script won't attempt to replace any variables // (with blanks). toolScript = toolScript.Replace("$", "\\$"); var uploadScript = $@" cat <<EOF > {linuxSafeAptGetPath} {toolScript} EOF chmod 754 {linuxSafeAptGetPath} "; distro.SudoExecuteScript(uploadScript).EnsureSuccess(); } // Perform the build. var buildScript = $@" set -euo pipefail safe-apt-get install -yq gcc cd {linuxBuildFolder} gcc *.c -o {linuxOutputPath} {sbGccArgs} "; distro.SudoExecuteScript(buildScript).EnsureSuccess(); // Copy the build output to the Windows output path. Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); NeonHelper.DeleteFile(outputPath); File.Copy(distro.ToWindowsPath(linuxOutputPath), outputPath); } finally { // Remove the temporary distro folder. NeonHelper.DeleteFolder(windowsBuildFolder); } await Task.CompletedTask; }
/// <summary> /// Configures Varnish based on the current traffic manager configuration. /// </summary> /// <remarks> /// This method will terminate the service if Varnish could not be started /// for the first call. /// </remarks> public async static Task ConfigureVarnish() { try { // Retrieve the configuration HASH and compare that with what // we have already deployed. log.LogInfo(() => $"VARNISH-SHIM: Retrieving configuration HASH from Consul path [{configHashKey}]."); string configHash; try { configHash = await consul.KV.GetString(configHashKey, terminator.CancellationToken); } catch (OperationCanceledException) { return; } catch (Exception e) { SetErrorTime(); log.LogError($"VARNISH-SHIM: Cannot retrieve [{configHashKey}] from Consul.", e); return; } if (configHash == deployedHash) { log.LogInfo(() => $"VARNISH-SHIM: Configuration with [hash={configHash}] is already deployed."); return; } else { log.LogInfo(() => $"VARNISH-SHIM: Configuration hash has changed from [{deployedHash}] to [{configHash}]."); } // Download the configuration archive from Consul and extract it to // the new configuration directory (after ensuring that the directory // has been cleared). log.LogInfo(() => $"VARNISH-SHIM: Retrieving configuration ZIP archive from Consul path [{configKey}]."); byte[] zipBytes; try { zipBytes = await consul.KV.GetBytes(configKey, terminator.CancellationToken); } catch (OperationCanceledException) { return; } catch (Exception e) { SetErrorTime(); log.LogError($"VARNISH-SHIM: Cannot retrieve [{configKey}] from Consul.", e); return; } if (configHash == deployedHash) { log.LogInfo(() => $"VARNISH-SHIM: Configuration with [hash={configHash}] is already deployed."); return; } var zipPath = Path.Combine(configUpdateFolder, "haproxy.zip"); log.LogInfo(() => $"VARNISH-SHIM: Extracting ZIP archive to [{configUpdateFolder}]."); // Ensure that we have a fresh update folder. NeonHelper.DeleteFolder(configUpdateFolder); Directory.CreateDirectory(configUpdateFolder); // Unzip the configuration archive to the update folder. File.WriteAllBytes(zipPath, zipBytes); var response = NeonHelper.ExecuteCapture("unzip", new object[] { "-o", zipPath, "-d", configUpdateFolder }); response.EnsureSuccess(); // It's possible that very old versions of [neon-proxy-manager] haven't // included a generated [varnish.vcl] file within the ZIP archive. We'll // create a stub (do-nothing) file in this case to make Varnish happy. if (!File.Exists(configUpdatePath)) { const string stubVcl = @"vcl 4.0; # The proxy configuration archive did not include a [varnish.vcl] file so # we'll use this stub VCL file that doesn't do anything. backend stub { .host = ""localhost""; .port = ""8080""; } "; File.WriteAllText(configUpdatePath, NeonHelper.ToLinuxLineEndings(stubVcl)); } // Compare the VCL just downloaded with that we saved from the last update // (if this isn't the first). If the two programs are the same then we // don't need to do anything. This can happen if the HAProxy config changed // but the Varnish config didn't. var newVCL = File.ReadAllText(configUpdatePath); if (lastVcl != null) { if (lastVcl == newVCL) { log.LogInfo(() => "VARNISH-SHIM: VCL is unchanged. No need to update Varnish."); return; } } // Verify the configuration. log.LogInfo(() => "VARNISH-SHIM: Verifying Varnish configuration."); var verifyWorkDir = "/tmp/verify"; response = NeonHelper.ExecuteCapture("varnishd", new object[] { "-C", "-f", configUpdatePath, "-n", verifyWorkDir, "-a", "127.0.0.2:9090" // Avoid conflicting with the production instance via an internal address/port }); NeonHelper.DeleteFolder(verifyWorkDir); if (response.ExitCode == 0) { log.LogInfo(() => "VARNISH-SHIM: Configuration is OK."); } else { SetErrorTime(); // If Varnish is running then we'll let it continue using // the out-of-date configuration as a fail-safe. If it's not // running, we're going to terminate the service. if (!GetVarnishProcessIds().IsEmpty()) { log.LogError(() => $"VARNISH-SHIM: Invalid Varnish configuration: {response.AllText}."); log.LogError(() => $"VARNISH-SHIM: Using out-of-date configuration as a fail-safe."); } else { log.LogCritical(() => $"VARNISH-SHIM: Invalid Varnish configuration: {response.AllText}."); log.LogCritical(() => "VARNISH-SHIM: Terminating service."); Program.Exit(1); return; } } // Purge the contents of the [configFolder] and copy the contents // of [configUpdateFolder] into it. NeonHelper.DeleteFolder(configFolder); Directory.CreateDirectory(configFolder); NeonHelper.CopyFolder(configUpdateFolder, configFolder); // Start Varnish if it's not already running. if (GetVarnishProcessIds().IsEmpty()) { log.LogInfo(() => $"VARNISH-SHIM: Starting Vanish."); response = NeonHelper.ExecuteCapture("varnishd", new object[] { "-f", configPath, "-s", $"malloc,{memoryLimit}", "-T", AdminInterface, "-a", "0.0.0.0:80", "-n", workDir }); if (response.ExitCode == 0) { log.LogInfo(() => $"VARNISH-SHIM: Varnish has started."); // Update the deployed hash so we won't try to update the same // configuration again. deployedHash = configHash; } else { log.LogCritical(() => $"VARNISH-SHIM: Cannot start Varnish: {response.ErrorText}"); Program.Exit(1); return; } } // Update the Varnish config. log.LogInfo(() => $"VARNISH-SHIM: Updating Varnish."); var oldVclProgram = vclVersion == 0 ? "boot" : $"main-{vclVersion}"; var newVclProgram = $"main-{++vclVersion}"; try { log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.load {newVclProgram} {configPath}"); response = NeonHelper.ExecuteCapture("varnishadm", new object[] { "-n", workDir, "vcl.load", newVclProgram, configPath }); response.EnsureSuccess(); // Activate the new VCL. log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.use {newVclProgram}"); response = NeonHelper.ExecuteCapture("varnishadm", new object[] { "-n", workDir, "vcl.use", newVclProgram }); response.EnsureSuccess(); // Remove the previous VCL program. log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.discard {newVclProgram}"); response = NeonHelper.ExecuteCapture("varnishadm", new object[] { "-n", workDir, "vcl.discard", oldVclProgram }); response.EnsureSuccess(); // Update the deployed hash so we won't try to update the same // configuration again. deployedHash = configHash; lastVcl = newVCL; log.LogInfo(() => $"VARNISH-SHIM: Varnish is up-to-date."); // Read the cache settings and update the cache warmer. var cacheSettingsJson = File.ReadAllText(Path.Combine(configFolder, "cache-settings.json")); var cacheSettings = NeonHelper.JsonDeserialize <TrafficCacheSettings>(cacheSettingsJson); UpdateCacheWarmer(cacheSettings); } catch (ExecuteException e) { SetErrorTime(); log.LogError(() => $"VARNISH-SHIM: Cannot update Varnish: {e.Message}"); return; } // Varnish was updated successfully so we can reset the error time // so to ensure that periodic error reporting will stop. ResetErrorTime(); } catch (OperationCanceledException) { log.LogInfo(() => "VARNISH-SHIM: Terminating"); throw; } finally { // When DEBUG mode is not enabled, we're going to clear the // both the old and new configuration folders so we don't leave // secrets like TLS private keys lying around in a file system. // // We'll leave these intact for DEBUG mode so we can manually // poke around the config. if (!debugMode) { NeonHelper.DeleteFolder(configFolder); NeonHelper.DeleteFolder(configUpdateFolder); } } }
/// <summary> /// Configures HAProxy based on the current traffic manager configuration. /// </summary> /// <remarks> /// This method will terminate the service if HAProxy could not be started /// for the first call. /// </remarks> public async static Task ConfigureHAProxy() { try { // Retrieve the configuration HASH and compare that with what // we have already deployed. log.LogInfo(() => $"HAPROXY-SHIM: Retrieving configuration HASH from Consul path [{configHashKey}]."); string configHash; try { configHash = await consul.KV.GetString(configHashKey, terminator.CancellationToken); } catch (OperationCanceledException) { return; } catch (Exception e) { SetErrorTime(); log.LogError($"HAPROXY-SHIM: Cannot retrieve [{configHashKey}] from Consul.", e); return; } if (configHash == deployedHash) { log.LogInfo(() => $"HAPROXY-SHIM: Configuration with [hash={configHash}] is already deployed."); return; } else { log.LogInfo(() => $"HAPROXY-SHIM: Configuration hash has changed from [{deployedHash}] to [{configHash}]."); } // Download the configuration archive from Consul and extract it to // the new configuration directory (after ensuring that the directory // has been cleared). log.LogInfo(() => $"HAPROXY-SHIM: Retrieving configuration ZIP archive from Consul path [{configKey}]."); byte[] zipBytes; try { zipBytes = await consul.KV.GetBytes(configKey, terminator.CancellationToken); } catch (OperationCanceledException) { return; } catch (Exception e) { SetErrorTime(); log.LogError($"HAPROXY-SHIM: Cannot retrieve [{configKey}] from Consul.", e); return; } if (configHash == deployedHash) { log.LogInfo(() => $"HAPROXY-SHIM: Configuration with [hash={configHash}] is already deployed."); return; } var zipPath = Path.Combine(configUpdateFolder, "haproxy.zip"); log.LogInfo(() => $"HAPROXY-SHIM: Extracting ZIP archive to [{configUpdateFolder}]."); // Ensure that we have a fresh update folder. NeonHelper.DeleteFolder(configUpdateFolder); Directory.CreateDirectory(configUpdateFolder); // Unzip the configuration archive to the update folder. File.WriteAllBytes(zipPath, zipBytes); var response = NeonHelper.ExecuteCapture("unzip", new object[] { "-o", zipPath, "-d", configUpdateFolder }); response.EnsureSuccess(); // The [certs.list] file (if present) describes the certificates // to be downloaded from Vault. // // Each line contains three fields separated by a space: // the Vault object path, the relative destination folder // path and the file name. // // Note that certificates are stored in Vault as JSON using // the [TlsCertificate] schema, so we'll need to extract and // combine the [cert] and [key] properties. var certsPath = Path.Combine(configUpdateFolder, "certs.list"); if (File.Exists(certsPath)) { using (var reader = new StreamReader(certsPath)) { foreach (var line in reader.Lines()) { if (string.IsNullOrWhiteSpace(line)) { continue; // Ignore blank lines } if (isBridge) { log.LogWarn(() => $"HAPROXY-SHIM: Bridge cannot process unexpected TLS certificate reference: {line}"); return; } var fields = line.Split(' '); var certKey = fields[0]; var certDir = Path.Combine(configUpdateFolder, fields[1]); var certFile = fields[2]; Directory.CreateDirectory(certDir); var cert = await vault.ReadJsonAsync <TlsCertificate>(certKey, terminator.CancellationToken); File.WriteAllText(Path.Combine(certDir, certFile), cert.CombinedPemNormalized); } } } // Verify the configuration. Note that HAProxy will return a // 0 error code if the configuration is OK and specifies at // least one route. It will return 2 if the configuration is // OK but there are no routes. In this case, HAProxy won't // actually launch. Any other exit code indicates that the // configuration is not valid. log.LogInfo(() => "Verifying HAProxy configuration."); Environment.SetEnvironmentVariable("HAPROXY_CONFIG_FOLDER", configUpdateFolder); response = NeonHelper.ExecuteCapture("haproxy", new object[] { "-c", "-q", "-f", configUpdateFolder }); switch (response.ExitCode) { case 0: log.LogInfo(() => "HAPROXY-SHIM: Configuration is OK."); break; case 2: log.LogInfo(() => "HAPROXY-SHIM: Configuration is valid but specifies no routes."); // Ensure that any existing HAProxy instances are stopped and that // the configuration folders are cleared (for non-DEBUG mode) and // then return so we won't try to spin up another HAProxy. foreach (var processId in GetHAProxyProcessIds()) { KillProcess(processId); } if (!debugMode) { NeonHelper.DeleteFolder(configFolder); NeonHelper.DeleteFolder(configUpdateFolder); } return; default: SetErrorTime(); log.LogError(() => $"HAPROXY-SHIM: Invalid HAProxy configuration: {response.AllText}."); // If HAProxy is running then we'll let it continue using // the out-of-date configuration as a fail-safe. If it's not // running, we're going to terminate service. if (!GetHAProxyProcessIds().IsEmpty()) { log.LogWarn(() => "HAPROXY-SHIM: Continuining to use the previous configuration as a fail-safe."); } else { log.LogCritical(() => "HAPROXY-SHIM: Terminating service because there is no valid configuration to fall back to."); Program.Exit(1); return; } break; } // Purge the contents of the [configFolder] and copy the contents // of [configUpdateolder] into it. NeonHelper.DeleteFolder(configFolder); Directory.CreateDirectory(configFolder); NeonHelper.CopyFolder(configUpdateFolder, configFolder); // Start HAProxy if it's not already running. // // ...or we'll generally do a soft stop when HAProxy is already running, // which means that HAProxy will try hard to maintain existing connections // as it reloads its config. The presence of a [.hardstop] file in the // configuration folder will enable a hard stop. // // Note that there may actually be more then one HAProxy process running. // One will be actively handling new connections and the rest will be // waiting gracefully for existing connections to be closed before they // terminate themselves. // $todo(jeff.lill): // // I don't believe that [neon-proxy-manager] ever generates a // [.hardstop] file. This was an idea for the future. This would // probably be better replaced by a boolean [HardStop] oroperty // passed with the notification message. var haProxyProcessIds = GetHAProxyProcessIds(); var stopType = string.Empty; var stopOptions = new List <string>(); var restart = false; if (haProxyProcessIds.Count > 0) { restart = true; if (File.Exists(Path.Combine(configFolder, ".hardstop"))) { stopType = "(hard stop)"; stopOptions.Add("-st"); foreach (var processId in haProxyProcessIds) { stopOptions.Add(processId.ToString()); } } else { stopType = "(soft stop)"; stopOptions.Add("-sf"); foreach (var processId in haProxyProcessIds) { stopOptions.Add(processId.ToString()); } } log.LogInfo(() => $"HAPROXY-SHIM: Restarting HAProxy {stopType}."); } else { restart = false; log.LogInfo(() => $"HAPROXY-SHIM: Starting HAProxy."); } // Enable HAProxy debugging mode to get a better idea of why health // checks are failing. var debugOption = string.Empty; if (debugMode) { debugOption = "-d"; } // Execute HAProxy. If we're not running in DEBUG mode then HAProxy // will fork another HAProxy process that will handle the traffic and // return. For DEBUG mode, this HAProxy call will ignore daemon mode // and run in the forground. In that case, we need to fork HAProxy // so we won't block here forever. Environment.SetEnvironmentVariable("HAPROXY_CONFIG_FOLDER", configFolder); if (!debugMode) { // Regular mode. response = NeonHelper.ExecuteCapture("haproxy", new object[] { "-f", configPath, stopOptions, debugOption, "-V" }); if (response.ExitCode != 0) { SetErrorTime(); log.LogError(() => $"HAPROXY-SHIM: HAProxy failure: {response.ErrorText}"); return; } } else { // DEBUG mode steps: // // 1: Kill any existing HAProxy processes. // 2: Fork the new one to pick up the latest config. foreach (var processId in haProxyProcessIds) { KillProcess(processId); } NeonHelper.Fork("haproxy", new object[] { "-f", configPath, stopOptions, debugOption, "-V" }); } // Give HAProxy a chance to start/restart cleanly. await Task.Delay(startDelay, terminator.CancellationToken); if (restart) { log.LogInfo(() => "HAPROXY-SHIM: HAProxy has been updated."); } else { log.LogInfo(() => "HAPROXY-SHIM: HAProxy has started."); } // Update the deployed hash so we won't try to update the same // configuration again. deployedHash = configHash; // Ensure that we're not exceeding the limit for HAProxy processes. if (maxHAProxyCount > 0) { var newHaProxyProcessIds = GetHAProxyProcessIds(); if (newHaProxyProcessIds.Count > maxHAProxyCount) { log.LogWarn(() => $"HAProxy process count [{newHaProxyProcessIds}] exceeds [MAX_HAPROXY_COUNT={maxHAProxyCount}] so we're killing the oldest inactive instance."); KillOldestProcess(haProxyProcessIds); } } // HAProxy was updated successfully so we can reset the error time // so to ensure that periodic error reporting will stop. ResetErrorTime(); } catch (OperationCanceledException) { log.LogInfo(() => "HAPROXY-SHIM: Terminating"); throw; } catch (Exception e) { if (GetHAProxyProcessIds().Count == 0) { log.LogCritical("HAPROXY-SHIM: Terminating because we cannot launch HAProxy.", e); Program.Exit(1); return; } else { log.LogError("HAPROXY-SHIM: Unable to reconfigure HAProxy. Using the old configuration as a fail-safe.", e); } SetErrorTime(); } finally { // When DEBUG mode is not enabled, we're going to clear the // both the old and new configuration folders so we don't leave // secrets like TLS private keys lying around in a file system. // // We'll leave these intact for DEBUG mode so we can manually // poke around the config. if (!debugMode) { NeonHelper.DeleteFolder(configFolder); NeonHelper.DeleteFolder(configUpdateFolder); } } }