Skip to content

Commit 968a198

Browse files
authored
Integration tests for windows service support (#3733)
* Create integration tests for windows service support Signed-off-by: Guilherme Carvalho <[email protected]>
1 parent 0f20b0a commit 968a198

16 files changed

+275
-3
lines changed

pkg/common/entrypoint/entrypoint_windows.go

+46-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ package entrypoint
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"os"
11+
"strings"
12+
"unsafe"
13+
14+
"golang.org/x/sys/windows"
1015

1116
"golang.org/x/sys/windows/svc"
1217
)
@@ -20,7 +25,10 @@ type systemCall struct {
2025
}
2126

2227
func (s *systemCall) IsWindowsService() (bool, error) {
23-
return svc.IsWindowsService()
28+
// We are using a custom function because the svc.IsWindowsService() one still has an open issue in which it states
29+
// that it is not working properly in Windows containers: https://github.com/golang/go/issues/56335. Soon as we have
30+
// a fix for that, we can use the original function.
31+
return isWindowsService()
2432
}
2533

2634
func (s *systemCall) Run(name string, handler svc.Handler) error {
@@ -51,11 +59,11 @@ func (e *EntryPoint) Main() int {
5159
// Determining if SPIRE is running as a Windows service is done
5260
// with a best-effort approach. If there is an error, just fallback
5361
// to the behavior of not running as a Windows service.
54-
isWindowsService, err := e.sc.IsWindowsService()
62+
isWindowsSvc, err := e.sc.IsWindowsService()
5563
if err != nil {
5664
fmt.Fprintf(os.Stderr, "Could not determine if running as a Windows service: %v", err)
5765
}
58-
if isWindowsService {
66+
if isWindowsSvc {
5967
errChan := make(chan error)
6068
go func() {
6169
// Since the service runs in its own process, the service name is ignored.
@@ -71,3 +79,38 @@ func (e *EntryPoint) Main() int {
7179

7280
return e.runCmdFn(context.Background(), os.Args[1:])
7381
}
82+
83+
// isWindowsService is a copy of the svc.IsWindowsService() function, but without the parentProcess.SessionID == 0 check
84+
// that is causing the issue in Windows containers, this logic is exactly the same from .NET runtime (>= 6.0.10).
85+
func isWindowsService() (bool, error) {
86+
// The below technique looks a bit hairy, but it's actually
87+
// exactly what the .NET runtime (>= 6.0.10) does for the similarly named function:
88+
// https://github.com/dotnet/runtime/blob/36bf84fc4a89209f4fdbc1fc201e81afd8be49b0/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceHelpers.cs#L20-L33
89+
// Specifically, it looks up whether the parent process is called "services".
90+
91+
var currentProcess windows.PROCESS_BASIC_INFORMATION
92+
infoSize := uint32(unsafe.Sizeof(currentProcess))
93+
err := windows.NtQueryInformationProcess(windows.CurrentProcess(), windows.ProcessBasicInformation, unsafe.Pointer(&currentProcess), infoSize, &infoSize)
94+
if err != nil {
95+
return false, err
96+
}
97+
var parentProcess *windows.SYSTEM_PROCESS_INFORMATION
98+
for infoSize = uint32((unsafe.Sizeof(*parentProcess) + unsafe.Sizeof(uintptr(0))) * 1024); ; {
99+
parentProcess = (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&make([]byte, infoSize)[0]))
100+
err = windows.NtQuerySystemInformation(windows.SystemProcessInformation, unsafe.Pointer(parentProcess), infoSize, &infoSize)
101+
if err == nil {
102+
break
103+
} else if !errors.Is(err, windows.STATUS_INFO_LENGTH_MISMATCH) {
104+
return false, err
105+
}
106+
}
107+
for ; ; parentProcess = (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(uintptr(unsafe.Pointer(parentProcess)) + uintptr(parentProcess.NextEntryOffset))) {
108+
if parentProcess.UniqueProcessID == currentProcess.InheritedFromUniqueProcessId {
109+
return strings.EqualFold("services.exe", parentProcess.ImageName.String()), nil
110+
}
111+
if parentProcess.NextEntryOffset == 0 {
112+
break
113+
}
114+
}
115+
return false, nil
116+
}

test/integration/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,4 @@ The following environment variables are available to the teardown script:
8989
* [Self Test](suites/self-test/README.md)
9090
* [SPIRE Server CLI](suites/spire-server-cli/README.md)
9191
* [Upgrade](suites/upgrade/README.md)
92+
* [Windows Service](suites-windows/windows-service/README.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
"${ROOTDIR}/setup/x509pop/setup.sh" conf/server conf/agent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
source ./common
3+
4+
docker-up spire-server
5+
6+
create-service spire-server C:/spire/bin/spire-server.exe
7+
start-service spire-server run -config C:/spire/conf/server/server.conf
8+
assert-service-status spire-server RUNNING
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
3+
log-debug "bootstrapping agent..."
4+
docker-compose exec -T spire-server \
5+
c:/spire/bin/spire-server bundle show > conf/agent/bootstrap.crt || fail-now "failed to bootstrap agent"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
source ./common
3+
4+
docker-up spire-agent
5+
6+
create-service spire-agent C:/spire/bin/spire-agent.exe
7+
start-service spire-agent run -config C:/spire/conf/agent/agent.conf
8+
assert-service-status spire-agent RUNNING
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
source ./common
3+
4+
log-debug "creating regular registration entry..."
5+
docker-compose exec -T spire-server \
6+
c:/spire/bin/spire-server entry create \
7+
-parentID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" \
8+
-spiffeID "spiffe://domain.test/workload" \
9+
-selector "windows:user_name:User Manager\ContainerUser" \
10+
-ttl 0
11+
12+
assert-synced-entry "spiffe://domain.test/workload"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
log-debug "test fetch x509 SVID..."
4+
docker-compose exec -T -u ContainerUser spire-agent \
5+
c:/spire/bin/spire-agent api fetch x509 || fail-now "failed to fetch x509"
6+
7+
log-debug "test fetch JWT SVID..."
8+
docker-compose exec -T -u ContainerUser spire-agent \
9+
c:/spire/bin/spire-agent api fetch jwt -audience mydb || fail-now "failed to fetch JWT"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
source ./common
3+
4+
stop-service spire-agent
5+
assert-service-status spire-agent STOPPED
6+
assert-graceful-shutdown agent
7+
8+
stop-service spire-server
9+
assert-service-status spire-server STOPPED
10+
assert-graceful-shutdown server
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
source ./common
3+
4+
start-service spire-server run -config invalid-config-path
5+
assert-service-status spire-server STOPPED
6+
7+
start-service spire-agent run -config invalid-config-path
8+
assert-service-status spire-agent STOPPED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# SPIRE Server CLI Suite
2+
3+
## Description
4+
5+
This suite validates that we can run both spire agent and spire server natively on Windows OS, asserting that spire components
6+
can run as a [windows service application](https://learn.microsoft.com/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications#service-applications-vs-other-visual-studio-applications),
7+
and perform [service state transitions](https://learn.microsoft.com/en-us/windows/win32/services/service-status-transitions).
8+
9+
The suite steps are structured as follows:
10+
11+
1. Spire server and agent are installed as Windows services.
12+
2. Spire server and agent services starts, their respective status is asserted as **_RUNNING_**, and the node attestation
13+
is performed with x509pop.
14+
3. Workload registration entries are created.
15+
4. The feature of fetching SVIDs (x509 and JWT) is asserted with the running spire agent service.
16+
5. Spire server and agent services are stopped, their respective status is asserted as **_STOPPED_**, and graceful
17+
shutdown is verified via application logs.
18+
6. Spire server and agent services are started again, but this time with an invalid config; their respective status is
19+
asserted as **_STOPPED_**.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/bin/bash
2+
3+
assert-synced-entry() {
4+
# Check at most 30 times (with one second in between) that the agent has
5+
# successfully synced down the workload entry.
6+
MAXCHECKS=30
7+
CHECKINTERVAL=1
8+
for ((i=1;i<=MAXCHECKS;i++)); do
9+
log-info "checking for synced entry ($i of $MAXCHECKS max)..."
10+
if grep -wq "$1" conf/agent/logs.txt; then
11+
return 0
12+
fi
13+
sleep "${CHECKINTERVAL}"
14+
done
15+
16+
fail-now "timed out waiting for agent to sync down entry"
17+
}
18+
19+
assert-service-status() {
20+
MAXCHECKS=10
21+
CHECKINTERVAL=1
22+
for ((i=1;i<=MAXCHECKS;i++)); do
23+
log-info "checking for $1 service $2 ($i of $MAXCHECKS max)..."
24+
scCommand=$([ "$2" == "STOPPED" ] && echo "query" || echo "interrogate")
25+
if docker-compose exec -T -u ContainerAdministrator "$1" sc "$scCommand" "$1" | grep -wq "$2"; then
26+
log-info "$1 is in $2 state"
27+
return 0
28+
fi
29+
sleep "${CHECKINTERVAL}"
30+
done
31+
32+
fail-now "$1 service failed to reach $2 state"
33+
}
34+
35+
assert-graceful-shutdown() {
36+
MAXCHECKS=10
37+
CHECKINTERVAL=1
38+
for ((i=1;i<=MAXCHECKS;i++)); do
39+
log-info "checking for graceful shutdown ($i of $MAXCHECKS max)..."
40+
if grep -wq "stopped gracefully" conf/"$1"/logs.txt; then
41+
log-info "$1 stopped gracefully"
42+
return 0
43+
fi
44+
sleep "${CHECKINTERVAL}"
45+
done
46+
47+
fail-now "timed out waiting for $1 graceful shutdown"
48+
}
49+
50+
create-service() {
51+
log-info "creating $1 service..."
52+
docker-compose exec -T -u ContainerAdministrator "$1" \
53+
sc create "$1" binPath="$2" || grep "STOPPED" fail-now "failed to create $1 service"
54+
}
55+
56+
stop-service() {
57+
log-info "stopping $1 service..."
58+
docker-compose exec -T -u ContainerAdministrator "$1" \
59+
sc stop "$1" || fail-now "failed to stop $1 service"
60+
}
61+
62+
start-service(){
63+
log-info "starting $1 service..."
64+
docker-compose exec -T -u ContainerAdministrator "$1" \
65+
sc start "$@" | grep -wq "START_PENDING" || fail-now "failed to start $2 service"
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
agent {
2+
data_dir = "c:/spire/data/agent"
3+
log_level = "DEBUG"
4+
server_address = "spire-server"
5+
log_file ="c:/spire/conf/agent/logs.txt"
6+
server_port = "8081"
7+
trust_bundle_path = "c:/spire/conf/agent/bootstrap.crt"
8+
trust_domain = "domain.test"
9+
}
10+
11+
plugins {
12+
NodeAttestor "x509pop" {
13+
plugin_data {
14+
private_key_path = "c:/spire/conf/agent/agent.key.pem"
15+
certificate_path = "c:/spire/conf/agent/agent.crt.pem"
16+
}
17+
}
18+
KeyManager "disk" {
19+
plugin_data {
20+
directory = "c:/spire/data/agent"
21+
}
22+
}
23+
WorkloadAttestor "windows" {
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
server {
2+
bind_address = "0.0.0.0"
3+
bind_port = "8081"
4+
trust_domain = "domain.test"
5+
log_file ="c:/spire/conf/server/logs.txt"
6+
data_dir = "c:/spire/data/server"
7+
log_level = "DEBUG"
8+
}
9+
10+
plugins {
11+
DataStore "sql" {
12+
plugin_data {
13+
database_type = "sqlite3"
14+
connection_string = "c:/spire/data/server/datastore.sqlite3"
15+
}
16+
}
17+
NodeAttestor "x509pop" {
18+
plugin_data {
19+
ca_bundle_path = "c:/spire/conf/server/agent-cacert.pem"
20+
}
21+
}
22+
KeyManager "memory" {
23+
plugin_data = {}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: '3'
2+
3+
services:
4+
spire-server:
5+
image: spire-server-windows:latest-local
6+
hostname: spire-server
7+
volumes:
8+
- ./conf/server:c:/spire/conf/server
9+
user: ContainerAdministrator
10+
entrypoint:
11+
- cmd
12+
command:
13+
- cmd /c ping -t localhost > NUL
14+
spire-agent:
15+
image: spire-agent-windows:latest-local
16+
hostname: spire-agent
17+
depends_on: ["spire-server"]
18+
volumes:
19+
- ./conf/agent:c:/spire/conf/agent
20+
user: ContainerAdministrator
21+
entrypoint:
22+
- cmd
23+
command:
24+
- cmd /c ping -t localhost > NUL
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
if [ -z "$SUCCESS" ]; then
4+
docker-compose logs
5+
fi
6+
docker-down

0 commit comments

Comments
 (0)