Skip to content

Commit 7252f07

Browse files
authored
Add version change support to Windows install script (#6304)
* Add version change support to Windows install script * Run the golang tests from powershell.exe * PR Feedback * PR Feedback 01 * PR Feedback 02
1 parent e82f3ef commit 7252f07

File tree

3 files changed

+295
-11
lines changed

3 files changed

+295
-11
lines changed

.github/workflows/win-package-test.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,42 @@ jobs:
287287
if: ${{ always() && matrix.WITH_FLUENTD == 'true' }}
288288
run: Get-Content -Path "${env:SYSTEMDRIVE}\opt\td-agent\td-agent.log"
289289

290+
windows-script-upgrade-test:
291+
strategy:
292+
matrix:
293+
OS: [ "windows-2022", "windows-2025" ]
294+
runs-on: otel-windows # Tests with uninstall require a more powerful runner
295+
needs: [msi-test, windows-install-script-test]
296+
timeout-minutes: 45
297+
steps:
298+
- name: Check out the codebase.
299+
uses: actions/checkout@v4
300+
301+
- name: Downloading msi build
302+
uses: actions/download-artifact@v4
303+
with:
304+
name: msi-build-${{ matrix.OS }}
305+
path: ./dist
306+
- name: Ensure required ports in the dynamic range are available
307+
run: |
308+
$ErrorActionPreference = 'Continue'
309+
& ${{ github.workspace }}\.github\workflows\scripts\win-required-ports.ps1
310+
311+
- name: Set the MSI_COLLECTOR_PATH and INSTALL_SCRIPT_PATH environment variable
312+
run: |
313+
$ErrorActionPreference = 'Stop'
314+
$msi_path = Resolve-Path .\dist\splunk-otel-collector*.msi
315+
Test-Path $msi_path
316+
"MSI_COLLECTOR_PATH=$msi_path" | Out-File -FilePath $env:GITHUB_ENV -Append
317+
$ps1_path = Resolve-Path .\packaging\installer\install.ps1
318+
Test-Path $ps1_path
319+
"INSTALL_SCRIPT_PATH=$ps1_path" | Out-File -FilePath $env:GITHUB_ENV -Append
320+
321+
- name: Run the script upgrade tests
322+
shell: powershell # Use PowerShell to run the script since running the go test from pwsh.exe hides the Get-ExecutionPolicy cmdlet.
323+
run: |
324+
go test -v github.com/signalfx/splunk-otel-collector/tests/windows-install-script
325+
290326
choco-build:
291327
runs-on: ${{ matrix.OS }}
292328
strategy:

packaging/installer/install.ps1

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@
124124
If specified, the -mode parameter will be ignored.
125125
.EXAMPLE
126126
.\install.ps1 -config_path "C:\SOME_FOLDER\my_config.yaml"
127+
.PARAMETER preserve_prev_default_config
128+
(OPTIONAL) Preserve the default configuration files, located at `$Env:ProgramData\Splunk\OpenTelemetry Collector`, of previous version when upgrading the collector. By default it is $false since version changes can include breaking configuration changes.
129+
.EXAMPLE
130+
.\install.ps1 -preserve_prev_default_config $true
127131
#>
128132

129133
param (
@@ -145,6 +149,7 @@ param (
145149
[string]$msi_path = "",
146150
[string]$msi_public_properties = "",
147151
[string]$config_path = "",
152+
[bool]$preserve_prev_default_config = $false,
148153
[string]$collector_msi_url = "",
149154
[string]$fluentd_msi_url = "",
150155
[string]$dotnet_psm1_path = "",
@@ -154,6 +159,8 @@ param (
154159
[bool]$UNIT_TEST = $false
155160
)
156161

162+
New-Variable -Name UninstallWildcardRegPath -Option Constant -Value "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"
163+
New-Variable -Name CollectorServiceDisplayName -Option Constant -Value "Splunk OpenTelemetry Collector"
157164
$arch = "amd64"
158165
$format = "msi"
159166
$service_name = "splunk-otel-collector"
@@ -370,8 +377,29 @@ function download_collector_package([string]$collector_version=$collector_versio
370377
}
371378

372379
# check registry for the agent msi package
373-
function msi_installed([string]$name) {
374-
return (Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where { $_.DisplayName -eq $name }) -ne $null
380+
function is_msi_installed([string]$product_name) {
381+
return $null -ne (Get-ItemProperty $UninstallWildcardRegPath | Where { $_.DisplayName -eq $product_name })
382+
}
383+
384+
function get_msi_installation_sids([string]$product_name) {
385+
$sids = [string[]]@()
386+
387+
$uninstallEntry = Get-ItemProperty $UninstallWildcardRegPath -ErrorAction SilentlyContinue |
388+
Where-Object { $_.DisplayName -eq $product_name }
389+
if ($uninstallEntry) {
390+
$userInstalls = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties' -ErrorAction SilentlyContinue |
391+
Where-Object { $_.DisplayName -eq $product_name }
392+
foreach ($user in $userInstalls) {
393+
# Not all entries are valid user SIDS, e.g.: some are SIDs with suffixes like "_Classes"
394+
# We only want the SIDs.
395+
if ($user.PSPath -match 'UserData\\(?<SID>S-1-[0-9\-]+)') {
396+
$sid = $Matches['SID']
397+
$sids += , @($sid)
398+
}
399+
}
400+
}
401+
402+
return $sids
375403
}
376404

377405
function update_registry([string]$path, [string]$name, [string]$value) {
@@ -413,6 +441,21 @@ function install_msi([string]$path) {
413441
Write-Host "- Done"
414442
}
415443

444+
function uninstall_msi([string]$product_name) {
445+
Write-Host "Uninstalling $product_name ..."
446+
$uninstall_entry = Get-ItemProperty $UninstallWildcardRegPath -ErrorAction SilentlyContinue |
447+
Where-Object { $_.DisplayName -eq $product_name } | Select-Object -First 1
448+
if (-not $uninstall_entry) {
449+
throw "Failed to find the uninstall registry entry for $product_name"
450+
}
451+
$proc = (Start-Process msiexec.exe -Wait -PassThru -ArgumentList "/X `"$($uninstall_entry.PSChildName)`" /qn /norestart")
452+
if ($proc.ExitCode -ne 0) {
453+
Write-Warning "The uninstall attempt failed with error code $($proc.ExitCode)."
454+
Exit $proc.ExitCode
455+
}
456+
Write-Host "- Done"
457+
}
458+
416459
$ErrorActionPreference = 'Stop'; # stop on all errors
417460

418461
# check administrator status
@@ -427,12 +470,47 @@ if (!(check_if_admin)) {
427470
echo 'Checking execution policy'
428471
check_policy
429472

430-
if (msi_installed -name "Splunk OpenTelemetry Collector") {
431-
throw "The Splunk OpenTelemetry Collector is already installed. Remove or uninstall the Collector and rerun this script."
432-
}
433-
434473
if (Get-Service -Name $service_name -ErrorAction SilentlyContinue) {
435-
throw "The $service_name service is already installed. Remove or uninstall the Collector and rerun this script."
474+
Write-Host "The $service_name service is already installed. Checking installation for automatic update."
475+
476+
$uninstall_collector = $true
477+
$collector_sids = get_msi_installation_sids -product_name $CollectorServiceDisplayName
478+
if ($collector_sids.Count -eq 0) {
479+
$uninstall_collector = $false
480+
Write-Warning "The $service_name service exists but it is not on the Windows installation database."
481+
}
482+
else {
483+
if ($collector_sids.Count -gt 1) {
484+
$sids_list = $collector_sids -join ", "
485+
throw "The $CollectorServiceDisplayName is already installed for multiple users (SIDs: $sids_list). Uninstall the collector and remove remaining users installations from the registry."
486+
}
487+
488+
# "S-1-5-18" is the SID for the Local System account, which is used for machine-wide installations.
489+
if ("S-1-5-18" -ne $collector_sids[0]) {
490+
# not a machine wide installation, check if it is the same user
491+
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
492+
$currentUserSID = $currentUser.User.Value
493+
if ($currentUserSID -ne $collector_sids[0]) {
494+
$sid = New-Object System.Security.Principal.SecurityIdentifier($userSid)
495+
$user = $sid.Translate([System.Security.Principal.NTAccount])
496+
throw "The $CollectorServiceDisplayName was last installed by '${user.Value}' it must be updated or uninstalled by the same user."
497+
}
498+
}
499+
}
500+
501+
Write-Host "Stopping $service_name service..."
502+
stop_service -name "$service_name"
503+
if ($uninstall_collector) {
504+
uninstall_msi -product_name $CollectorServiceDisplayName
505+
}
506+
if (-not $preserve_prev_default_config) {
507+
$default_config_files = @("agent_config.yaml", "gateway_config.yaml")
508+
foreach ($file in $default_config_files) {
509+
$target = Join-Path "${Env:ProgramData}\Splunk\OpenTelemetry Collector" "$file"
510+
Write-Host "Deleting previous version default configuration file '$target'"
511+
Remove-Item -Path $target
512+
}
513+
}
436514
}
437515

438516
if ($with_fluentd -And (Get-Service -name $fluentd_service_name -ErrorAction SilentlyContinue)) {
@@ -447,7 +525,7 @@ if ($with_fluentd -And (Test-Path -Path "$fluentd_base_dir\bin\fluentd")) {
447525
$tempdir = create_temp_dir -tempdir $tempdir
448526

449527
if ($with_dotnet_instrumentation) {
450-
if ((msi_installed -name "SignalFx .NET Tracing 64-bit") -Or (msi_installed -name "SignalFx .NET Tracing 32-bit")) {
528+
if ((is_msi_installed -name "SignalFx .NET Tracing 64-bit") -Or (is_msi_installed -name "SignalFx .NET Tracing 32-bit")) {
451529
throw "SignalFx .NET Instrumentation is already installed. Stop all instrumented applications and uninstall SignalFx Instrumentation for .NET before running this script again."
452530
}
453531
echo "Downloading Splunk Distribution of OpenTelemetry .NET ..."
@@ -590,7 +668,7 @@ if ($network_interface -Ne "") {
590668
set_service_environment $service_name $collector_env_vars
591669

592670
$message = "
593-
The Splunk OpenTelemetry Collector for Windows has been successfully installed.
671+
The $CollectorServiceDisplayName for Windows has been successfully installed.
594672
Make sure that your system's time is relatively accurate or else datapoints may not be accepted.
595673
The collector's main configuration file is located at $config_path,
596674
and the environment variables are stored in the $regkey registry key.
@@ -643,7 +721,7 @@ if ($with_fluentd) {
643721
install_msi -path "$fluentd_msi_path"
644722

645723
$message = "
646-
Fluentd has been installed and configured to forward log events to the Splunk OpenTelemetry Collector.
724+
Fluentd has been installed and configured to forward log events to the $CollectorServiceDisplayName.
647725
By default, all log events with the @SPLUNK label will be forwarded to the collector.
648726
649727
The main fluentd configuration file is located at $fluentd_config_path.
@@ -704,7 +782,7 @@ if ($with_dotnet_instrumentation) {
704782
}
705783

706784
$message = "
707-
Splunk Distribution of OpenTelemetry for .NET has been installed and configured to forward traces to the Splunk OpenTelemetry Collector.
785+
Splunk Distribution of OpenTelemetry for .NET has been installed and configured to forward traces to the $CollectorServiceDisplayName.
708786
By default, the .NET instrumentation will automatically generate telemetry only for .NET applications running on IIS.
709787
"
710788
echo "$message"
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright Splunk, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build windows
16+
17+
package windows_install_script
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"os/exec"
23+
"strings"
24+
"testing"
25+
"time"
26+
27+
"github.com/stretchr/testify/require"
28+
"golang.org/x/sys/windows/registry"
29+
"golang.org/x/sys/windows/svc"
30+
"golang.org/x/sys/windows/svc/mgr"
31+
)
32+
33+
const (
34+
// Old version to install first, this version by default is not installed as machine-wide.
35+
oldCollectorVersion = "0.94.0"
36+
// Service name
37+
serviceName = "splunk-otel-collector"
38+
// Service display name
39+
serviceDisplayName = "Splunk OpenTelemetry Collector"
40+
)
41+
42+
func TestUpgradeFromNonMachineWideVersion(t *testing.T) {
43+
t.Setenv("VERIFY_ACCESS_TOKEN", "false")
44+
45+
requireNoPendingFileOperations(t)
46+
47+
scm, err := mgr.Connect()
48+
require.NoError(t, err)
49+
defer scm.Disconnect()
50+
51+
t.Logf(" *** Installing old collector version %s", oldCollectorVersion)
52+
installCollector(t, oldCollectorVersion, "")
53+
verifyServiceExists(t, scm)
54+
verifyServiceState(t, scm, svc.Running)
55+
legacySvcVersion := getCurrentServiceVersion(t)
56+
require.Equal(t, oldCollectorVersion, legacySvcVersion)
57+
58+
msiInstallerPath := getFilePathFromEnvVar(t, "MSI_COLLECTOR_PATH")
59+
t.Logf(" *** Installing collector from %q", msiInstallerPath)
60+
installCollector(t, "", msiInstallerPath)
61+
verifyServiceExists(t, scm)
62+
verifyServiceState(t, scm, svc.Running)
63+
latestSvcVersion := getCurrentServiceVersion(t)
64+
require.NotEqual(t, oldCollectorVersion, latestSvcVersion)
65+
requireNoPendingFileOperations(t)
66+
}
67+
68+
func installCollector(t *testing.T, version string, msiPath string) {
69+
require.False(t, version == "" && msiPath == "", "Either version or msiPath must be provided")
70+
require.False(t, version != "" && msiPath != "", "Only one of version or msiPath should be provided")
71+
args := []string{
72+
"-ExecutionPolicy", "Bypass",
73+
"-File", getFilePathFromEnvVar(t, "INSTALL_SCRIPT_PATH"),
74+
"-access_token", "fake-token",
75+
}
76+
77+
if version != "" {
78+
args = append(args, "-collector_version", version)
79+
} else if msiPath != "" {
80+
args = append(args, "-msi_path", msiPath)
81+
} else {
82+
require.Fail(t, "Either version or msiPath must be provided")
83+
}
84+
85+
cmd := exec.Command("powershell.exe", args...)
86+
87+
output, err := cmd.CombinedOutput()
88+
t.Logf("Install output: %s", string(output))
89+
require.NoError(t, err, "Failed to install collector (version:%q msiPath:%q)", version, msiPath)
90+
}
91+
92+
func verifyServiceExists(t *testing.T, scm *mgr.Mgr) {
93+
service, err := scm.OpenService(serviceName)
94+
require.NoError(t, err)
95+
service.Close()
96+
}
97+
98+
func verifyServiceState(t *testing.T, scm *mgr.Mgr, desiredState svc.State) {
99+
service, err := scm.OpenService(serviceName)
100+
require.NoError(t, err)
101+
defer service.Close()
102+
103+
// Wait for the service to reach the running state
104+
require.Eventually(t, func() bool {
105+
status, err := service.Query()
106+
require.NoError(t, err)
107+
return status.State == desiredState
108+
}, 10*time.Second, 500*time.Millisecond, "Service failed to reach the desired state")
109+
}
110+
111+
func getCurrentServiceVersion(t *testing.T) string {
112+
// Read the service version from the registry, need to find the GUID registry key
113+
// given the service name.
114+
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`, registry.ALL_ACCESS)
115+
require.NoError(t, err)
116+
defer key.Close()
117+
118+
// Enumerate all subkeys to find the one that matches the service name
119+
subKeys, err := key.ReadSubKeyNames(0)
120+
require.NoError(t, err)
121+
122+
for _, subKey := range subKeys {
123+
subKeyPath := fmt.Sprintf(`Software\Microsoft\Windows\CurrentVersion\Uninstall\%s`, subKey)
124+
subKeyHandle, err := registry.OpenKey(registry.LOCAL_MACHINE, subKeyPath, registry.QUERY_VALUE)
125+
if err != nil {
126+
continue
127+
}
128+
defer subKeyHandle.Close()
129+
130+
displayName, _, err := subKeyHandle.GetStringValue("DisplayName")
131+
if err == nil && strings.Contains(displayName, serviceDisplayName) {
132+
// Found the subkey for the service, now get the version
133+
version, _, err := subKeyHandle.GetStringValue("DisplayVersion")
134+
require.NoError(t, err)
135+
return version
136+
}
137+
}
138+
139+
require.Fail(t, "Failed to find service version in registry")
140+
return ""
141+
}
142+
143+
func requireNoPendingFileOperations(t *testing.T) {
144+
// Check for pending file rename operations
145+
pendingFileRenameKey, err := registry.OpenKey(
146+
registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager`, registry.QUERY_VALUE)
147+
require.NoError(t, err)
148+
defer pendingFileRenameKey.Close()
149+
pendingFileRenameEntries, _, err := pendingFileRenameKey.GetStringsValue("PendingFileRenameOperations")
150+
if err != nil {
151+
require.ErrorIs(t, err, registry.ErrNotExist)
152+
}
153+
154+
for _, fileName := range pendingFileRenameEntries {
155+
if strings.Contains(strings.ToLower(fileName), "splunk") {
156+
require.Fail(t, "Found pending file rename: %s", fileName)
157+
}
158+
}
159+
}
160+
161+
func getFilePathFromEnvVar(t *testing.T, envVar string) string {
162+
filePath := os.Getenv(envVar)
163+
require.NotEmpty(t, filePath, "%s environment variable is not set", envVar)
164+
_, err := os.Stat(filePath)
165+
require.NoError(t, err, "File %s does not exist", filePath)
166+
if strings.Contains(filePath, " ") {
167+
filePath = "\"" + filePath + "\""
168+
}
169+
return filePath
170+
}

0 commit comments

Comments
 (0)