mirror of
https://github.com/juanfont/headscale.git
synced 2025-12-02 06:07:41 -05:00
cmd/hi: reject if we are already running (#2919)
This commit is contained in:
@@ -26,8 +26,93 @@ var (
|
|||||||
ErrTestFailed = errors.New("test failed")
|
ErrTestFailed = errors.New("test failed")
|
||||||
ErrUnexpectedContainerWait = errors.New("unexpected end of container wait")
|
ErrUnexpectedContainerWait = errors.New("unexpected end of container wait")
|
||||||
ErrNoDockerContext = errors.New("no docker context found")
|
ErrNoDockerContext = errors.New("no docker context found")
|
||||||
|
ErrAnotherRunInProgress = errors.New("another integration test run is already in progress")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RunningTestInfo contains information about a currently running integration test.
|
||||||
|
type RunningTestInfo struct {
|
||||||
|
RunID string
|
||||||
|
ContainerID string
|
||||||
|
ContainerName string
|
||||||
|
StartTime time.Time
|
||||||
|
Duration time.Duration
|
||||||
|
TestPattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNoRunningTests indicates that no integration test is currently running.
|
||||||
|
var ErrNoRunningTests = errors.New("no running tests found")
|
||||||
|
|
||||||
|
// checkForRunningTests checks if there's already an integration test running.
|
||||||
|
// Returns ErrNoRunningTests if no test is running, or RunningTestInfo with details about the running test.
|
||||||
|
func checkForRunningTests(ctx context.Context) (*RunningTestInfo, error) {
|
||||||
|
cli, err := createDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
// List all running containers
|
||||||
|
containers, err := cli.ContainerList(ctx, container.ListOptions{
|
||||||
|
All: false, // Only running containers
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for containers with hi.test-type=test-runner label
|
||||||
|
for _, cont := range containers {
|
||||||
|
if cont.Labels != nil && cont.Labels["hi.test-type"] == "test-runner" {
|
||||||
|
// Found a running test runner container
|
||||||
|
runID := cont.Labels["hi.run-id"]
|
||||||
|
|
||||||
|
containerName := ""
|
||||||
|
for _, name := range cont.Names {
|
||||||
|
containerName = strings.TrimPrefix(name, "/")
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get more details via inspection
|
||||||
|
inspect, err := cli.ContainerInspect(ctx, cont.ID)
|
||||||
|
if err != nil {
|
||||||
|
// Return basic info if inspection fails
|
||||||
|
return &RunningTestInfo{
|
||||||
|
RunID: runID,
|
||||||
|
ContainerID: cont.ID,
|
||||||
|
ContainerName: containerName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime, _ := time.Parse(time.RFC3339Nano, inspect.State.StartedAt)
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
// Try to extract test pattern from command
|
||||||
|
testPattern := ""
|
||||||
|
|
||||||
|
if len(inspect.Config.Cmd) > 0 {
|
||||||
|
for i, arg := range inspect.Config.Cmd {
|
||||||
|
if arg == "-run" && i+1 < len(inspect.Config.Cmd) {
|
||||||
|
testPattern = inspect.Config.Cmd[i+1]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RunningTestInfo{
|
||||||
|
RunID: runID,
|
||||||
|
ContainerID: cont.ID,
|
||||||
|
ContainerName: containerName,
|
||||||
|
StartTime: startTime,
|
||||||
|
Duration: duration,
|
||||||
|
TestPattern: testPattern,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNoRunningTests
|
||||||
|
}
|
||||||
|
|
||||||
// runTestContainer executes integration tests in a Docker container.
|
// runTestContainer executes integration tests in a Docker container.
|
||||||
func runTestContainer(ctx context.Context, config *RunConfig) error {
|
func runTestContainer(ctx context.Context, config *RunConfig) error {
|
||||||
cli, err := createDockerClient()
|
cli, err := createDockerClient()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/creachadair/command"
|
"github.com/creachadair/command"
|
||||||
@@ -13,6 +14,58 @@ import (
|
|||||||
|
|
||||||
var ErrTestPatternRequired = errors.New("test pattern is required as first argument or use --test flag")
|
var ErrTestPatternRequired = errors.New("test pattern is required as first argument or use --test flag")
|
||||||
|
|
||||||
|
// formatRunningTestError creates a detailed error message about a running test.
|
||||||
|
func formatRunningTestError(info *RunningTestInfo) error {
|
||||||
|
var msg strings.Builder
|
||||||
|
msg.WriteString("\n")
|
||||||
|
msg.WriteString("╔══════════════════════════════════════════════════════════════════╗\n")
|
||||||
|
msg.WriteString("║ Another integration test run is already in progress! ║\n")
|
||||||
|
msg.WriteString("╚══════════════════════════════════════════════════════════════════╝\n")
|
||||||
|
msg.WriteString("\n")
|
||||||
|
msg.WriteString("Running test details:\n")
|
||||||
|
msg.WriteString(fmt.Sprintf(" Run ID: %s\n", info.RunID))
|
||||||
|
msg.WriteString(fmt.Sprintf(" Container: %s\n", info.ContainerName))
|
||||||
|
|
||||||
|
if info.TestPattern != "" {
|
||||||
|
msg.WriteString(fmt.Sprintf(" Test: %s\n", info.TestPattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.StartTime.IsZero() {
|
||||||
|
msg.WriteString(fmt.Sprintf(" Started: %s\n", info.StartTime.Format("2006-01-02 15:04:05")))
|
||||||
|
msg.WriteString(fmt.Sprintf(" Running for: %s\n", formatDuration(info.Duration)))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.WriteString("\n")
|
||||||
|
msg.WriteString("Please wait for the current test to complete, or stop it with:\n")
|
||||||
|
msg.WriteString(" go run ./cmd/hi clean containers\n")
|
||||||
|
msg.WriteString("\n")
|
||||||
|
msg.WriteString("To monitor the running test:\n")
|
||||||
|
msg.WriteString(fmt.Sprintf(" docker logs -f %s\n", info.ContainerName))
|
||||||
|
|
||||||
|
return fmt.Errorf("%w\n%s", ErrAnotherRunInProgress, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondsPerMinute = 60
|
||||||
|
|
||||||
|
// formatDuration formats a duration in a human-readable way.
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
if d < time.Minute {
|
||||||
|
return fmt.Sprintf("%d seconds", int(d.Seconds()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d < time.Hour {
|
||||||
|
minutes := int(d.Minutes())
|
||||||
|
seconds := int(d.Seconds()) % secondsPerMinute
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d minutes, %d seconds", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
hours := int(d.Hours())
|
||||||
|
minutes := int(d.Minutes()) % secondsPerMinute
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
type RunConfig struct {
|
type RunConfig struct {
|
||||||
TestPattern string `flag:"test,Test pattern to run"`
|
TestPattern string `flag:"test,Test pattern to run"`
|
||||||
Timeout time.Duration `flag:"timeout,default=120m,Test timeout"`
|
Timeout time.Duration `flag:"timeout,default=120m,Test timeout"`
|
||||||
@@ -27,6 +80,7 @@ type RunConfig struct {
|
|||||||
Stats bool `flag:"stats,default=false,Collect and display container resource usage statistics"`
|
Stats bool `flag:"stats,default=false,Collect and display container resource usage statistics"`
|
||||||
HSMemoryLimit float64 `flag:"hs-memory-limit,default=0,Fail test if any Headscale container exceeds this memory limit in MB (0 = disabled)"`
|
HSMemoryLimit float64 `flag:"hs-memory-limit,default=0,Fail test if any Headscale container exceeds this memory limit in MB (0 = disabled)"`
|
||||||
TSMemoryLimit float64 `flag:"ts-memory-limit,default=0,Fail test if any Tailscale container exceeds this memory limit in MB (0 = disabled)"`
|
TSMemoryLimit float64 `flag:"ts-memory-limit,default=0,Fail test if any Tailscale container exceeds this memory limit in MB (0 = disabled)"`
|
||||||
|
Force bool `flag:"force,default=false,Kill any running test and start a new one"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// runIntegrationTest executes the integration test workflow.
|
// runIntegrationTest executes the integration test workflow.
|
||||||
@@ -44,6 +98,23 @@ func runIntegrationTest(env *command.Env) error {
|
|||||||
runConfig.GoVersion = detectGoVersion()
|
runConfig.GoVersion = detectGoVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if another test run is already in progress
|
||||||
|
runningTest, err := checkForRunningTests(env.Context())
|
||||||
|
if err != nil && !errors.Is(err, ErrNoRunningTests) {
|
||||||
|
log.Printf("Warning: failed to check for running tests: %v", err)
|
||||||
|
} else if runningTest != nil {
|
||||||
|
if runConfig.Force {
|
||||||
|
log.Printf("Force flag set, killing existing test run: %s", runningTest.RunID)
|
||||||
|
|
||||||
|
err = killTestContainers(env.Context())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to kill existing test containers: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return formatRunningTestError(runningTest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run pre-flight checks
|
// Run pre-flight checks
|
||||||
if runConfig.Verbose {
|
if runConfig.Verbose {
|
||||||
log.Printf("Running pre-flight system checks...")
|
log.Printf("Running pre-flight system checks...")
|
||||||
|
|||||||
Reference in New Issue
Block a user