Merge pull request #25522 from l0rd/fix-wsl-check

Fix WSL checks and run unit tests in CI
This commit is contained in:
openshift-merge-bot[bot] 2025-03-24 01:36:29 +00:00 committed by GitHub
commit 7f11ce85e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 337 additions and 44 deletions

View File

@ -558,6 +558,44 @@ unit_test_task:
always: *logs_artifacts always: *logs_artifacts
unit_test_windows_task:
name: "Unit tests on Windows"
alias: unit_test_windows
# Docs: ./contrib/cirrus/CIModes.md (Cirrus Task contexts and runtime modes)
# only when: - main rules (see doc above); or
# - unit test files are changed (contains a false positves such as test/e2e/
# but that should not be an issue, it only runs when it doesn't have to)
# - actual source code changed
only_if: >-
$CIRRUS_PR == '' ||
$CIRRUS_CHANGE_TITLE =~ '.*CI:ALL.*' ||
changesInclude('.cirrus.yml', 'Makefile', 'contrib/cirrus/**', 'vendor/**', 'test/tools/**', 'test/registries*.conf', 'hack/**', 'version/rawversion/*') ||
changesInclude('winmake.ps1') ||
changesInclude('**/*_test.go') ||
(changesInclude('**/*.go', '**/*.c', '**/*.h') && !changesIncludeOnly('test/**', 'pkg/machine/e2e/**'))
# Special case, we do not run macos/windows builds on rhel branches.
# Thus the machine task should not be run too, while we use only_if
# everywhere to do so here it would mean we would need duplicate the
# full big only_if condition which is more difficult to maintain so
# use the skip here.
skip: &skip_rhel_release |
$CIRRUS_BRANCH =~ 'v[0-9\.]+-rhel' ||
$CIRRUS_BASE_BRANCH =~ 'v[0-9\.]+-rhel'
depends_on: *build
ec2_instance: *windows
timeout_in: 20m
env:
<<: *winenv
TEST_FLAVOR: unit
clone_script: *winclone
main_script: ".\\repo\\contrib\\cirrus\\win-unit-main.ps1"
always:
# Required for `contrib/cirrus/logformatter` to work properly
html_artifacts:
path: ./*.html
type: text/html
apiv2_test_task: apiv2_test_task:
name: "APIv2 test on $DISTRO_NV ($PRIV_NAME)" name: "APIv2 test on $DISTRO_NV ($PRIV_NAME)"
alias: apiv2_test alias: apiv2_test
@ -760,9 +798,7 @@ podman_machine_windows_task:
# everywhere to do so here it would mean we would need duplicate the # everywhere to do so here it would mean we would need duplicate the
# full big only_if condition which is more difficult to maintain so # full big only_if condition which is more difficult to maintain so
# use the skip here. # use the skip here.
skip: &skip_rhel_release | skip: *skip_rhel_release
$CIRRUS_BRANCH =~ 'v[0-9\.]+-rhel' ||
$CIRRUS_BASE_BRANCH =~ 'v[0-9\.]+-rhel'
depends_on: *build depends_on: *build
ec2_instance: ec2_instance:
<<: *windows <<: *windows
@ -1060,6 +1096,7 @@ success_task:
- win_installer - win_installer
- docker-py_test - docker-py_test
- unit_test - unit_test
- unit_test_windows
- apiv2_test - apiv2_test
- compose_test - compose_test
- local_integration_test - local_integration_test

View File

@ -652,7 +652,7 @@ localunit: test/goecho/goecho test/version/version
UNIT=1 $(GINKGO) \ UNIT=1 $(GINKGO) \
-r \ -r \
$(TESTFLAGS) \ $(TESTFLAGS) \
--skip-package test/e2e,pkg/bindings,hack,pkg/machine/e2e \ --skip-package test/e2e,pkg/bindings,hack,pkg/machine/e2e,pkg/machine/wsl \
--cover \ --cover \
--covermode atomic \ --covermode atomic \
--coverprofile coverprofile \ --coverprofile coverprofile \

View File

@ -1,3 +1,5 @@
//go:build !windows
// most of these validate and parse functions have been taken from projectatomic/docker // most of these validate and parse functions have been taken from projectatomic/docker
// and modified for cri-o // and modified for cri-o
package parse package parse

View File

@ -0,0 +1,13 @@
#!/usr/bin/env powershell
. $PSScriptRoot\win-lib.ps1
if ($Env:CI -eq "true") {
Push-Location "$ENV:CIRRUS_WORKING_DIR\repo"
} else {
Push-Location $PSScriptRoot\..\..
}
Run-Command ".\winmake.ps1 localunit"
Pop-Location

View File

@ -1,3 +1,5 @@
//go:build !windows
package auth package auth
import ( import (

View File

@ -3,15 +3,41 @@
package machine package machine
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
"golang.org/x/sys/windows"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// shortPathToLongPath converts a Windows short path (C:\PROGRA~1) to its
// long path equivalent (C:\Program Files). It returns an error if shortPath
// doesn't exist.
func shortPathToLongPath(shortPath string) (string, error) {
shortPathPtr, err := windows.UTF16PtrFromString(shortPath)
if err != nil {
return "", err
}
len, err := windows.GetLongPathName(shortPathPtr, nil, 0)
if err != nil {
return "", err
}
if len == 0 {
return "", fmt.Errorf("failed to get buffer size for path: %s", shortPath)
}
longPathPtr := &(make([]uint16, len)[0])
_, err = windows.GetLongPathName(shortPathPtr, longPathPtr, len)
if err != nil {
return "", err
}
return windows.UTF16PtrToString(longPathPtr), nil
}
// CreateNewItemWithPowerShell creates a new item using PowerShell. // CreateNewItemWithPowerShell creates a new item using PowerShell.
// It's an helper to easily create junctions on Windows (as well as other file types). // It's an helper to easily create junctions on Windows (as well as other file types).
// It constructs a PowerShell command to create a new item at the specified path with the given item type. // It constructs a PowerShell command to create a new item at the specified path with the given item type.
@ -23,15 +49,21 @@ import (
// - itemType: The type of the item to be created (e.g., "File", "SymbolicLink", "Junction"). // - itemType: The type of the item to be created (e.g., "File", "SymbolicLink", "Junction").
// - target: The target for the new item, if applicable. // - target: The target for the new item, if applicable.
func CreateNewItemWithPowerShell(t *testing.T, path string, itemType string, target string) { func CreateNewItemWithPowerShell(t *testing.T, path string, itemType string, target string) {
var pwshCmd string var pwshCmd, pwshPath string
// Look for Powershell 7 first as it allow Symlink creation for non-admins too
pwshPath, err := exec.LookPath("pwsh.exe")
if err != nil {
// Use Powershell 5 that is always present
pwshPath = "powershell.exe"
}
pwshCmd = "New-Item -Path " + path + " -ItemType " + itemType pwshCmd = "New-Item -Path " + path + " -ItemType " + itemType
if target != "" { if target != "" {
pwshCmd += " -Target " + target pwshCmd += " -Target " + target
} }
cmd := exec.Command("pwsh", "-Command", pwshCmd) cmd := exec.Command(pwshPath, "-Command", pwshCmd)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() err = cmd.Run()
require.NoError(t, err) require.NoError(t, err)
} }
@ -45,13 +77,16 @@ func CreateNewItemWithPowerShell(t *testing.T, path string, itemType string, tar
// with filepath.EvalSymlink(). // with filepath.EvalSymlink().
func TestEvalSymlinksOrClean(t *testing.T) { func TestEvalSymlinksOrClean(t *testing.T) {
// Create a temporary directory to store the normal file // Create a temporary directory to store the normal file
normalFileDir := t.TempDir() normalFileDir, err := shortPathToLongPath(t.TempDir())
require.NoError(t, err)
// Create a temporary directory to store the (hard/sym)link files // Create a temporary directory to store the (hard/sym)link files
linkFilesDir := t.TempDir() linkFilesDir, err := shortPathToLongPath(t.TempDir())
require.NoError(t, err)
// Create a temporary directory where the mount point will be created // Create a temporary directory where the mount point will be created
mountPointDir := t.TempDir() mountPointDir, err := shortPathToLongPath(t.TempDir())
require.NoError(t, err)
// Create a normal file // Create a normal file
normalFile := filepath.Join(normalFileDir, "testFile") normalFile := filepath.Join(normalFileDir, "testFile")

View File

@ -317,12 +317,12 @@ func checkAndInstallWSL(reExec bool) (bool, error) {
admin := HasAdminRights() admin := HasAdminRights()
if !IsWSLFeatureEnabled() { if !wutil.IsWSLFeatureEnabled() {
return false, attemptFeatureInstall(reExec, admin) return false, attemptFeatureInstall(reExec, admin)
} }
skip := false skip := false
if reExec && !admin { if !reExec && !admin {
fmt.Println("Launching WSL Kernel Install...") fmt.Println("Launching WSL Kernel Install...")
if err := launchElevate(wslInstallKernel); err != nil { if err := launchElevate(wslInstallKernel); err != nil {
return false, err return false, err
@ -363,11 +363,11 @@ func attemptFeatureInstall(reExec, admin bool) error {
message += "NOTE: A system reboot will be required as part of this process. " + message += "NOTE: A system reboot will be required as part of this process. " +
"If you prefer, you may abort now, and perform a manual installation using the \"wsl --install\" command." "If you prefer, you may abort now, and perform a manual installation using the \"wsl --install\" command."
if reExec && MessageBox(message, "Podman Machine", false) != 1 { if !reExec && MessageBox(message, "Podman Machine", false) != 1 {
return errors.New("the WSL installation aborted") return errors.New("the WSL installation aborted")
} }
if reExec && !admin { if !reExec && !admin {
return launchElevate("install the Windows WSL Features") return launchElevate("install the Windows WSL Features")
} }
@ -622,10 +622,6 @@ func obtainGlobalConfigLock() (*fileLock, error) {
return lockFile(filepath.Join(lockDir, "podman-config.lck")) return lockFile(filepath.Join(lockDir, "podman-config.lck"))
} }
func IsWSLFeatureEnabled() bool {
return wutil.SilentExec(wutil.FindWSL(), "--set-default-version", "2") == nil
}
func isWSLRunning(dist string) (bool, error) { func isWSLRunning(dist string) (bool, error) {
return wslCheckExists(dist, true) return wslCheckExists(dist, true)
} }

View File

@ -19,16 +19,26 @@ import (
) )
var ( var (
once sync.Once onceFind, onceStatus sync.Once
wslPath string wslPath string
status wslStatus
wslNotInstalledMessages = []string{"kernel file is not found", "The Windows Subsystem for Linux is not installed"}
vmpDisabledMessages = []string{"enable the Virtual Machine Platform Windows feature", "Enable \"Virtual Machine Platform\""}
wslDisabledMessages = []string{"enable the \"Windows Subsystem for Linux\" optional component"}
) )
type wslStatus struct {
installed bool
vmpFeatureEnabled bool
wslFeatureEnabled bool
}
func FindWSL() string { func FindWSL() string {
// At the time of this writing, a defect appeared in the OS preinstalled WSL executable // At the time of this writing, a defect appeared in the OS preinstalled WSL executable
// where it no longer reliably locates the preferred Windows App Store variant. // where it no longer reliably locates the preferred Windows App Store variant.
// //
// Manually discover (and cache) the wsl.exe location to bypass the problem // Manually discover (and cache) the wsl.exe location to bypass the problem
once.Do(func() { onceFind.Do(func() {
var locs []string var locs []string
// Prefer Windows App Store version // Prefer Windows App Store version
@ -87,24 +97,44 @@ func SilentExecCmd(command string, args ...string) *exec.Cmd {
return cmd return cmd
} }
func parseWSLStatus() wslStatus {
onceStatus.Do(func() {
status = wslStatus{
installed: false,
vmpFeatureEnabled: false,
wslFeatureEnabled: false,
}
cmd := SilentExecCmd(FindWSL(), "--status")
out, err := cmd.StdoutPipe()
cmd.Stderr = nil
if err != nil {
return
}
if err = cmd.Start(); err != nil {
return
}
status = matchOutputLine(out)
if err := cmd.Wait(); err != nil {
return
}
})
return status
}
func IsWSLInstalled() bool { func IsWSLInstalled() bool {
cmd := SilentExecCmd(FindWSL(), "--status") status := parseWSLStatus()
out, err := cmd.StdoutPipe() return status.installed && status.vmpFeatureEnabled
cmd.Stderr = nil }
if err != nil {
func IsWSLFeatureEnabled() bool {
if SilentExec(FindWSL(), "--set-default-version", "2") != nil {
return false return false
} }
if err = cmd.Start(); err != nil { status := parseWSLStatus()
return false return status.vmpFeatureEnabled
}
kernelNotFound := matchOutputLine(out, "kernel file is not found")
if err := cmd.Wait(); err != nil {
return false
}
return !kernelNotFound
} }
func IsWSLStoreVersionInstalled() bool { func IsWSLStoreVersionInstalled() bool {
@ -118,13 +148,30 @@ func IsWSLStoreVersionInstalled() bool {
return true return true
} }
func matchOutputLine(output io.ReadCloser, match string) bool { func matchOutputLine(output io.ReadCloser) wslStatus {
status := wslStatus{
installed: true,
vmpFeatureEnabled: true,
wslFeatureEnabled: true,
}
scanner := bufio.NewScanner(transform.NewReader(output, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) scanner := bufio.NewScanner(transform.NewReader(output, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.Contains(line, match) { for _, match := range wslNotInstalledMessages {
return true if strings.Contains(line, match) {
status.installed = false
}
}
for _, match := range vmpDisabledMessages {
if strings.Contains(line, match) {
status.vmpFeatureEnabled = false
}
}
for _, match := range wslDisabledMessages {
if strings.Contains(line, match) {
status.wslFeatureEnabled = false
}
} }
} }
return false return status
} }

View File

@ -0,0 +1,145 @@
//go:build windows
package wutil
import (
"github.com/stretchr/testify/assert"
"golang.org/x/text/encoding/unicode"
"io"
"strings"
"testing"
)
const (
WSL1InstalledWithWSLAndVMPEnabled = `Default Version: 1`
WSL2InstalledWithWSLAndVMPEnabled = `Default Version: 2`
WSL1NotInstalled = `Default Version: 1
The Windows Subsystem for Linux kernel can be manually updated with 'wsl --update', but automatic updates cannot occur due to your system settings.
To receive automatic kernel updates, please enable the Windows Update setting: 'Receive updates for other Microsoft products when you update Windows'.
For more information please visit https://aka.ms/wsl2kernel.
The WSL 2 kernel file is not found. To update or restore the kernel please run 'wsl --update'.`
WSL2NotInstalled = `The Windows Subsystem for Linux is not installed. You can install by running 'wsl.exe --install'.
For more information please visit https://aka.ms/wslinstall`
WSL2InstalledWithWSLDisabled = `Default Version: 2
WSL1 is not supported with your current machine configuration.
Please enable the "Windows Subsystem for Linux" optional component to use WSL1.`
WSL2InstalledWithVMPDisabled = `Default Version: 2
WSL2 is not supported with your current machine configuration.
Please enable the "Virtual Machine Platform" optional component and ensure virtualization is enabled in the BIOS.
Enable "Virtual Machine Platform" by running: wsl.exe --install --no-distribution
For information please visit https://aka.ms/enablevirtualization`
WSL2InstalledWithWSLAndVMPDisabled = `Default Version: 2
WSL1 is not supported with your current machine configuration.
Please enable the "Windows Subsystem for Linux" optional component to use WSL1.
WSL2 is not supported with your current machine configuration.
Please enable the "Virtual Machine Platform" optional component and ensure virtualization is enabled in the BIOS.
Enable "Virtual Machine Platform" by running: wsl.exe --install --no-distribution
For information please visit https://aka.ms/enablevirtualization`
WSL1InstalledWithVMPDisabled = `Default Version: 1
Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.
For information please visit https://aka.ms/enablevirtualization`
WSL1InstalledWithWSLDisabled = `Default Version: 1
WSL1 is not supported with your current machine configuration.
Please enable the "Windows Subsystem for Linux" optional component to use WSL1.`
)
func TestMatchOutputLine(t *testing.T) {
tests := []struct {
winVariant string
statusOutput string
want wslStatus
}{
{
"WSL1 configured and both Virtual Machine Platform enabled and Windows Subsystem for Linux are enabled",
WSL1InstalledWithWSLAndVMPEnabled,
wslStatus{
installed: true,
vmpFeatureEnabled: true,
wslFeatureEnabled: true,
},
},
{
"WSL2 configured and both Virtual Machine Platform enabled and Windows Subsystem for Linux enabled",
WSL2InstalledWithWSLAndVMPEnabled,
wslStatus{
installed: true,
vmpFeatureEnabled: true,
wslFeatureEnabled: true,
},
},
{
"WSL not installed (was previously configured as version 1)",
WSL1NotInstalled,
wslStatus{
installed: false,
vmpFeatureEnabled: true,
wslFeatureEnabled: true,
},
},
{
"WSL not installed (was previously configured as version 2)",
WSL2NotInstalled,
wslStatus{
installed: false,
vmpFeatureEnabled: true,
wslFeatureEnabled: true,
},
},
{
"WSL2 configured and Virtual Machine Platform is enabled but Windows Subsystem for Linux is disabled",
WSL2InstalledWithWSLDisabled,
wslStatus{
installed: true,
vmpFeatureEnabled: true,
wslFeatureEnabled: false,
},
},
{
"WSL2 configured and Virtual Machine Platform is disabled but Windows Subsystem for Linux is enabled",
WSL2InstalledWithVMPDisabled,
wslStatus{
installed: true,
vmpFeatureEnabled: false,
wslFeatureEnabled: true,
},
},
{
"WSL2 configured and both Virtual Machine Platform and Windows Subsystem for Linux are disabled",
WSL2InstalledWithWSLAndVMPDisabled,
wslStatus{
installed: true,
vmpFeatureEnabled: false,
wslFeatureEnabled: false,
},
},
{
"WSL1 configured and Virtual Machine Platform is disabled but Windows Subsystem for Linux is enabled",
WSL1InstalledWithVMPDisabled,
wslStatus{
installed: true,
vmpFeatureEnabled: false,
wslFeatureEnabled: true,
},
},
{
"WSL1 configured and Virtual Machine Platform is enabled but Windows Subsystem for Linux is disabled",
WSL1InstalledWithWSLDisabled,
wslStatus{
installed: true,
vmpFeatureEnabled: true,
wslFeatureEnabled: false,
},
},
}
for _, tt := range tests {
t.Run(tt.winVariant, func(t *testing.T) {
encoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder()
encodedOutput, err := encoder.String(tt.statusOutput)
assert.Nil(t, err)
reader := io.NopCloser(strings.NewReader(encodedOutput))
assert.Equal(t, tt.want, matchOutputLine(reader))
})
}
}

View File

@ -3,6 +3,7 @@ package util
import ( import (
"fmt" "fmt"
"math" "math"
"runtime"
"sort" "sort"
"testing" "testing"
"time" "time"
@ -827,6 +828,9 @@ func TestProcessOptions(t *testing.T) {
} }
func TestGetRootlessPauseProcessPidPath(t *testing.T) { func TestGetRootlessPauseProcessPidPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Not implemented on Windows")
}
dir, err := GetRootlessPauseProcessPidPath() dir, err := GetRootlessPauseProcessPidPath()
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEqual(t, dir, "libpod/tmp/pause.pid") assert.NotEqual(t, dir, "libpod/tmp/pause.pid")

View File

@ -44,6 +44,14 @@ function Make-Clean{
} }
} }
function Local-Unit {
Build-Ginkgo
$skippackages="hack,internal\domain\infra\abi,internal\domain\infra\tunnel,libpod\lock\shm,pkg\api\handlers\libpod,pkg\api\handlers\utils,pkg\bindings,"
$skippackages+="pkg\domain\infra\abi,pkg\emulation,pkg\machine\apple,pkg\machine\applehv,pkg\machine\e2e,pkg\machine\libkrun,"
$skippackages+="pkg\machine\provider,pkg\machine\proxyenv,pkg\machine\qemu,pkg\specgen\generate,pkg\systemd,test\e2e,test\utils"
Run-Command "./test/tools/build/ginkgo.exe -vv -r --tags `"$remotetags`" --timeout=15m --trace --no-color --skip-package `"$skippackages`""
}
function Local-Machine { function Local-Machine {
param ( param (
[string]$files [string]$files
@ -53,7 +61,7 @@ function Local-Machine {
$files = " --focus-file $files " $files = " --focus-file $files "
} }
Run-Command "./test/tools/build/ginkgo.exe -vv --tags `"$remotetags`" -timeout=90m --trace --no-color $files pkg/machine/e2e/." Run-Command "./test/tools/build/ginkgo.exe -vv --tags `"$remotetags`" --timeout=90m --trace --no-color $files pkg/machine/e2e/."
} }
# Expect starting directory to be /podman # Expect starting directory to be /podman
@ -219,9 +227,7 @@ function Build-Ginkgo{
return return
} }
Write-Host "Building Ginkgo" Write-Host "Building Ginkgo"
Push-Location ./test/tools Run-Command "go build -o ./test/tools/build/ginkgo.exe ./vendor/github.com/onsi/ginkgo/v2/ginkgo"
Run-Command "go build -o build/ginkgo.exe ./vendor/github.com/onsi/ginkgo/v2/ginkgo"
Pop-Location
} }
function Git-Commit{ function Git-Commit{
@ -287,6 +293,9 @@ switch ($target) {
{$_ -in '', 'podman-remote', 'podman'} { {$_ -in '', 'podman-remote', 'podman'} {
Podman-Remote Podman-Remote
} }
'localunit' {
Local-Unit
}
'localmachine' { 'localmachine' {
if ($args.Count -gt 1) { if ($args.Count -gt 1) {
$files = $args[1] $files = $args[1]
@ -331,6 +340,9 @@ switch ($target) {
Write-Host "Example: Build podman-remote " Write-Host "Example: Build podman-remote "
Write-Host " .\winmake podman-remote" Write-Host " .\winmake podman-remote"
Write-Host Write-Host
Write-Host "Example: Run all unit tests "
Write-Host " .\winmake localunit"
Write-Host
Write-Host "Example: Run all machine tests " Write-Host "Example: Run all machine tests "
Write-Host " .\winmake localmachine" Write-Host " .\winmake localmachine"
Write-Host Write-Host