mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-10 17:41:06 -04:00
* cmd/hi: add integration test runner CLI tool Add a new CLI tool 'hi' for running headscale integration tests with Docker automation. The tool replaces manual Docker command composition with an automated solution. Features: - Run integration tests in golang:1.24 containers - Docker context detection (supports colima and other contexts) - Test isolation with unique run IDs and isolated control_logs - Automatic Docker image pulling and container management - Comprehensive cleanup operations for containers, networks, images - Docker volume caching for Go modules - Verbose logging and detailed test artifact reporting - Support for PostgreSQL/SQLite selection and various test flags Usage: go run ./cmd/hi run TestPingAllByIP --verbose The tool uses creachadair/command and flax for CLI parsing and provides cleanup subcommands for Docker resource management. Updates flake.nix vendorHash for new Go dependencies. * ci: update integration tests to use hi CLI tool Replace manual Docker command composition in GitHub Actions workflow with the new hi CLI tool for running integration tests. Changes: - Replace complex docker run command with simple 'go run ./cmd/hi run' - Remove manual environment variable setup (handled by hi tool) - Update artifact paths for new timestamped log directory structure - Simplify command from 15+ lines to 3 lines - Maintain all existing functionality (postgres/sqlite, timeout, test patterns) The hi tool automatically handles Docker context detection, container management, volume mounting, and environment variable setup that was previously done manually in the workflow. * makefile: remove test integration Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
123 lines
3.1 KiB
Go
123 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/creachadair/command"
|
|
)
|
|
|
|
var ErrTestPatternRequired = errors.New("test pattern is required as first argument or use --test flag")
|
|
|
|
type RunConfig struct {
|
|
TestPattern string `flag:"test,Test pattern to run"`
|
|
Timeout time.Duration `flag:"timeout,default=120m,Test timeout"`
|
|
FailFast bool `flag:"failfast,default=true,Stop on first test failure"`
|
|
UsePostgres bool `flag:"postgres,default=false,Use PostgreSQL instead of SQLite"`
|
|
GoVersion string `flag:"go-version,Go version to use (auto-detected from go.mod)"`
|
|
CleanBefore bool `flag:"clean-before,default=true,Clean resources before test"`
|
|
CleanAfter bool `flag:"clean-after,default=true,Clean resources after test"`
|
|
KeepOnFailure bool `flag:"keep-on-failure,default=false,Keep containers on test failure"`
|
|
LogsDir string `flag:"logs-dir,default=control_logs,Control logs directory"`
|
|
Verbose bool `flag:"verbose,default=false,Verbose output"`
|
|
}
|
|
|
|
// runIntegrationTest executes the integration test workflow.
|
|
func runIntegrationTest(env *command.Env) error {
|
|
args := env.Args
|
|
if len(args) > 0 && runConfig.TestPattern == "" {
|
|
runConfig.TestPattern = args[0]
|
|
}
|
|
|
|
if runConfig.TestPattern == "" {
|
|
return ErrTestPatternRequired
|
|
}
|
|
|
|
if runConfig.GoVersion == "" {
|
|
runConfig.GoVersion = detectGoVersion()
|
|
}
|
|
|
|
// Run pre-flight checks
|
|
if runConfig.Verbose {
|
|
log.Printf("Running pre-flight system checks...")
|
|
}
|
|
if err := runDoctorCheck(env.Context()); err != nil {
|
|
return fmt.Errorf("pre-flight checks failed: %w", err)
|
|
}
|
|
|
|
if runConfig.Verbose {
|
|
log.Printf("Running test: %s", runConfig.TestPattern)
|
|
log.Printf("Go version: %s", runConfig.GoVersion)
|
|
log.Printf("Timeout: %s", runConfig.Timeout)
|
|
log.Printf("Use PostgreSQL: %t", runConfig.UsePostgres)
|
|
}
|
|
|
|
return runTestContainer(env.Context(), &runConfig)
|
|
}
|
|
|
|
// detectGoVersion reads the Go version from go.mod file.
|
|
func detectGoVersion() string {
|
|
goModPath := filepath.Join("..", "..", "go.mod")
|
|
|
|
if _, err := os.Stat("go.mod"); err == nil {
|
|
goModPath = "go.mod"
|
|
} else if _, err := os.Stat("../../go.mod"); err == nil {
|
|
goModPath = "../../go.mod"
|
|
}
|
|
|
|
content, err := os.ReadFile(goModPath)
|
|
if err != nil {
|
|
return "1.24"
|
|
}
|
|
|
|
lines := splitLines(string(content))
|
|
for _, line := range lines {
|
|
if len(line) > 3 && line[:3] == "go " {
|
|
version := line[3:]
|
|
if idx := indexOf(version, " "); idx != -1 {
|
|
version = version[:idx]
|
|
}
|
|
|
|
return version
|
|
}
|
|
}
|
|
|
|
return "1.24"
|
|
}
|
|
|
|
// splitLines splits a string into lines without using strings.Split.
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
var current string
|
|
|
|
for _, char := range s {
|
|
if char == '\n' {
|
|
lines = append(lines, current)
|
|
current = ""
|
|
} else {
|
|
current += string(char)
|
|
}
|
|
}
|
|
|
|
if current != "" {
|
|
lines = append(lines, current)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// indexOf finds the first occurrence of substr in s.
|
|
func indexOf(s, substr string) int {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return i
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|