-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create an "installer" executable for use with the Datadog installer (#…
…6643) ## Summary of changes Creates an "installer" executable, designed to be executed by the fleet installer as part of configuring Windows SSI. ## Reason for change We don't want to use the existing MSI, primarily because it requires stopping and starting IIS when you update it, otherwise we risk causing crashes. The datadog-installer is responsible for copying the files added to the Windows OCI image, and running the Datadog.FleetInstaller.exe executable to configure the application. Similarly, it calls this exe when uninstalling a tracer version or removing the product ## Implementation details - Uses the fusion.dll API directly to manage the GAC entries, based on the generated PInvoke APIs defined [here](https://github.com/dotnet/pinvoke/tree/main/src/Fusion) - Sets the envivornment variables by setting the `environmentVariables` section in applicationHost.config. Uses [the _Microsoft.Web.Administration_ NuGet](https://www.nuget.org/packages/Microsoft.Web.Administration) to interact with the native API. ## Test coverage Manually tested. Subsequent PRs in the stack will add smoke and integration tests. ## Other details Explicitly designed to run on Windows Server 2016+, so targets .NET FX for simplicity. The _Microsoft.Web.Administration_ nuget is a .NET Standard 1.x dll, which creates an annoying number of dlls in the output, but it's just ugly, it works fine. There's a fair amount of duplication in the Fusion API/Microsoft.Web.Administration code with what the `dd-dotnet`/`dd-trace` tool currently does. We could certainly look at consolidating that down the line, and potentially rewriting the fleet installer tool to be a NativeAOT executable, but it's probably not worth the effort at this point. The explicit `<RuntimeIdentifier>win-x64</RuntimeIdentifier>` in the _.csproj_ is an odd one. Without it, for some reason, the gitlab build restores for x86 but tries to build for x64 which is... weird. Also, can't repro the issue locally. _Really_ don't understand what's happening there, but this works so... 🙈 Part of a stack of PRs: - #6643 👈 - #6644 - #6645
- Loading branch information
1 parent
035e21b
commit 0252136
Showing
42 changed files
with
2,474 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
// <copyright file="AppHostHelper.cs" company="Datadog"> | ||
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
// </copyright> | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.IO; | ||
using Microsoft.Web.Administration; | ||
|
||
namespace Datadog.FleetInstaller; | ||
|
||
internal static class AppHostHelper | ||
{ | ||
public static bool SetAllEnvironmentVariables(ILogger log, TracerValues tracerValues) | ||
{ | ||
log.WriteInfo("Setting app pool environment variables"); | ||
return ModifyEnvironmentVariablesWithRetry(log, tracerValues.RequiredEnvVariables, SetEnvVars); | ||
} | ||
|
||
public static bool RemoveAllEnvironmentVariables(ILogger log) | ||
{ | ||
log.WriteInfo("Removing app pool environment variables"); | ||
// we don't need to know the exact tracer values, we just use the _keys_ in removeEnvVars | ||
var envVars = new TracerValues(string.Empty).RequiredEnvVariables; | ||
return ModifyEnvironmentVariablesWithRetry(log, envVars, RemoveEnvVars); | ||
} | ||
|
||
private static bool ModifyEnvironmentVariablesWithRetry( | ||
ILogger log, | ||
ReadOnlyDictionary<string, string> envVars, | ||
Action<ConfigurationElementCollection, ReadOnlyDictionary<string, string>> updateEnvVars) | ||
{ | ||
// If the IIS host config is being modified, this may fail | ||
// We retry multiple times, as the final update is atomic | ||
// We could consider adding backoff here, but it's not clear that it's necessary | ||
var attempt = 0; | ||
while (attempt < 3) | ||
{ | ||
attempt++; | ||
if (attempt > 1) | ||
{ | ||
log.WriteInfo($"Attempt {attempt} to update IIS failed, retrying."); | ||
} | ||
|
||
if (ModifyEnvironmentVariables(log, envVars, updateEnvVars)) | ||
{ | ||
return true; | ||
} | ||
} | ||
|
||
log.WriteError($"Failed to update IIS after {attempt} attempts"); | ||
return false; | ||
} | ||
|
||
private static bool ModifyEnvironmentVariables( | ||
ILogger log, | ||
ReadOnlyDictionary<string, string> envVars, | ||
Action<ConfigurationElementCollection, ReadOnlyDictionary<string, string>> updateEnvVars) | ||
{ | ||
if (!SetEnvironmentVariables(log, envVars, updateEnvVars, out var appPoolsWeMustReenableRecycling)) | ||
{ | ||
// If we failed to set the environment variables, we don't need to re-enable recycling | ||
// because by definition we can't have saved successfully | ||
return false; | ||
} | ||
|
||
// We do this separately, because we have to do all the work again no matter what we do | ||
return ReEnableRecycling(log, appPoolsWeMustReenableRecycling); | ||
} | ||
|
||
private static bool SetEnvironmentVariables( | ||
ILogger log, | ||
ReadOnlyDictionary<string, string> envVars, | ||
Action<ConfigurationElementCollection, ReadOnlyDictionary<string, string>> updateEnvVars, | ||
out HashSet<string> appPoolsWeMustReenableRecycling) | ||
{ | ||
appPoolsWeMustReenableRecycling = []; | ||
|
||
try | ||
{ | ||
using var serverManager = new ServerManager(); | ||
appPoolsWeMustReenableRecycling = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|
||
var appPoolsSection = GetApplicationPoolsSection(log, serverManager); | ||
if (appPoolsSection is null) | ||
{ | ||
return false; | ||
} | ||
|
||
var (applicationPoolDefaults, applicationPoolsCollection) = appPoolsSection.Value; | ||
|
||
// Update defaults | ||
log.WriteInfo($"Updating applicationPoolDefaults environment variables"); | ||
updateEnvVars(applicationPoolDefaults.GetCollection("environmentVariables"), envVars); | ||
|
||
// Update app pools | ||
foreach (var appPoolElement in applicationPoolsCollection) | ||
{ | ||
if (string.Equals(appPoolElement.ElementTagName, "add", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
// An app pool element | ||
var poolName = appPoolElement.GetAttributeValue("name") as string; | ||
if (poolName is null) | ||
{ | ||
// poolName can never be null, if it is, weirdness is afoot, so bail out | ||
log.WriteInfo("Found app pool element without a name, skipping"); | ||
continue; | ||
} | ||
|
||
log.WriteInfo($"Updating app pool '{poolName}' environment variables"); | ||
|
||
// disable recycling of the pool, so that we don't force a restart when we update the pool | ||
// we can't distinguish between "not set" and "set to false", but we only really care about | ||
// if it was set to "true", as we don't want to accidentally revert that later. | ||
if (appPoolElement.GetChildElement("recycling")["disallowRotationOnConfigChange"] as bool? ?? false) | ||
{ | ||
// already disallowed, which is what we want, so don't need to modify it now _or_ later | ||
} | ||
else | ||
{ | ||
appPoolsWeMustReenableRecycling.Add(poolName); | ||
appPoolElement.GetChildElement("recycling")["disallowRotationOnConfigChange"] = true; | ||
} | ||
|
||
// Set the pool-specific env variables | ||
updateEnvVars(appPoolElement.GetCollection("environmentVariables"), envVars); | ||
} | ||
} | ||
|
||
log.WriteInfo("Saving applicationHost.config"); | ||
serverManager.CommitChanges(); | ||
return true; | ||
} | ||
catch (Exception ex) | ||
{ | ||
log.WriteError(ex, $"Error updating application pools"); | ||
return false; | ||
} | ||
} | ||
|
||
private static bool ReEnableRecycling(ILogger log, HashSet<string> appPoolsWhichNeedToAllowRecycling) | ||
{ | ||
try | ||
{ | ||
using var serverManager = new ServerManager(); | ||
|
||
var appPoolsSection = GetApplicationPoolsSection(log, serverManager); | ||
if (appPoolsSection is null) | ||
{ | ||
return false; | ||
} | ||
|
||
// Set env variables | ||
foreach (var appPoolElement in appPoolsSection.Value.AppPools) | ||
{ | ||
if (string.Equals(appPoolElement.ElementTagName, "add", StringComparison.OrdinalIgnoreCase) | ||
&& appPoolElement.GetAttributeValue("name") is string poolName | ||
&& appPoolsWhichNeedToAllowRecycling.Contains(poolName)) | ||
{ | ||
log.WriteInfo($"Re-enabling rotation on config change for app pool '{poolName}'"); | ||
appPoolElement.GetChildElement("recycling")["disallowRotationOnConfigChange"] = false; | ||
} | ||
} | ||
|
||
log.WriteInfo("Saving applicationHost.config"); | ||
serverManager.CommitChanges(); | ||
return true; | ||
} | ||
catch (Exception ex) | ||
{ | ||
log.WriteError(ex, $"Error re-enabling application pool recycling"); | ||
return false; | ||
} | ||
} | ||
|
||
private static (ConfigurationElement AppPoolDefaults, ConfigurationElementCollection AppPools)? GetApplicationPoolsSection(ILogger log, ServerManager serverManager) | ||
{ | ||
var config = serverManager.GetApplicationHostConfiguration(); | ||
if (config is null) | ||
{ | ||
log.WriteError("Error fetching application host configuration"); | ||
return null; | ||
} | ||
|
||
var appPoolSectionName = "system.applicationHost/applicationPools"; | ||
var appPoolsSection = config.GetSection(appPoolSectionName); | ||
if (appPoolsSection is null) | ||
{ | ||
log.WriteError($"Error fetching application pools: no section {appPoolSectionName} found"); | ||
return null; | ||
} | ||
|
||
var applicationPoolDefaults = appPoolsSection.GetChildElement("applicationPoolDefaults"); | ||
if (applicationPoolDefaults is null) | ||
{ | ||
log.WriteError("Error fetching application pool defaults: applicationPoolDefaults returned null"); | ||
return null; | ||
} | ||
|
||
var applicationPoolsCollection = appPoolsSection.GetCollection(); | ||
if (applicationPoolsCollection is null) | ||
{ | ||
log.WriteError("Error fetching application pool collection: applicationPools collection returned null"); | ||
return null; | ||
} | ||
|
||
return (applicationPoolDefaults, applicationPoolsCollection); | ||
} | ||
|
||
private static void SetEnvVars(ConfigurationElementCollection envVars, ReadOnlyDictionary<string, string> requiredVariables) | ||
{ | ||
// Try to find the value we need | ||
// Update all the values we need | ||
var remainingValues = new Dictionary<string, string>(requiredVariables); | ||
|
||
foreach (var envVarEle in envVars) | ||
{ | ||
if (!string.Equals(envVarEle.ElementTagName, "add", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
continue; | ||
} | ||
|
||
if (envVarEle.GetAttributeValue("name") is not string key | ||
|| !remainingValues.TryGetValue(key, out var envVarValue)) | ||
{ | ||
continue; | ||
} | ||
|
||
envVarEle["value"] = envVarValue; | ||
// log.WriteInfo($"Updated environment variable {key} to {envVarValue}"); | ||
remainingValues.Remove(key); | ||
} | ||
|
||
foreach (var kvp in remainingValues) | ||
{ | ||
var addEle = envVars.CreateElement("add"); | ||
addEle["name"] = kvp.Key; | ||
addEle["value"] = kvp.Value; | ||
envVars.Add(addEle); | ||
} | ||
} | ||
|
||
private static void RemoveEnvVars(ConfigurationElementCollection envVars, ReadOnlyDictionary<string, string> requiredVariables) | ||
{ | ||
// Try to find the value we need | ||
// Update all the values we need | ||
List<ConfigurationElement>? envVarsToRemove = null; | ||
|
||
foreach (var envVarEle in envVars) | ||
{ | ||
if (!string.Equals(envVarEle.ElementTagName, "add", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
continue; | ||
} | ||
|
||
if (envVarEle.GetAttributeValue("name") is not string key | ||
|| !requiredVariables.ContainsKey(key)) | ||
{ | ||
continue; | ||
} | ||
|
||
envVarsToRemove ??= new(); | ||
envVarsToRemove.Add(envVarEle); | ||
} | ||
|
||
if (envVarsToRemove is null) | ||
{ | ||
return; | ||
} | ||
|
||
foreach (var element in envVarsToRemove) | ||
{ | ||
envVars.Remove(element); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// <copyright file="CommandBase.cs" company="Datadog"> | ||
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
// </copyright> | ||
|
||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
using System.CommandLine.Parsing; | ||
using System.Runtime.InteropServices; | ||
using System.Security.Principal; | ||
using System.Threading.Tasks; | ||
|
||
namespace Datadog.FleetInstaller.Commands; | ||
|
||
internal abstract class CommandBase : Command | ||
{ | ||
protected CommandBase(string name, string? description = null) | ||
: base(name, description) | ||
{ | ||
} | ||
|
||
protected bool IsValidEnvironment(CommandResult commandResult) | ||
{ | ||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||
{ | ||
commandResult.ErrorMessage = $"This installer is only intended to run on Windows, it cannot be used on {RuntimeInformation.OSDescription}"; | ||
return false; | ||
} | ||
|
||
using var identity = WindowsIdentity.GetCurrent(); | ||
var principal = new WindowsPrincipal(identity); | ||
if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) | ||
{ | ||
commandResult.ErrorMessage = $"This installer must be run with administrator privileges. Current user {identity.Name} is not an administrator."; | ||
return false; | ||
} | ||
|
||
if (!RegistryHelper.TryGetIisVersion(Log.Instance, out var version)) | ||
{ | ||
commandResult.ErrorMessage = "This installer requires IIS 10.0 or later. Could not determine the IIS version; is the IIS feature enabled?"; | ||
return false; | ||
} | ||
|
||
if (version.Major < 10) | ||
{ | ||
commandResult.ErrorMessage = $"This installer requires IIS 10.0 or later. Detected IIS version {version.Major}.{version.Minor}"; | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
Oops, something went wrong.