Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic MSI tests to CI #753

Merged
merged 25 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e48e506
Add github workflow for MSI tests
pjanotti Nov 26, 2024
10607bd
Find MSI location (temporary)
pjanotti Nov 26, 2024
51d43d1
Fix path for upload-artifact for MSI
pjanotti Nov 26, 2024
2134da7
Add golang tests
pjanotti Dec 2, 2024
2ff95d7
Merge branch 'main' into add-msi-tests
pjanotti Dec 2, 2024
ccf3dbd
Merge branch 'add-msi-tests' of github.com:pjanotti/opentelemetry-col…
pjanotti Dec 2, 2024
a115df7
Fix path to MSI
pjanotti Dec 3, 2024
438d181
Temporary step to check MSI location
pjanotti Dec 3, 2024
7df27a8
Fix MSI location
pjanotti Dec 3, 2024
711b38a
Debug powershell cmds
pjanotti Dec 3, 2024
a194949
Testing powershell cmds
pjanotti Dec 3, 2024
9b507fc
More pwsh debugging
pjanotti Dec 3, 2024
8ed952c
Fix path (still with debugging)
pjanotti Dec 3, 2024
5b15d64
Cleanup CI debug code
pjanotti Dec 3, 2024
6367017
Merge branch 'main' into add-msi-tests
pjanotti Dec 5, 2024
b2c452e
Remove debug step
pjanotti Dec 5, 2024
4e9364a
Merge branch 'add-msi-tests' of github.com:pjanotti/opentelemetry-col…
pjanotti Dec 5, 2024
f0293b0
Better name for action
pjanotti Dec 5, 2024
56d5433
Merge branch 'main' into add-msi-tests
pjanotti Dec 10, 2024
6f31771
Merge branch 'main' into add-msi-tests
pjanotti Dec 11, 2024
bbac6f7
Merge branch 'main' into add-msi-tests
pjanotti Dec 12, 2024
e830fa4
Merge branch 'main' into add-msi-tests
pjanotti Dec 13, 2024
eab9e88
Merge branch 'main' into add-msi-tests
pjanotti Dec 16, 2024
31c8ba6
remove "${{ }}" since it was not needed
pjanotti Dec 18, 2024
656acfd
Merge branch 'main' into add-msi-tests
pjanotti Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' }}
pjanotti marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading