Skip to content

Commit

Permalink
Create an "installer" executable for use with the Datadog installer (#…
Browse files Browse the repository at this point in the history
…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
andrewlock authored Feb 12, 2025
1 parent 035e21b commit 0252136
Show file tree
Hide file tree
Showing 42 changed files with 2,474 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Datadog.Trace.sln
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.AzureFunctions.V4Is
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.AzureFunctions.V4Isolated.AspNetCore", "tracer\test\test-applications\azure-functions\Samples.AzureFunctions.V4Isolated.AspNetCore\Samples.AzureFunctions.V4Isolated.AspNetCore.csproj", "{0F8EAB52-0C5B-4F60-92C5-42FAC21F4E77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Datadog.FleetInstaller", "tracer\src\Datadog.FleetInstaller\Datadog.FleetInstaller.csproj", "{47C1970A-0098-45A2-9D65-7790607D9A68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.XUnitTestsV3", "tracer\test\test-applications\integrations\Samples.XUnitTestsV3\Samples.XUnitTestsV3.csproj", "{E5BF2436-0BE7-4096-9F25-5118916F4BE4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.XUnitTestsRetriesV3", "tracer\test\test-applications\integrations\Samples.XUnitTestsRetriesV3\Samples.XUnitTestsRetriesV3.csproj", "{E2EDDD17-B5E6-4240-9EF8-34F2D274AA19}"
Expand Down Expand Up @@ -1459,6 +1461,10 @@ Global
{0F8EAB52-0C5B-4F60-92C5-42FAC21F4E77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F8EAB52-0C5B-4F60-92C5-42FAC21F4E77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F8EAB52-0C5B-4F60-92C5-42FAC21F4E77}.Release|Any CPU.Build.0 = Release|Any CPU
{47C1970A-0098-45A2-9D65-7790607D9A68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47C1970A-0098-45A2-9D65-7790607D9A68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47C1970A-0098-45A2-9D65-7790607D9A68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47C1970A-0098-45A2-9D65-7790607D9A68}.Release|Any CPU.Build.0 = Release|Any CPU
{E5BF2436-0BE7-4096-9F25-5118916F4BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5BF2436-0BE7-4096-9F25-5118916F4BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5BF2436-0BE7-4096-9F25-5118916F4BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1707,6 +1713,7 @@ Global
{18767A3E-9ADC-485C-A8C7-50660D5B579D} = {C4C1E313-C7C1-4490-AECE-0DD0062380A4}
{5D2C6B9C-FCE2-4E46-B4ED-BC3B11CFBB3C} = {C4C1E313-C7C1-4490-AECE-0DD0062380A4}
{0F8EAB52-0C5B-4F60-92C5-42FAC21F4E77} = {C4C1E313-C7C1-4490-AECE-0DD0062380A4}
{47C1970A-0098-45A2-9D65-7790607D9A68} = {9E5F0022-0A50-40BF-AC6A-C3078585ECAB}
{E5BF2436-0BE7-4096-9F25-5118916F4BE4} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A}
{E2EDDD17-B5E6-4240-9EF8-34F2D274AA19} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A}
{0C0578CB-3B67-4F95-8547-206CD2A560CD} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A}
Expand Down
278 changes: 278 additions & 0 deletions tracer/src/Datadog.FleetInstaller/AppHostHelper.cs
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);
}
}
}
52 changes: 52 additions & 0 deletions tracer/src/Datadog.FleetInstaller/Commands/CommandBase.cs
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;
}
}
Loading

0 comments on commit 0252136

Please sign in to comment.