mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-14 11:21:54 -04:00
352 lines
8.4 KiB
Go
352 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
var ErrSystemChecksFailed = errors.New("system checks failed")
|
|
|
|
// DoctorResult represents the result of a single health check.
|
|
type DoctorResult struct {
|
|
Name string
|
|
Status string // "PASS", "FAIL", "WARN"
|
|
Message string
|
|
Suggestions []string
|
|
}
|
|
|
|
// runDoctorCheck performs comprehensive pre-flight checks for integration testing.
|
|
func runDoctorCheck(ctx context.Context) error {
|
|
results := []DoctorResult{}
|
|
|
|
// Check 1: Docker binary availability
|
|
results = append(results, checkDockerBinary())
|
|
|
|
// Check 2: Docker daemon connectivity
|
|
dockerResult := checkDockerDaemon(ctx)
|
|
results = append(results, dockerResult)
|
|
|
|
// If Docker is available, run additional checks
|
|
if dockerResult.Status == "PASS" {
|
|
results = append(results, checkDockerContext(ctx))
|
|
results = append(results, checkDockerSocket(ctx))
|
|
results = append(results, checkGolangImage(ctx))
|
|
}
|
|
|
|
// Check 3: Go installation
|
|
results = append(results, checkGoInstallation())
|
|
|
|
// Check 4: Git repository
|
|
results = append(results, checkGitRepository())
|
|
|
|
// Check 5: Required files
|
|
results = append(results, checkRequiredFiles())
|
|
|
|
// Display results
|
|
displayDoctorResults(results)
|
|
|
|
// Return error if any critical checks failed
|
|
for _, result := range results {
|
|
if result.Status == "FAIL" {
|
|
return fmt.Errorf("%w - see details above", ErrSystemChecksFailed)
|
|
}
|
|
}
|
|
|
|
log.Printf("✅ All system checks passed - ready to run integration tests!")
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDockerBinary verifies Docker binary is available.
|
|
func checkDockerBinary() DoctorResult {
|
|
_, err := exec.LookPath("docker")
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Binary",
|
|
Status: "FAIL",
|
|
Message: "Docker binary not found in PATH",
|
|
Suggestions: []string{
|
|
"Install Docker: https://docs.docker.com/get-docker/",
|
|
"For macOS: consider using colima or Docker Desktop",
|
|
"Ensure docker is in your PATH",
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Docker Binary",
|
|
Status: "PASS",
|
|
Message: "Docker binary found",
|
|
}
|
|
}
|
|
|
|
// checkDockerDaemon verifies Docker daemon is running and accessible.
|
|
func checkDockerDaemon(ctx context.Context) DoctorResult {
|
|
cli, err := createDockerClient()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Daemon",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot create Docker client: %v", err),
|
|
Suggestions: []string{
|
|
"Start Docker daemon/service",
|
|
"Check Docker Desktop is running (if using Docker Desktop)",
|
|
"For colima: run 'colima start'",
|
|
"Verify DOCKER_HOST environment variable if set",
|
|
},
|
|
}
|
|
}
|
|
defer cli.Close()
|
|
|
|
_, err = cli.Ping(ctx)
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Daemon",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot ping Docker daemon: %v", err),
|
|
Suggestions: []string{
|
|
"Ensure Docker daemon is running",
|
|
"Check Docker socket permissions",
|
|
"Try: docker info",
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Docker Daemon",
|
|
Status: "PASS",
|
|
Message: "Docker daemon is running and accessible",
|
|
}
|
|
}
|
|
|
|
// checkDockerContext verifies Docker context configuration.
|
|
func checkDockerContext(_ context.Context) DoctorResult {
|
|
contextInfo, err := getCurrentDockerContext()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Context",
|
|
Status: "WARN",
|
|
Message: "Could not detect Docker context, using default settings",
|
|
Suggestions: []string{
|
|
"Check: docker context ls",
|
|
"Consider setting up a specific context if needed",
|
|
},
|
|
}
|
|
}
|
|
|
|
if contextInfo == nil {
|
|
return DoctorResult{
|
|
Name: "Docker Context",
|
|
Status: "PASS",
|
|
Message: "Using default Docker context",
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Docker Context",
|
|
Status: "PASS",
|
|
Message: "Using Docker context: " + contextInfo.Name,
|
|
}
|
|
}
|
|
|
|
// checkDockerSocket verifies Docker socket accessibility.
|
|
func checkDockerSocket(ctx context.Context) DoctorResult {
|
|
cli, err := createDockerClient()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Socket",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot access Docker socket: %v", err),
|
|
Suggestions: []string{
|
|
"Check Docker socket permissions",
|
|
"Add user to docker group: sudo usermod -aG docker $USER",
|
|
"For colima: ensure socket is accessible",
|
|
},
|
|
}
|
|
}
|
|
defer cli.Close()
|
|
|
|
info, err := cli.Info(ctx)
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Docker Socket",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot get Docker info: %v", err),
|
|
Suggestions: []string{
|
|
"Check Docker daemon status",
|
|
"Verify socket permissions",
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Docker Socket",
|
|
Status: "PASS",
|
|
Message: fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion),
|
|
}
|
|
}
|
|
|
|
// checkGolangImage verifies we can access the golang Docker image.
|
|
func checkGolangImage(ctx context.Context) DoctorResult {
|
|
cli, err := createDockerClient()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Golang Image",
|
|
Status: "FAIL",
|
|
Message: "Cannot create Docker client for image check",
|
|
}
|
|
}
|
|
defer cli.Close()
|
|
|
|
goVersion := detectGoVersion()
|
|
imageName := "golang:" + goVersion
|
|
|
|
// Check if we can pull the image
|
|
err = ensureImageAvailable(ctx, cli, imageName, false)
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Golang Image",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot pull golang image %s: %v", imageName, err),
|
|
Suggestions: []string{
|
|
"Check internet connectivity",
|
|
"Verify Docker Hub access",
|
|
"Try: docker pull " + imageName,
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Golang Image",
|
|
Status: "PASS",
|
|
Message: fmt.Sprintf("Golang image %s is available", imageName),
|
|
}
|
|
}
|
|
|
|
// checkGoInstallation verifies Go is installed and working.
|
|
func checkGoInstallation() DoctorResult {
|
|
_, err := exec.LookPath("go")
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Go Installation",
|
|
Status: "FAIL",
|
|
Message: "Go binary not found in PATH",
|
|
Suggestions: []string{
|
|
"Install Go: https://golang.org/dl/",
|
|
"Ensure go is in your PATH",
|
|
},
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("go", "version")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Go Installation",
|
|
Status: "FAIL",
|
|
Message: fmt.Sprintf("Cannot get Go version: %v", err),
|
|
}
|
|
}
|
|
|
|
version := strings.TrimSpace(string(output))
|
|
|
|
return DoctorResult{
|
|
Name: "Go Installation",
|
|
Status: "PASS",
|
|
Message: version,
|
|
}
|
|
}
|
|
|
|
// checkGitRepository verifies we're in a git repository.
|
|
func checkGitRepository() DoctorResult {
|
|
cmd := exec.Command("git", "rev-parse", "--git-dir")
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return DoctorResult{
|
|
Name: "Git Repository",
|
|
Status: "FAIL",
|
|
Message: "Not in a Git repository",
|
|
Suggestions: []string{
|
|
"Run from within the headscale git repository",
|
|
"Clone the repository: git clone https://github.com/juanfont/headscale.git",
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Git Repository",
|
|
Status: "PASS",
|
|
Message: "Running in Git repository",
|
|
}
|
|
}
|
|
|
|
// checkRequiredFiles verifies required files exist.
|
|
func checkRequiredFiles() DoctorResult {
|
|
requiredFiles := []string{
|
|
"go.mod",
|
|
"integration/",
|
|
"cmd/hi/",
|
|
}
|
|
|
|
var missingFiles []string
|
|
for _, file := range requiredFiles {
|
|
cmd := exec.Command("test", "-e", file)
|
|
if err := cmd.Run(); err != nil {
|
|
missingFiles = append(missingFiles, file)
|
|
}
|
|
}
|
|
|
|
if len(missingFiles) > 0 {
|
|
return DoctorResult{
|
|
Name: "Required Files",
|
|
Status: "FAIL",
|
|
Message: "Missing required files: " + strings.Join(missingFiles, ", "),
|
|
Suggestions: []string{
|
|
"Ensure you're in the headscale project root directory",
|
|
"Check that integration/ directory exists",
|
|
"Verify this is a complete headscale repository",
|
|
},
|
|
}
|
|
}
|
|
|
|
return DoctorResult{
|
|
Name: "Required Files",
|
|
Status: "PASS",
|
|
Message: "All required files found",
|
|
}
|
|
}
|
|
|
|
// displayDoctorResults shows the results in a formatted way.
|
|
func displayDoctorResults(results []DoctorResult) {
|
|
log.Printf("🔍 System Health Check Results")
|
|
log.Printf("================================")
|
|
|
|
for _, result := range results {
|
|
var icon string
|
|
switch result.Status {
|
|
case "PASS":
|
|
icon = "✅"
|
|
case "WARN":
|
|
icon = "⚠️"
|
|
case "FAIL":
|
|
icon = "❌"
|
|
default:
|
|
icon = "❓"
|
|
}
|
|
|
|
log.Printf("%s %s: %s", icon, result.Name, result.Message)
|
|
|
|
if len(result.Suggestions) > 0 {
|
|
for _, suggestion := range result.Suggestions {
|
|
log.Printf(" 💡 %s", suggestion)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("================================")
|
|
}
|