From fa88b009ec397efe8856b1921e3289e85652ca99 Mon Sep 17 00:00:00 2001 From: Branden Clark Date: Thu, 9 Jan 2025 18:22:46 -0500 Subject: [PATCH] mutually exclusive MSI flavors (#32752) --- .gitlab/e2e_install_packages/windows.yml | 2 + .../mutually_exclusive_product_test.go | 87 +++++++++++++++++++ .../WixSetup/Datadog Agent/AgentFlavor.cs | 16 ++++ .../WixSetup/Datadog Agent/AgentInstaller.cs | 12 +++ .../WixSetup/MutuallyExclusiveProduct.cs | 83 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 test/new-e2e/tests/windows/install-test/mutually_exclusive_product_test.go create mode 100644 tools/windows/DatadogAgentInstaller/WixSetup/MutuallyExclusiveProduct.cs diff --git a/.gitlab/e2e_install_packages/windows.yml b/.gitlab/e2e_install_packages/windows.yml index 60f5c2d095c979..40fb862e2397b3 100644 --- a/.gitlab/e2e_install_packages/windows.yml +++ b/.gitlab/e2e_install_packages/windows.yml @@ -58,6 +58,8 @@ - E2E_MSI_TEST: TestNPMInstallWithAddLocal - E2E_MSI_TEST: TestNPMUpgradeFromBeta - E2E_MSI_TEST: TestUpgradeFromV6 + - E2E_MSI_TEST: TestFIPSAgentDoesNotInstallOverAgent + - E2E_MSI_TEST: TestAgentDoesNotInstallOverFIPSAgent new-e2e_windows_powershell_module_test: extends: .new_e2e_template diff --git a/test/new-e2e/tests/windows/install-test/mutually_exclusive_product_test.go b/test/new-e2e/tests/windows/install-test/mutually_exclusive_product_test.go new file mode 100644 index 00000000000000..9220ed8e5c6f79 --- /dev/null +++ b/test/new-e2e/tests/windows/install-test/mutually_exclusive_product_test.go @@ -0,0 +1,87 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package installtest + +import ( + "os" + "path/filepath" + "strings" + + "github.com/DataDog/datadog-agent/test/new-e2e/pkg/environments" + "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows" + windowsCommon "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common" + windowsAgent "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common/agent" + + "github.com/stretchr/testify/require" + "testing" +) + +type mutuallyExclusiveInstallSuite struct { + windows.BaseAgentInstallerSuite[environments.WindowsHost] + + previousAgentPackage *windowsAgent.Package +} + +// TestFIPSAgentDoesNotInstallOverAgent tests that the FIPS agent cannot be installed over the base Agent +// +// This test uses the last stable base Agent package and the pipeline produced FIPS Agent package. +func TestFIPSAgentDoesNotInstallOverAgent(t *testing.T) { + s := &mutuallyExclusiveInstallSuite{} + os.Setenv(windowsAgent.PackageFlavorEnvVar, "base") + previousAgentPackage, err := windowsAgent.GetLastStablePackageFromEnv() + require.NoError(t, err, "should get last stable agent package from env") + s.previousAgentPackage = previousAgentPackage + os.Setenv(windowsAgent.PackageFlavorEnvVar, "fips") + run(t, s) +} + +// TestAgentDoesNotInstallOverFIPSAgent tests that the base Agent cannot be installed over the FIPS agent +// +// This test uses the pipeline produced MSI packages for both flavors. This is necessary for now +// because the previous Agent versions do not contain the changes to detect mutually exclusive products. +func TestAgentDoesNotInstallOverFIPSAgent(t *testing.T) { + s := &mutuallyExclusiveInstallSuite{} + os.Setenv(windowsAgent.PackageFlavorEnvVar, "fips") + previousAgentPackage, err := windowsAgent.GetPackageFromEnv() + require.NoError(t, err, "should get Agent package from env") + s.previousAgentPackage = previousAgentPackage + os.Setenv(windowsAgent.PackageFlavorEnvVar, "base") + run(t, s) +} + +func (s *mutuallyExclusiveInstallSuite) SetupSuite() { + // Base looks up the first Agent package + s.BaseAgentInstallerSuite.SetupSuite() + host := s.Env().RemoteHost + var err error + + s.T().Logf("Using previous Agent package: %#vvi", s.previousAgentPackage) + + // Install first Agent + _, err = s.InstallAgent(host, windowsAgent.WithPackage(s.previousAgentPackage)) + s.Require().NoError(err) +} + +func (s *mutuallyExclusiveInstallSuite) TestMutuallyExclusivePackage() { + host := s.Env().RemoteHost + + // Install second Agent + logFilePath := filepath.Join(s.SessionOutputDir(), "secondInstall.log") + _, err := s.InstallAgent(host, + windowsAgent.WithPackage(s.AgentPackage), + windowsAgent.WithInstallLogFile(logFilePath), + ) + s.Require().Error(err) + + // Ensure that the log file contains the expected error message + logData, err := os.ReadFile(logFilePath) + s.Require().NoError(err) + // convert from utf-16 to utf-8 + logData, err = windowsCommon.ConvertUTF16ToUTF8(logData) + s.Require().NoError(err) + // We don't use assert.Contains because it will print the very large logData on error + s.Assert().True(strings.Contains(string(logData), "This product cannot be installed at the same time as ")) +} diff --git a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs index f02c66f3248b61..f98ff5a3226248 100644 --- a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs +++ b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs @@ -4,10 +4,26 @@ namespace WixSetup.Datadog_Agent { internal static class AgentFlavorFactory { + private const string FipsFlavor = "fips"; + private const string BaseFlavor = "base"; + + public static string[] GetAllAgentFlavors() + { + return new[] + { + BaseFlavor, + FipsFlavor + }; + } + public static IAgentFlavor New(AgentVersion agentVersion) { var flavor = Environment.GetEnvironmentVariable("AGENT_FLAVOR"); + return New(flavor, agentVersion); + } + public static IAgentFlavor New(string flavor, AgentVersion agentVersion) + { return flavor switch { "fips" => new FIPSAgent(agentVersion), diff --git a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs index f7aaaf44ca494f..2b10538d2e3b53 100644 --- a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs +++ b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs @@ -207,6 +207,18 @@ public Project Configure() "Automatic downgrades are not supported. Uninstall the current version, and then reinstall the desired version."; project.ReinstallMode = "amus"; + // Add upgrade elements for all agent flavors except the current one + // to prevent them from being installed side-by-side. + foreach (var flavorType in AgentFlavorFactory.GetAllAgentFlavors()) + { + IAgentFlavor flavor = AgentFlavorFactory.New(flavorType, _agentVersion); + if (flavor.UpgradeCode == _agentFlavor.UpgradeCode) + { + continue; + } + project.Add(new MutuallyExclusiveProducts(flavor.ProductFullName, flavor.UpgradeCode)); + } + project.Platform = Platform.x64; // MSI 5.0 was shipped in Windows Server 2012 R2. // https://learn.microsoft.com/en-us/windows/win32/msi/released-versions-of-windows-installer diff --git a/tools/windows/DatadogAgentInstaller/WixSetup/MutuallyExclusiveProduct.cs b/tools/windows/DatadogAgentInstaller/WixSetup/MutuallyExclusiveProduct.cs new file mode 100644 index 00000000000000..c6ccbf4545ceca --- /dev/null +++ b/tools/windows/DatadogAgentInstaller/WixSetup/MutuallyExclusiveProduct.cs @@ -0,0 +1,83 @@ +using System; +using System.Xml.Linq; +using WixSharp; + +namespace WixSetup +{ + internal class MutuallyExclusiveProducts : WixEntity, IGenericEntity + { + private static int usageCounter = 0; + + public Guid UpgradeCode { get; set; } + public string ProductName { get; set; } + + public MutuallyExclusiveProducts() + { + } + + public MutuallyExclusiveProducts(string productName, Guid upgradeCode) + { + UpgradeCode = upgradeCode; + ProductName = productName; + } + + /// + /// Adds elements to the WiX to enforce that the product cannot be installed at the same time as another product. + /// + /// + /// The FindRelatedProducts action will set a property if a product matching the provided UpgradeCode + /// is found on the system. We check this property in a LaunchCondition to prevent installation. + /// See https://learn.microsoft.com/en-us/windows/win32/msi/findrelatedproducts-action + /// + /// Example WiX: + /// + /// + /// + /// NOT MUTUALLY_EXCLUSIVE_PRODUCTS_1 + /// + /// + public void Process(ProcessingContext context) + { + // Append a unique number to the property name. + // + // Windows Installer appends each product code found to the property + // so we could use a single property for all mutually exclusive products, + // but using a unique property for each product lets us include the product name + // in the condition message, which makes the message more user-friendly. + // https://learn.microsoft.com/en-us/windows/win32/msi/upgrade-table + usageCounter++; + var propertyName = $"MUTUALLY_EXCLUSIVE_PRODUCTS_{usageCounter}"; + + var upgradeElement = new XElement("Upgrade"); + upgradeElement.SetAttributeValue("Id", UpgradeCode); + + var upgradeVersionElement = new XElement("UpgradeVersion", + new XAttribute("Minimum", "0.0.0.0"), + new XAttribute("IncludeMinimum", "yes"), + new XAttribute("OnlyDetect", "yes"), + // 255 is the maximum + // https://learn.microsoft.com/en-us/windows/win32/msi/productversion + new XAttribute("Maximum", "255.255.0.0"), + new XAttribute("IncludeMaximum", "yes"), + new XAttribute("Property", propertyName) + ); + upgradeElement.Add(upgradeVersionElement); + context.XParent.Add(upgradeElement); + + var conditionElement = new XElement("Condition", + new XAttribute("Message", + $"This product cannot be installed at the same time as {ProductName}. Please uninstall {ProductName} before continuing."), + $"NOT {propertyName}"); + context.XParent.Add(conditionElement); + + // The property specified in this column must be a public property and the + // package author must add the property to the SecureCustomProperties property. + // https://learn.microsoft.com/en-us/windows/win32/msi/upgrade-table + var propertyElement = new XElement("Property", + new XAttribute("Id", propertyName), + new XAttribute("Secure", "yes") + ); + context.XParent.Add(propertyElement); + } + } +}