Skip to content

Commit

Permalink
mutually exclusive MSI flavors (DataDog#32752)
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkb7 authored Jan 9, 2025
1 parent 13dd63f commit fa88b00
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitlab/e2e_install_packages/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 "))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

/// <summary>
/// Adds elements to the WiX to enforce that the product cannot be installed at the same time as another product.
/// </summary>
/// <remarks>
/// 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:
/// <Upgrade Id="PUT-GUID-HERE">
/// <UpgradeVersion Minimum="0.0.0.0" IncludeMinimum="yes" OnlyDetect="yes" Maximum="255.255.0.0" IncludeMaximum="no" Property="MUTUALLY_EXCLUSIVE_PRODUCTS_1" />
/// </Upgrade>
/// <Condition Message="This product cannot be installed at the same time as [ProductName]. Please uninstall [ProductName] before continuing.">NOT MUTUALLY_EXCLUSIVE_PRODUCTS_1</Condition>
/// <Property Id="MUTUALLY_EXCLUSIVE_PRODUCTS_1" Secure="yes" />
/// </remarks>
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);
}
}
}

0 comments on commit fa88b00

Please sign in to comment.