Skip to content

Commit

Permalink
Add basic MSI tests to CI (#753)
Browse files Browse the repository at this point in the history
* 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 <moritz.wiesinger@dynatrace.com>

---------

Co-authored-by: Moritz Wiesinger <moritz.wiesinger@dynatrace.com>
  • Loading branch information
pjanotti and mowies authored Dec 18, 2024
1 parent e1cbbc0 commit 917aa99
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .github/workflows/base-ci-goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,11 @@ jobs:
name: linux-packages
path: distributions/${{ inputs.distribution }}/dist/linux_amd64_v1/*
if-no-files-found: error

- name: Upload MSI packages
if: matrix.GOOS == 'windows' && matrix.GOARCH == 'amd64'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: msi-packages
path: distributions/${{ inputs.distribution }}/dist/windows_amd64_v1/**/*.msi
if-no-files-found: error
8 changes: 8 additions & 0 deletions .github/workflows/ci-goreleaser-contrib.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ jobs:
with:
distribution: otelcol-contrib
type: '[ "deb", "rpm" ]'

msi-tests:
name: MSI tests
needs: check-goreleaser
uses: ./.github/workflows/msi-tests.yaml
with:
distribution: otelcol-contrib
type: '[ "msi" ]'
9 changes: 8 additions & 1 deletion .github/workflows/ci-goreleaser-core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ on:
- "go.mod"
- "go.sum"


jobs:
check-goreleaser:
name: Continuous Integration - Core - GoReleaser
Expand All @@ -41,3 +40,11 @@ jobs:
with:
distribution: otelcol
type: '[ "deb", "rpm" ]'

msi-tests:
name: MSI tests
needs: check-goreleaser
uses: ./.github/workflows/msi-tests.yaml
with:
distribution: otelcol
type: '[ "msi" ]'
43 changes: 43 additions & 0 deletions .github/workflows/msi-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: MSI Tests

on:
workflow_call:
inputs:
type:
required: true
type: string
distribution:
required: true
type: string

jobs:
msi-tests:
name: MSI Tests
runs-on: otel-windows-latest-8-cores
strategy:
matrix:
type: ${{ fromJSON(inputs.type) }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Download built artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: msi-packages

- name: Set required environment variables for MSI tests
run: |
$ErrorActionPreference = 'Stop'
$alt_config_path = Resolve-Path .\distributions\${{ inputs.distribution }}\config.yaml
Test-Path $alt_config_path
$msi_path = Resolve-Path .\msi\*\*.msi
Test-Path $msi_path
"MSI_TEST_ALTERNATE_CONFIG_FILE=$alt_config_path" | Out-File -FilePath $env:GITHUB_ENV -Append
"MSI_TEST_COLLECTOR_PATH=$msi_path" | Out-File -FilePath $env:GITHUB_ENV -Append
"MSI_TEST_COLLECTOR_SERVICE_NAME=${{ inputs.distribution }}" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Run the MSI tests
working-directory: tests/msi
run: |
go test -timeout 15m -v ./...
14 changes: 14 additions & 0 deletions tests/msi/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module msi

go 1.23

require (
github.com/stretchr/testify v1.10.0
golang.org/x/sys v0.27.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 12 additions & 0 deletions tests/msi/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
197 changes: 197 additions & 0 deletions tests/msi/msi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows

package msi

import (
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)

// Test structure for MSI installation tests
type msiTest struct {
name string
collectorServiceArgs string
skipSvcStop bool
}

func TestMSI(t *testing.T) {
msiInstallerPath := getInstallerPath(t)

tests := []msiTest{
{
name: "default",
},
{
name: "custom",
collectorServiceArgs: "--config " + quotedIfRequired(getAlternateConfigFile(t)),
skipSvcStop: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runMsiTest(t, tt, msiInstallerPath)
})
}
}

func runMsiTest(t *testing.T, test msiTest, msiInstallerPath string) {
// Build the MSI installation arguments and include the MSI properties map.
installLogFile := filepath.Join(os.TempDir(), "install.log")
args := []string{"/i", msiInstallerPath, "/qn", "/l*v", installLogFile}

serviceArgs := quotedIfRequired(test.collectorServiceArgs)
if test.collectorServiceArgs != "" {
args = append(args, "COLLECTOR_SVC_ARGS="+serviceArgs)
}

// Run the MSI installer
installCmd := exec.Command("msiexec")

// msiexec is one of the noticeable exceptions about how to format the parameters,
// see https://pkg.go.dev/os/exec#Command, so we need to join the args manually.
cmdLine := strings.Join(args, " ")
installCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec " + cmdLine}
err := installCmd.Run()
if err != nil {
logText, _ := os.ReadFile(installLogFile)
t.Log(string(logText))
}
t.Logf("Install command: %s", installCmd.SysProcAttr.CmdLine)
require.NoError(t, err, "Failed to install the MSI: %v\nArgs: %v", err, args)

defer func() {
// Uninstall the MSI
uninstallCmd := exec.Command("msiexec")
uninstallCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec /x " + msiInstallerPath + " /qn"}
err := uninstallCmd.Run()
t.Logf("Uninstall command: %s", uninstallCmd.SysProcAttr.CmdLine)
require.NoError(t, err, "Failed to uninstall the MSI: %v", err)
}()

// Verify the service
scm, err := mgr.Connect()
require.NoError(t, err)
defer scm.Disconnect()

collectorSvcName := getServiceName(t)
service, err := scm.OpenService(collectorSvcName)
require.NoError(t, err)
defer service.Close()

// Wait for the service to reach the running state
require.Eventually(t, func() bool {
status, err := service.Query()
require.NoError(t, err)
return status.State == svc.Running
}, 10*time.Second, 500*time.Millisecond, "Failed to start the service")

if !test.skipSvcStop {
defer func() {
_, err = service.Control(svc.Stop)
require.NoError(t, err)

require.Eventually(t, func() bool {
status, err := service.Query()
require.NoError(t, err)
return status.State == svc.Stopped
}, 10*time.Second, 500*time.Millisecond, "Failed to stop the service")
}()
}

assertServiceCommand(t, collectorSvcName, serviceArgs)
}

func assertServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) {
// Verify the service command
actualCommand := getServiceCommand(t, serviceName)
expectedCommand := expectedServiceCommand(t, serviceName, collectorServiceArgs)
assert.Equal(t, expectedCommand, actualCommand)
}

func getServiceCommand(t *testing.T, serviceName string) string {
scm, err := mgr.Connect()
require.NoError(t, err)
defer scm.Disconnect()

service, err := scm.OpenService(serviceName)
require.NoError(t, err)
defer service.Close()

config, err := service.Config()
require.NoError(t, err)

return config.BinaryPathName
}

func expectedServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) string {
programFilesDir := os.Getenv("PROGRAMFILES")
require.NotEmpty(t, programFilesDir, "PROGRAMFILES environment variable is not set")

collectorDir := filepath.Join(programFilesDir, "OpenTelemetry Collector")
collectorExe := filepath.Join(collectorDir, serviceName) + ".exe"

if collectorServiceArgs == "" {
collectorServiceArgs = "--config " + quotedIfRequired(filepath.Join(collectorDir, "config.yaml"))
} else {
// Remove any quotation added for the msiexec command line
collectorServiceArgs = strings.Trim(collectorServiceArgs, "\"")
collectorServiceArgs = strings.ReplaceAll(collectorServiceArgs, "\"\"", "\"")
}

return quotedIfRequired(collectorExe) + " " + collectorServiceArgs
}

func getServiceName(t *testing.T) string {
serviceName := os.Getenv("MSI_TEST_COLLECTOR_SERVICE_NAME")
require.NotEmpty(t, serviceName, "MSI_TEST_COLLECTOR_SERVICE_NAME environment variable is not set")
return serviceName
}

func getInstallerPath(t *testing.T) string {
msiInstallerPath := os.Getenv("MSI_TEST_COLLECTOR_PATH")
require.NotEmpty(t, msiInstallerPath, "MSI_TEST_COLLECTOR_PATH environment variable is not set")
_, err := os.Stat(msiInstallerPath)
require.NoError(t, err)
return msiInstallerPath
}

func getAlternateConfigFile(t *testing.T) string {
alternateConfigFile := os.Getenv("MSI_TEST_ALTERNATE_CONFIG_FILE")
require.NotEmpty(t, alternateConfigFile, "MSI_TEST_ALTERNATE_CONFIG_FILE environment variable is not set")
_, err := os.Stat(alternateConfigFile)
require.NoError(t, err)
return alternateConfigFile
}

func quotedIfRequired(s string) string {
if strings.Contains(s, "\"") || strings.Contains(s, " ") {
s = strings.ReplaceAll(s, "\"", "\"\"")
return "\"" + s + "\""
}
return s
}

0 comments on commit 917aa99

Please sign in to comment.