Merge pull request #517 from jeremyje/fixwin

Windows Support: Fix Build Regressions, Tests Pass
This commit is contained in:
Kubernetes Prow Robot
2021-03-15 21:10:34 -07:00
committed by GitHub
20 changed files with 322 additions and 55 deletions

View File

@@ -31,3 +31,4 @@ script:
- BUILD_TAGS="disable_stackdriver_exporter" make test
- make clean && ENABLE_JOURNALD=0 make
- ENABLE_JOURNALD=0 make test
- ENABLE_JOURNALD=0 make build-binaries

View File

@@ -38,7 +38,12 @@ UPLOAD_PATH:=$(shell echo $(UPLOAD_PATH) | sed '$$s/\/*$$//')
PKG:=k8s.io/node-problem-detector
# PKG_SOURCES are all the go source code.
ifeq ($(OS),Windows_NT)
PKG_SOURCES:=
# TODO: File change detection does not work in Windows.
else
PKG_SOURCES:=$(shell find pkg cmd -name '*.go')
endif
# PARALLEL specifies the number of parallel test nodes to run for e2e tests.
PARALLEL?=3
@@ -74,6 +79,12 @@ BUILD_TAGS?=
LINUX_BUILD_TAGS = $(BUILD_TAGS)
WINDOWS_BUILD_TAGS = $(BUILD_TAGS)
ifeq ($(OS),Windows_NT)
HOST_PLATFORM_BUILD_TAGS = $(WINDOWS_BUILD_TAGS)
else
HOST_PLATFORM_BUILD_TAGS = $(LINUX_BUILD_TAGS)
endif
ifeq ($(ENABLE_JOURNALD), 1)
# Enable journald build tag.
LINUX_BUILD_TAGS := $(BUILD_TAGS) journald
@@ -91,9 +102,9 @@ else
endif
vet:
GO111MODULE=on go list -mod vendor -tags "$(LINUX_BUILD_TAGS)" ./... | \
GO111MODULE=on go list -mod vendor -tags "$(HOST_PLATFORM_BUILD_TAGS)" ./... | \
grep -v "./vendor/*" | \
GO111MODULE=on xargs go vet -mod vendor -tags "$(LINUX_BUILD_TAGS)"
GO111MODULE=on xargs go vet -mod vendor -tags "$(HOST_PLATFORM_BUILD_TAGS)"
fmt:
find . -type f -name "*.go" | grep -v "./vendor/*" | xargs gofmt -s -w -l
@@ -109,7 +120,10 @@ ifeq ($(ENABLE_JOURNALD), 1)
LINUX_AMD64_BINARIES += bin/linux_amd64/log-counter
endif
windows-binaries: $(WINDOWS_AMD64_BINARIES) $(WINDOWS_AMD64_TEST_BINARIES)
WINDOWS_BINARIES = $(WINDOWS_AMD64_BINARIES) $(WINDOWS_AMD64_TEST_BINARIES)
LINUX_BINARIES = $(LINUX_AMD64_BINARIES) $(LINUX_AMD64_TEST_BINARIES)
windows-binaries: $(WINDOWS_BINARIES)
bin/windows_amd64/%.exe: $(PKG_SOURCES)
ifeq ($(ENABLE_JOURNALD), 1)
@@ -191,10 +205,10 @@ endif
cmd/healthchecker/health_checker.go
test: vet fmt
GO111MODULE=on go test -mod vendor -timeout=1m -v -race -short -tags "$(LINUX_BUILD_TAGS)" ./...
GO111MODULE=on go test -mod vendor -timeout=1m -v -race -short -tags "$(HOST_PLATFORM_BUILD_TAGS)" ./...
e2e-test: vet fmt build-tar
GO111MODULE=on ginkgo -nodes=$(PARALLEL) -mod vendor -timeout=10m -v -tags "$(LINUX_BUILD_TAGS)" -stream \
GO111MODULE=on ginkgo -nodes=$(PARALLEL) -mod vendor -timeout=10m -v -tags "$(HOST_PLATFORM_BUILD_TAGS)" -stream \
./test/e2e/metriconly/... -- \
-project=$(PROJECT) -zone=$(ZONE) \
-image=$(VM_IMAGE) -image-family=$(IMAGE_FAMILY) -image-project=$(IMAGE_PROJECT) \
@@ -203,7 +217,7 @@ e2e-test: vet fmt build-tar
-boskos-project-type=$(BOSKOS_PROJECT_TYPE) -job-name=$(JOB_NAME) \
-artifacts-dir=$(ARTIFACTS)
build-binaries: ./bin/node-problem-detector ./bin/log-counter ./bin/health-checker
build-binaries: ./bin/node-problem-detector ./bin/log-counter ./bin/health-checker $(WINDOWS_BINARIES) $(LINUX_BINARIES)
build-container: build-binaries Dockerfile
docker build -t $(IMAGE) --build-arg BASEIMAGE=$(BASEIMAGE) --build-arg LOGCOUNTER=$(LOGCOUNTER) .

View File

@@ -8,13 +8,18 @@
"logPath": "C:\\Program Files\\containerd\\containerd.log",
"lookback": "5m",
"bufferSize": 10,
"source": "docker-monitor",
"source": "containerd",
"conditions": [],
"rules": [
{
"type": "temporary",
"reason": "BadCNIConfig",
"pattern": "failed to reload cni configuration.*"
"reason": "MissingPigz",
"pattern": "unpigz not found.*"
},
{
"type": "temporary",
"reason": "IncompatibleContainer",
"pattern": ".*CreateComputeSystem.*"
}
]
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/golang/glog"
cpmtypes "k8s.io/node-problem-detector/pkg/custompluginmonitor/types"
"k8s.io/node-problem-detector/pkg/util"
"k8s.io/node-problem-detector/pkg/util/tomb"
)
@@ -147,12 +148,7 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
}
defer cancel()
// create a process group
sysProcAttr := &syscall.SysProcAttr{
Setpgid: true,
}
cmd := exec.Command(rule.Path, rule.Args...)
cmd.SysProcAttr = sysProcAttr
cmd := util.Exec(rule.Path, rule.Args...)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
@@ -172,6 +168,9 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
waitChan := make(chan struct{})
defer close(waitChan)
var m sync.Mutex
timeout := false
go func() {
select {
case <-ctx.Done():
@@ -183,7 +182,12 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
glog.Errorf("Error in cmd.Process check %q", rule.Path)
break
}
err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
m.Lock()
timeout = true
m.Unlock()
err := util.Kill(cmd)
if err != nil {
glog.Errorf("Error in kill process %d, %v", cmd.Process.Pid, err)
}
@@ -202,12 +206,12 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
wg.Add(2)
go func() {
defer wg.Done()
stdout, stdoutErr = readFromReader(stdoutPipe, maxCustomPluginBufferBytes)
wg.Done()
}()
go func() {
defer wg.Done()
stderr, stderrErr = readFromReader(stderrPipe, maxCustomPluginBufferBytes)
wg.Done()
}()
// This will wait for the reads to complete. If the execution times out, the pipes
// will be closed and the wait group unblocks.
@@ -234,7 +238,11 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
output = string(stdout)
output = strings.TrimSpace(output)
if cmd.ProcessState.Sys().(syscall.WaitStatus).Signaled() {
m.Lock()
cmdKilled := timeout
m.Unlock()
if cmdKilled {
output = fmt.Sprintf("Timeout when running plugin %q: state - %s. output - %q", rule.Path, cmd.ProcessState.String(), output)
}
@@ -257,6 +265,7 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
}
}
// Stop the plugin.
func (p *Plugin) Stop() {
p.tomb.Stop()
glog.Info("Stop plugin execution")

View File

@@ -17,6 +17,7 @@ limitations under the License.
package plugin
import (
"runtime"
"testing"
"time"
@@ -25,6 +26,13 @@ import (
func TestNewPluginRun(t *testing.T) {
ruleTimeout := 1 * time.Second
timeoutExitStatus := cpmtypes.Unknown
ext := "sh"
if runtime.GOOS == "windows" {
ext = "cmd"
timeoutExitStatus = cpmtypes.NonOK
}
utMetas := map[string]struct {
Rule cpmtypes.CustomRule
@@ -33,7 +41,7 @@ func TestNewPluginRun(t *testing.T) {
}{
"ok": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/ok.sh",
Path: "./test-data/ok." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.OK,
@@ -41,7 +49,7 @@ func TestNewPluginRun(t *testing.T) {
},
"non-ok": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/non-ok.sh",
Path: "./test-data/non-ok." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.NonOK,
@@ -49,7 +57,7 @@ func TestNewPluginRun(t *testing.T) {
},
"unknown": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/unknown.sh",
Path: "./test-data/unknown." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.Unknown,
@@ -57,6 +65,7 @@ func TestNewPluginRun(t *testing.T) {
},
"non executable": {
Rule: cpmtypes.CustomRule{
// Intentionally run .sh for Windows, this is meant to be not executable.
Path: "./test-data/non-executable.sh",
Timeout: &ruleTimeout,
},
@@ -65,7 +74,7 @@ func TestNewPluginRun(t *testing.T) {
},
"longer than 80 stdout with ok exit status": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/longer-than-80-stdout-with-ok-exit-status.sh",
Path: "./test-data/longer-than-80-stdout-with-ok-exit-status." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.OK,
@@ -73,7 +82,7 @@ func TestNewPluginRun(t *testing.T) {
},
"non defined exit status": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/non-defined-exit-status.sh",
Path: "./test-data/non-defined-exit-status." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.Unknown,
@@ -81,29 +90,32 @@ func TestNewPluginRun(t *testing.T) {
},
"sleep 3 second with ok exit status": {
Rule: cpmtypes.CustomRule{
Path: "./test-data/sleep-3-second-with-ok-exit-status.sh",
Path: "./test-data/sleep-3-second-with-ok-exit-status." + ext,
Timeout: &ruleTimeout,
},
ExitStatus: cpmtypes.Unknown,
Output: `Timeout when running plugin "./test-data/sleep-3-second-with-ok-exit-status.sh": state - signal: killed. output - ""`,
ExitStatus: timeoutExitStatus,
Output: `Timeout when running plugin "./test-data/sleep-3-second-with-ok-exit-status.` + ext + `": state - signal: killed. output - ""`,
},
}
conf := cpmtypes.CustomPluginConfig{}
(&conf).ApplyConfiguration()
p := Plugin{config: conf}
for desp, utMeta := range utMetas {
gotExitStatus, gotOutput := p.run(utMeta.Rule)
// cut at position max_output_length if expected output is longer than max_output_length bytes
if len(utMeta.Output) > *p.config.PluginGlobalConfig.MaxOutputLength {
utMeta.Output = utMeta.Output[:*p.config.PluginGlobalConfig.MaxOutputLength]
}
if gotExitStatus != utMeta.ExitStatus || gotOutput != utMeta.Output {
t.Errorf("%s", desp)
t.Errorf("Error in run plugin and get exit status and output for %q. "+
"Got exit status: %v, Expected exit status: %v. "+
"Got output: %q, Expected output: %q",
utMeta.Rule.Path, gotExitStatus, utMeta.ExitStatus, gotOutput, utMeta.Output)
}
for k, v := range utMetas {
desp := k
utMeta := v
t.Run(desp, func(t *testing.T) {
conf := cpmtypes.CustomPluginConfig{}
(&conf).ApplyConfiguration()
p := Plugin{config: conf}
gotExitStatus, gotOutput := p.run(utMeta.Rule)
// cut at position max_output_length if expected output is longer than max_output_length bytes
if len(utMeta.Output) > *p.config.PluginGlobalConfig.MaxOutputLength {
utMeta.Output = utMeta.Output[:*p.config.PluginGlobalConfig.MaxOutputLength]
}
if gotExitStatus != utMeta.ExitStatus || gotOutput != utMeta.Output {
t.Errorf("Error in run plugin and get exit status and output for %q. "+
"Got exit status: %v, Expected exit status: %v. "+
"Got output: %q, Expected output: %q",
utMeta.Rule.Path, gotExitStatus, utMeta.ExitStatus, gotOutput, utMeta.Output)
}
})
}
}

View File

@@ -0,0 +1,4 @@
@echo off
echo 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
exit 0

View File

@@ -0,0 +1,4 @@
@echo off
echo NON-DEFINED-EXIT-STATUS
exit 100

View File

@@ -0,0 +1,4 @@
@echo off
echo NonOK
exit 1

View File

@@ -0,0 +1,4 @@
@echo off
echo OK
exit 0

View File

@@ -0,0 +1,5 @@
@echo off
ping 127.0.0.1 -n 3 > nul
echo SLEEP 3S SECOND
exit 0

View File

@@ -0,0 +1,4 @@
@echo off
echo UNKNOWN
exit 3

View File

@@ -27,7 +27,7 @@ import (
"k8s.io/node-problem-detector/pkg/types"
problemutil "k8s.io/node-problem-detector/pkg/util"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/clock"
)
@@ -53,33 +53,43 @@ func newTestCondition(condition string) types.Condition {
func TestNeedUpdates(t *testing.T) {
m, _, _ := newTestManager()
var c types.Condition
for desc, test := range map[string]struct {
for _, testCase := range []struct {
name string
condition string
update bool
}{
"Init condition needs update": {
{
name: "Init condition needs update",
condition: "TestCondition",
update: true,
},
"Same condition doesn't need update": {
{
name: "Same condition doesn't need update",
// not set condition, the test will reuse the condition in last case.
update: false,
},
"Same condition with different timestamp need update": {
{
name: "Same condition with different timestamp need update",
condition: "TestCondition",
update: true,
},
"New condition needs update": {
{
name: "New condition needs update",
condition: "TestConditionNew",
update: true,
},
} {
if test.condition != "" {
c = newTestCondition(test.condition)
tc := testCase
t.Log(tc.name)
if tc.condition != "" {
// Guarantee that the time advances before creating a new condition.
for now := time.Now(); now == time.Now(); {
}
c = newTestCondition(tc.condition)
}
m.UpdateCondition(c)
assert.Equal(t, test.update, m.needUpdates(), desc)
assert.Equal(t, c, m.conditions[c.Type], desc)
assert.Equal(t, tc.update, m.needUpdates(), tc.name)
assert.Equal(t, c, m.conditions[c.Type], tc.name)
}
}

42
pkg/util/exec_linux.go Normal file
View File

@@ -0,0 +1,42 @@
/*
Copyright 2021 The Kubernetes Authors All rights reserved.
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.
*/
package util
import (
"fmt"
"os/exec"
"syscall"
)
// Exec creates a new process with the specified arguments.
func Exec(name string, arg ...string) *exec.Cmd {
// create a process group
sysProcAttr := &syscall.SysProcAttr{
Setpgid: true,
}
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = sysProcAttr
return cmd
}
// Kill the process and subprocesses.
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
return fmt.Errorf("%v does not have a process handle", cmd)
}
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}

62
pkg/util/exec_test.go Normal file
View File

@@ -0,0 +1,62 @@
/*
Copyright 2021 The Kubernetes Authors All rights reserved.
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.
*/
package util
import (
"fmt"
"runtime"
"testing"
)
func TestExec(t *testing.T) {
var cmds [][]string
if runtime.GOOS == "windows" {
cmds = [][]string{
{"powershell.exe"},
{"cmd.exe", "/C", "echo", "Hello"},
{"cmd.exe", "/K", "echo", "Wait", "forever"},
{"testdata/hello-world.cmd"},
{"testdata/hello-world.bat"},
{"testdata/hello-world.ps1"},
}
} else {
cmds = [][]string{
{"/bin/sh"},
{"/bin/bash"},
}
}
for _, v := range cmds {
args := v
t.Run(fmt.Sprintf("%v", args), func(t *testing.T) {
cmd := Exec(args[0], args[1:]...)
if err := Kill(cmd); err == nil {
t.Error("Kill(cmd) expected to have error because of empty handle, got none")
}
if err := cmd.Start(); err != nil {
t.Errorf("Start() got error, %v", err)
}
if err := Kill(cmd); err != nil {
t.Errorf("Kill(cmd) for %s %v got error, %v", cmd.Path, cmd.Args, err)
}
})
}
}

81
pkg/util/exec_windows.go Normal file
View File

@@ -0,0 +1,81 @@
/*
Copyright 2021 The Kubernetes Authors All rights reserved.
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.
*/
package util
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
)
// Exec creates a new process with the specified arguments.
func Exec(name string, arg ...string) *exec.Cmd {
// Windows does not handle relative path names in exec very well.
name = filepath.Clean(name)
cmdArgs := arg
// Detect scripts via file extension and automatically invoke them within a shell.
// This mirrors the Linux behavior if the execute bit on file where a shell context
// is automatically created.
switch strings.ToLower(filepath.Ext(name)) {
// Batch Scripts
case ".cmd", ".bat":
cmdArgs = append([]string{"/C", name}, cmdArgs...)
name = "cmd.exe"
// Powershell Scripts
case ".ps1":
cmdArgs = append([]string{"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "RemoteSigned", name}, cmdArgs...)
name = "powershell.exe"
default:
// Run directly.
}
return exec.Command(name, cmdArgs...)
}
// ExitStatus returns the exit code of the application.
func ExitStatus(cmd *exec.Cmd) int {
return cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
}
// Kill the process and subprocesses.
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
return fmt.Errorf("%v does not have a process handle", cmd)
}
// Use taskkill to kill the child process by process id.
// /F = Force
// /T = Kill child processes.
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill
kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
kill.Stderr = os.Stderr
kill.Stdout = os.Stdout
err := kill.Run()
if execErr, ok := err.(*exec.ExitError); ok {
// Error code 128 (ERROR_WAIT_NO_CHILDREN) means that taskkill couldn't find the process, it probably died already.
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
if execErr.ExitCode() == 128 {
return nil
}
}
return err
}

View File

@@ -71,6 +71,7 @@ func ParsePrometheusMetrics(metricsText string) ([]Float64MetricRepresentation,
var metrics []Float64MetricRepresentation
var textParser expfmt.TextParser
metricsText = strings.ReplaceAll(metricsText, "\r", "")
metricFamilies, err := textParser.TextToMetricFamilies(strings.NewReader(metricsText))
if err != nil {
return metrics, err

View File

@@ -63,7 +63,7 @@ func TestPrometheusMetricsParsingAndMatching(t *testing.T) {
Name: "host_uptime",
Labels: map[string]string{"kernel_version": "mismatched-version"},
},
// Non-exsistant metric.
// Non-existant metric.
{
Name: "host_downtime",
Labels: map[string]string{},
@@ -109,7 +109,7 @@ func TestPrometheusMetricsParsingAndMatching(t *testing.T) {
Name: "host_uptime",
Labels: map[string]string{"kernel_version": "mismatched-version"},
},
// Non-exsistant metric.
// Non-existant metric.
{
Name: "host_downtime",
Labels: map[string]string{},

View File

@@ -0,0 +1,2 @@
@echo off
echo Hello World

View File

@@ -0,0 +1,2 @@
@echo off
echo Hello World

View File

@@ -0,0 +1 @@
Write-Host "Hello World"