Skip to content

Commit 917aa99

Browse files
pjanottimowies
andauthored
Add basic MSI tests to CI (#753)
* Add github workflow for MSI tests * Find MSI location (temporary) * Fix path for upload-artifact for MSI * Add golang tests * Fix path to MSI * Temporary step to check MSI location * Fix MSI location * Debug powershell cmds * Testing powershell cmds * More pwsh debugging * Fix path (still with debugging) * Cleanup CI debug code * Remove debug step * Better name for action * remove "${{ }}" since it was not needed Co-authored-by: Moritz Wiesinger <[email protected]> --------- Co-authored-by: Moritz Wiesinger <[email protected]>
1 parent e1cbbc0 commit 917aa99

File tree

7 files changed

+290
-1
lines changed

7 files changed

+290
-1
lines changed

.github/workflows/base-ci-goreleaser.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,11 @@ jobs:
8787
name: linux-packages
8888
path: distributions/${{ inputs.distribution }}/dist/linux_amd64_v1/*
8989
if-no-files-found: error
90+
91+
- name: Upload MSI packages
92+
if: matrix.GOOS == 'windows' && matrix.GOARCH == 'amd64'
93+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
94+
with:
95+
name: msi-packages
96+
path: distributions/${{ inputs.distribution }}/dist/windows_amd64_v1/**/*.msi
97+
if-no-files-found: error

.github/workflows/ci-goreleaser-contrib.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ jobs:
4040
with:
4141
distribution: otelcol-contrib
4242
type: '[ "deb", "rpm" ]'
43+
44+
msi-tests:
45+
name: MSI tests
46+
needs: check-goreleaser
47+
uses: ./.github/workflows/msi-tests.yaml
48+
with:
49+
distribution: otelcol-contrib
50+
type: '[ "msi" ]'

.github/workflows/ci-goreleaser-core.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ on:
2323
- "go.mod"
2424
- "go.sum"
2525

26-
2726
jobs:
2827
check-goreleaser:
2928
name: Continuous Integration - Core - GoReleaser
@@ -41,3 +40,11 @@ jobs:
4140
with:
4241
distribution: otelcol
4342
type: '[ "deb", "rpm" ]'
43+
44+
msi-tests:
45+
name: MSI tests
46+
needs: check-goreleaser
47+
uses: ./.github/workflows/msi-tests.yaml
48+
with:
49+
distribution: otelcol
50+
type: '[ "msi" ]'

.github/workflows/msi-tests.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: MSI Tests
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
type:
7+
required: true
8+
type: string
9+
distribution:
10+
required: true
11+
type: string
12+
13+
jobs:
14+
msi-tests:
15+
name: MSI Tests
16+
runs-on: otel-windows-latest-8-cores
17+
strategy:
18+
matrix:
19+
type: ${{ fromJSON(inputs.type) }}
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23+
24+
- name: Download built artifacts
25+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
26+
with:
27+
name: msi-packages
28+
29+
- name: Set required environment variables for MSI tests
30+
run: |
31+
$ErrorActionPreference = 'Stop'
32+
$alt_config_path = Resolve-Path .\distributions\${{ inputs.distribution }}\config.yaml
33+
Test-Path $alt_config_path
34+
$msi_path = Resolve-Path .\msi\*\*.msi
35+
Test-Path $msi_path
36+
"MSI_TEST_ALTERNATE_CONFIG_FILE=$alt_config_path" | Out-File -FilePath $env:GITHUB_ENV -Append
37+
"MSI_TEST_COLLECTOR_PATH=$msi_path" | Out-File -FilePath $env:GITHUB_ENV -Append
38+
"MSI_TEST_COLLECTOR_SERVICE_NAME=${{ inputs.distribution }}" | Out-File -FilePath $env:GITHUB_ENV -Append
39+
40+
- name: Run the MSI tests
41+
working-directory: tests/msi
42+
run: |
43+
go test -timeout 15m -v ./...

tests/msi/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module msi
2+
3+
go 1.23
4+
5+
require (
6+
github.com/stretchr/testify v1.10.0
7+
golang.org/x/sys v0.27.0
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v3 v3.0.1 // indirect
14+
)

tests/msi/go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
8+
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
12+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

tests/msi/msi_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright The OpenTelemetry Authors
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 msi
18+
19+
import (
20+
"os"
21+
"os/exec"
22+
"path/filepath"
23+
"strings"
24+
"syscall"
25+
"testing"
26+
"time"
27+
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
"golang.org/x/sys/windows/svc"
31+
"golang.org/x/sys/windows/svc/mgr"
32+
)
33+
34+
// Test structure for MSI installation tests
35+
type msiTest struct {
36+
name string
37+
collectorServiceArgs string
38+
skipSvcStop bool
39+
}
40+
41+
func TestMSI(t *testing.T) {
42+
msiInstallerPath := getInstallerPath(t)
43+
44+
tests := []msiTest{
45+
{
46+
name: "default",
47+
},
48+
{
49+
name: "custom",
50+
collectorServiceArgs: "--config " + quotedIfRequired(getAlternateConfigFile(t)),
51+
skipSvcStop: true,
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
runMsiTest(t, tt, msiInstallerPath)
58+
})
59+
}
60+
}
61+
62+
func runMsiTest(t *testing.T, test msiTest, msiInstallerPath string) {
63+
// Build the MSI installation arguments and include the MSI properties map.
64+
installLogFile := filepath.Join(os.TempDir(), "install.log")
65+
args := []string{"/i", msiInstallerPath, "/qn", "/l*v", installLogFile}
66+
67+
serviceArgs := quotedIfRequired(test.collectorServiceArgs)
68+
if test.collectorServiceArgs != "" {
69+
args = append(args, "COLLECTOR_SVC_ARGS="+serviceArgs)
70+
}
71+
72+
// Run the MSI installer
73+
installCmd := exec.Command("msiexec")
74+
75+
// msiexec is one of the noticeable exceptions about how to format the parameters,
76+
// see https://pkg.go.dev/os/exec#Command, so we need to join the args manually.
77+
cmdLine := strings.Join(args, " ")
78+
installCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec " + cmdLine}
79+
err := installCmd.Run()
80+
if err != nil {
81+
logText, _ := os.ReadFile(installLogFile)
82+
t.Log(string(logText))
83+
}
84+
t.Logf("Install command: %s", installCmd.SysProcAttr.CmdLine)
85+
require.NoError(t, err, "Failed to install the MSI: %v\nArgs: %v", err, args)
86+
87+
defer func() {
88+
// Uninstall the MSI
89+
uninstallCmd := exec.Command("msiexec")
90+
uninstallCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec /x " + msiInstallerPath + " /qn"}
91+
err := uninstallCmd.Run()
92+
t.Logf("Uninstall command: %s", uninstallCmd.SysProcAttr.CmdLine)
93+
require.NoError(t, err, "Failed to uninstall the MSI: %v", err)
94+
}()
95+
96+
// Verify the service
97+
scm, err := mgr.Connect()
98+
require.NoError(t, err)
99+
defer scm.Disconnect()
100+
101+
collectorSvcName := getServiceName(t)
102+
service, err := scm.OpenService(collectorSvcName)
103+
require.NoError(t, err)
104+
defer service.Close()
105+
106+
// Wait for the service to reach the running state
107+
require.Eventually(t, func() bool {
108+
status, err := service.Query()
109+
require.NoError(t, err)
110+
return status.State == svc.Running
111+
}, 10*time.Second, 500*time.Millisecond, "Failed to start the service")
112+
113+
if !test.skipSvcStop {
114+
defer func() {
115+
_, err = service.Control(svc.Stop)
116+
require.NoError(t, err)
117+
118+
require.Eventually(t, func() bool {
119+
status, err := service.Query()
120+
require.NoError(t, err)
121+
return status.State == svc.Stopped
122+
}, 10*time.Second, 500*time.Millisecond, "Failed to stop the service")
123+
}()
124+
}
125+
126+
assertServiceCommand(t, collectorSvcName, serviceArgs)
127+
}
128+
129+
func assertServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) {
130+
// Verify the service command
131+
actualCommand := getServiceCommand(t, serviceName)
132+
expectedCommand := expectedServiceCommand(t, serviceName, collectorServiceArgs)
133+
assert.Equal(t, expectedCommand, actualCommand)
134+
}
135+
136+
func getServiceCommand(t *testing.T, serviceName string) string {
137+
scm, err := mgr.Connect()
138+
require.NoError(t, err)
139+
defer scm.Disconnect()
140+
141+
service, err := scm.OpenService(serviceName)
142+
require.NoError(t, err)
143+
defer service.Close()
144+
145+
config, err := service.Config()
146+
require.NoError(t, err)
147+
148+
return config.BinaryPathName
149+
}
150+
151+
func expectedServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) string {
152+
programFilesDir := os.Getenv("PROGRAMFILES")
153+
require.NotEmpty(t, programFilesDir, "PROGRAMFILES environment variable is not set")
154+
155+
collectorDir := filepath.Join(programFilesDir, "OpenTelemetry Collector")
156+
collectorExe := filepath.Join(collectorDir, serviceName) + ".exe"
157+
158+
if collectorServiceArgs == "" {
159+
collectorServiceArgs = "--config " + quotedIfRequired(filepath.Join(collectorDir, "config.yaml"))
160+
} else {
161+
// Remove any quotation added for the msiexec command line
162+
collectorServiceArgs = strings.Trim(collectorServiceArgs, "\"")
163+
collectorServiceArgs = strings.ReplaceAll(collectorServiceArgs, "\"\"", "\"")
164+
}
165+
166+
return quotedIfRequired(collectorExe) + " " + collectorServiceArgs
167+
}
168+
169+
func getServiceName(t *testing.T) string {
170+
serviceName := os.Getenv("MSI_TEST_COLLECTOR_SERVICE_NAME")
171+
require.NotEmpty(t, serviceName, "MSI_TEST_COLLECTOR_SERVICE_NAME environment variable is not set")
172+
return serviceName
173+
}
174+
175+
func getInstallerPath(t *testing.T) string {
176+
msiInstallerPath := os.Getenv("MSI_TEST_COLLECTOR_PATH")
177+
require.NotEmpty(t, msiInstallerPath, "MSI_TEST_COLLECTOR_PATH environment variable is not set")
178+
_, err := os.Stat(msiInstallerPath)
179+
require.NoError(t, err)
180+
return msiInstallerPath
181+
}
182+
183+
func getAlternateConfigFile(t *testing.T) string {
184+
alternateConfigFile := os.Getenv("MSI_TEST_ALTERNATE_CONFIG_FILE")
185+
require.NotEmpty(t, alternateConfigFile, "MSI_TEST_ALTERNATE_CONFIG_FILE environment variable is not set")
186+
_, err := os.Stat(alternateConfigFile)
187+
require.NoError(t, err)
188+
return alternateConfigFile
189+
}
190+
191+
func quotedIfRequired(s string) string {
192+
if strings.Contains(s, "\"") || strings.Contains(s, " ") {
193+
s = strings.ReplaceAll(s, "\"", "\"\"")
194+
return "\"" + s + "\""
195+
}
196+
return s
197+
}

0 commit comments

Comments
 (0)