From ea7376f522607af8ba64ad73a980994da4ab00b4 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Jun 2025 11:22:15 +0200 Subject: [PATCH] cmd/hi: add integration test runner CLI tool (#2648) * 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 --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 25 +- Makefile | 11 - cmd/hi/cleanup.go | 144 ++++++++++ cmd/hi/docker.go | 364 ++++++++++++++++++++++++ cmd/hi/doctor.go | 353 +++++++++++++++++++++++ cmd/hi/main.go | 93 ++++++ cmd/hi/run.go | 122 ++++++++ flake.nix | 2 +- go.mod | 35 ++- go.sum | 94 +++--- 10 files changed, 1166 insertions(+), 77 deletions(-) create mode 100644 cmd/hi/cleanup.go create mode 100644 cmd/hi/docker.go create mode 100644 cmd/hi/doctor.go create mode 100644 cmd/hi/main.go create mode 100644 cmd/hi/run.go diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index b0e2daea..19020475 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -129,8 +129,6 @@ jobs: - name: Run Integration Test uses: Wandalen/wretry.action@master if: steps.changed-files.outputs.files == 'true' - env: - USE_POSTGRES: ${{ matrix.database == 'postgres' && '1' || '0' }} with: # Our integration tests are started like a thundering herd, often # hitting limits of the various external repositories we depend on @@ -144,30 +142,19 @@ jobs: attempt_delay: 300000 # 5 min attempt_limit: 10 command: | - nix develop --command -- docker run \ - --tty --rm \ - --volume ~/.cache/hs-integration-go:/go \ - --name headscale-test-suite \ - --volume $PWD:$PWD -w $PWD/integration \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - --volume $PWD/control_logs:/tmp/control \ - --env HEADSCALE_INTEGRATION_POSTGRES=${{env.USE_POSTGRES}} \ - golang:1 \ - go run gotest.tools/gotestsum@latest -- ./... \ - -failfast \ - -timeout 120m \ - -parallel 1 \ - -run "^${{ matrix.test }}$" + nix develop --command -- go run ./cmd/hi run "^${{ matrix.test }}$" \ + --timeout=120m \ + --postgres=${{ matrix.database == 'postgres' && 'true' || 'false' }} - uses: actions/upload-artifact@v4 if: always() && steps.changed-files.outputs.files == 'true' with: name: ${{ matrix.test }}-${{matrix.database}}-logs - path: "control_logs/*.log" + path: "control_logs/*/*.log" - uses: actions/upload-artifact@v4 if: always() && steps.changed-files.outputs.files == 'true' with: - name: ${{ matrix.test }}-${{matrix.database}}-pprof - path: "control_logs/*.pprof.tar" + name: ${{ matrix.test }}-${{matrix.database}}-archives + path: "control_logs/*/*.tar" - name: Setup a blocking tmux session if: ${{ env.HAS_TAILSCALE_SECRET }} uses: alexellis/block-with-tmux-action@master diff --git a/Makefile b/Makefile index 25fa1c67..7fff2724 100644 --- a/Makefile +++ b/Makefile @@ -24,17 +24,6 @@ dev: lint test build test: gotestsum -- -short -race -coverprofile=coverage.out ./... -test_integration: - docker run \ - -t --rm \ - -v ~/.cache/hs-integration-go:/go \ - --name headscale-test-suite \ - -v $$PWD:$$PWD -w $$PWD/integration \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v $$PWD/control_logs:/tmp/control \ - golang:1 \ - go run gotest.tools/gotestsum@latest -- -race -failfast ./... -timeout 120m -parallel 8 - lint: golangci-lint run --fix --timeout 10m diff --git a/cmd/hi/cleanup.go b/cmd/hi/cleanup.go new file mode 100644 index 00000000..d20fca73 --- /dev/null +++ b/cmd/hi/cleanup.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" +) + +// cleanupBeforeTest performs cleanup operations before running tests. +func cleanupBeforeTest(ctx context.Context) error { + if err := killTestContainers(ctx); err != nil { + return fmt.Errorf("failed to kill test containers: %w", err) + } + + if err := pruneDockerNetworks(ctx); err != nil { + return fmt.Errorf("failed to prune networks: %w", err) + } + + return nil +} + +// cleanupAfterTest removes the test container after completion. +func cleanupAfterTest(ctx context.Context, cli *client.Client, containerID string) error { + return cli.ContainerRemove(ctx, containerID, container.RemoveOptions{ + Force: true, + }) +} + +// killTestContainers terminates all running test containers. +func killTestContainers(ctx context.Context) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + containers, err := cli.ContainerList(ctx, container.ListOptions{ + All: true, + }) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) + } + + killed := 0 + for _, cont := range containers { + shouldKill := false + for _, name := range cont.Names { + if strings.Contains(name, "headscale-test-suite") || + strings.Contains(name, "hs-") || + strings.Contains(name, "ts-") { + shouldKill = true + break + } + } + + if shouldKill { + if err := cli.ContainerKill(ctx, cont.ID, "KILL"); err == nil { + killed++ + } + } + } + + return nil +} + +// pruneDockerNetworks removes unused Docker networks. +func pruneDockerNetworks(ctx context.Context) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + _, err = cli.NetworksPrune(ctx, filters.Args{}) + if err != nil { + return fmt.Errorf("failed to prune networks: %w", err) + } + + return nil +} + +// cleanOldImages removes test-related and old dangling Docker images. +func cleanOldImages(ctx context.Context) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + images, err := cli.ImageList(ctx, image.ListOptions{ + All: true, + }) + if err != nil { + return fmt.Errorf("failed to list images: %w", err) + } + + removed := 0 + for _, img := range images { + shouldRemove := false + for _, tag := range img.RepoTags { + if strings.Contains(tag, "hs-") || + strings.Contains(tag, "headscale-integration") || + strings.Contains(tag, "tailscale") { + shouldRemove = true + break + } + } + + if len(img.RepoTags) == 0 && time.Unix(img.Created, 0).Before(time.Now().Add(-7*24*time.Hour)) { + shouldRemove = true + } + + if shouldRemove { + _, err := cli.ImageRemove(ctx, img.ID, image.RemoveOptions{ + Force: true, + }) + if err == nil { + removed++ + } + } + } + + return nil +} + +// cleanCacheVolume removes the Docker volume used for Go module cache. +func cleanCacheVolume(ctx context.Context) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + volumeName := "hs-integration-go-cache" + _ = cli.VolumeRemove(ctx, volumeName, true) + + return nil +} diff --git a/cmd/hi/docker.go b/cmd/hi/docker.go new file mode 100644 index 00000000..8b22fa5e --- /dev/null +++ b/cmd/hi/docker.go @@ -0,0 +1,364 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/client" +) + +var ( + ErrTestFailed = errors.New("test failed") + ErrUnexpectedContainerWait = errors.New("unexpected end of container wait") + ErrNoDockerContext = errors.New("no docker context found") +) + +// runTestContainer executes integration tests in a Docker container. +func runTestContainer(ctx context.Context, config *RunConfig) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + runID := generateRunID() + containerName := "headscale-test-suite-" + runID + logsDir := filepath.Join(config.LogsDir, runID) + + if config.Verbose { + log.Printf("Run ID: %s", runID) + log.Printf("Container name: %s", containerName) + log.Printf("Logs directory: %s", logsDir) + } + + absLogsDir, err := filepath.Abs(logsDir) + if err != nil { + return fmt.Errorf("failed to get absolute path for logs directory: %w", err) + } + + const dirPerm = 0o755 + if err := os.MkdirAll(absLogsDir, dirPerm); err != nil { + return fmt.Errorf("failed to create logs directory: %w", err) + } + + if config.CleanBefore { + if config.Verbose { + log.Printf("Running pre-test cleanup...") + } + if err := cleanupBeforeTest(ctx); err != nil && config.Verbose { + log.Printf("Warning: pre-test cleanup failed: %v", err) + } + } + + goTestCmd := buildGoTestCommand(config) + if config.Verbose { + log.Printf("Command: %s", strings.Join(goTestCmd, " ")) + } + + imageName := "golang:" + config.GoVersion + if err := ensureImageAvailable(ctx, cli, imageName, config.Verbose); err != nil { + return fmt.Errorf("failed to ensure image availability: %w", err) + } + + resp, err := createGoTestContainer(ctx, cli, config, containerName, absLogsDir, goTestCmd) + if err != nil { + return fmt.Errorf("failed to create container: %w", err) + } + + if config.Verbose { + log.Printf("Created container: %s", resp.ID) + } + + if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start container: %w", err) + } + + log.Printf("Starting test: %s", config.TestPattern) + + exitCode, err := streamAndWait(ctx, cli, resp.ID) + + shouldCleanup := config.CleanAfter && (!config.KeepOnFailure || exitCode == 0) + if shouldCleanup { + if config.Verbose { + log.Printf("Running post-test cleanup...") + } + if cleanErr := cleanupAfterTest(ctx, cli, resp.ID); cleanErr != nil && config.Verbose { + log.Printf("Warning: post-test cleanup failed: %v", cleanErr) + } + } + + if err != nil { + return fmt.Errorf("test execution failed: %w", err) + } + + if exitCode != 0 { + return fmt.Errorf("%w: exit code %d", ErrTestFailed, exitCode) + } + + log.Printf("Test completed successfully!") + listControlFiles(logsDir) + + return nil +} + +// buildGoTestCommand constructs the go test command arguments. +func buildGoTestCommand(config *RunConfig) []string { + cmd := []string{"go", "test", "./..."} + + if config.TestPattern != "" { + cmd = append(cmd, "-run", config.TestPattern) + } + + if config.FailFast { + cmd = append(cmd, "-failfast") + } + + cmd = append(cmd, "-timeout", config.Timeout.String()) + cmd = append(cmd, "-v") + + return cmd +} + +// createGoTestContainer creates a Docker container configured for running integration tests. +func createGoTestContainer(ctx context.Context, cli *client.Client, config *RunConfig, containerName, logsDir string, goTestCmd []string) (container.CreateResponse, error) { + pwd, err := os.Getwd() + if err != nil { + return container.CreateResponse{}, fmt.Errorf("failed to get working directory: %w", err) + } + + projectRoot := findProjectRoot(pwd) + + env := []string{ + fmt.Sprintf("HEADSCALE_INTEGRATION_POSTGRES=%d", boolToInt(config.UsePostgres)), + } + + containerConfig := &container.Config{ + Image: "golang:" + config.GoVersion, + Cmd: goTestCmd, + Env: env, + WorkingDir: projectRoot + "/integration", + Tty: true, + } + + hostConfig := &container.HostConfig{ + AutoRemove: false, // We'll remove manually for better control + Binds: []string{ + fmt.Sprintf("%s:%s", projectRoot, projectRoot), + "/var/run/docker.sock:/var/run/docker.sock", + logsDir + ":/tmp/control", + }, + Mounts: []mount.Mount{ + { + Type: mount.TypeVolume, + Source: "hs-integration-go-cache", + Target: "/go", + }, + }, + } + + return cli.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName) +} + +// streamAndWait streams container output and waits for completion. +func streamAndWait(ctx context.Context, cli *client.Client, containerID string) (int, error) { + out, err := cli.ContainerLogs(ctx, containerID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + return -1, fmt.Errorf("failed to get container logs: %w", err) + } + defer out.Close() + + go func() { + _, _ = io.Copy(os.Stdout, out) + }() + + statusCh, errCh := cli.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + if err != nil { + return -1, fmt.Errorf("error waiting for container: %w", err) + } + case status := <-statusCh: + return int(status.StatusCode), nil + } + + return -1, ErrUnexpectedContainerWait +} + +// generateRunID creates a unique timestamp-based run identifier. +func generateRunID() string { + now := time.Now() + timestamp := now.Format("20060102-150405") + return timestamp +} + +// findProjectRoot locates the project root by finding the directory containing go.mod. +func findProjectRoot(startPath string) string { + current := startPath + for { + if _, err := os.Stat(filepath.Join(current, "go.mod")); err == nil { + return current + } + parent := filepath.Dir(current) + if parent == current { + return startPath + } + current = parent + } +} + +// boolToInt converts a boolean to an integer for environment variables. +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +// DockerContext represents Docker context information. +type DockerContext struct { + Name string `json:"Name"` + Metadata map[string]interface{} `json:"Metadata"` + Endpoints map[string]interface{} `json:"Endpoints"` + Current bool `json:"Current"` +} + +// createDockerClient creates a Docker client with context detection. +func createDockerClient() (*client.Client, error) { + contextInfo, err := getCurrentDockerContext() + if err != nil { + return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + } + + var clientOpts []client.Opt + clientOpts = append(clientOpts, client.WithAPIVersionNegotiation()) + + if contextInfo != nil { + if endpoints, ok := contextInfo.Endpoints["docker"]; ok { + if endpointMap, ok := endpoints.(map[string]interface{}); ok { + if host, ok := endpointMap["Host"].(string); ok { + if runConfig.Verbose { + log.Printf("Using Docker host from context '%s': %s", contextInfo.Name, host) + } + clientOpts = append(clientOpts, client.WithHost(host)) + } + } + } + } + + if len(clientOpts) == 1 { + clientOpts = append(clientOpts, client.FromEnv) + } + + return client.NewClientWithOpts(clientOpts...) +} + +// getCurrentDockerContext retrieves the current Docker context information. +func getCurrentDockerContext() (*DockerContext, error) { + cmd := exec.Command("docker", "context", "inspect") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to get docker context: %w", err) + } + + var contexts []DockerContext + if err := json.Unmarshal(output, &contexts); err != nil { + return nil, fmt.Errorf("failed to parse docker context: %w", err) + } + + if len(contexts) > 0 { + return &contexts[0], nil + } + + return nil, ErrNoDockerContext +} + +// ensureImageAvailable pulls the specified Docker image to ensure it's available. +func ensureImageAvailable(ctx context.Context, cli *client.Client, imageName string, verbose bool) error { + if verbose { + log.Printf("Pulling image %s...", imageName) + } + + reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{}) + if err != nil { + return fmt.Errorf("failed to pull image %s: %w", imageName, err) + } + defer reader.Close() + + if verbose { + _, err = io.Copy(os.Stdout, reader) + if err != nil { + return fmt.Errorf("failed to read pull output: %w", err) + } + } else { + _, err = io.Copy(io.Discard, reader) + if err != nil { + return fmt.Errorf("failed to read pull output: %w", err) + } + log.Printf("Image %s pulled successfully", imageName) + } + + return nil +} + +// listControlFiles displays the headscale test artifacts created in the control logs directory. +func listControlFiles(logsDir string) { + entries, err := os.ReadDir(logsDir) + if err != nil { + log.Printf("Logs directory: %s", logsDir) + return + } + + var logFiles []string + var tarFiles []string + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + // Only show headscale (hs-*) files + if !strings.HasPrefix(name, "hs-") { + continue + } + + switch { + case strings.HasSuffix(name, ".stderr.log") || strings.HasSuffix(name, ".stdout.log"): + logFiles = append(logFiles, name) + case strings.HasSuffix(name, ".pprof.tar") || strings.HasSuffix(name, ".maps.tar") || strings.HasSuffix(name, ".db.tar"): + tarFiles = append(tarFiles, name) + } + } + + log.Printf("Test artifacts saved to: %s", logsDir) + + if len(logFiles) > 0 { + log.Printf("Headscale logs:") + for _, file := range logFiles { + log.Printf(" %s", file) + } + } + + if len(tarFiles) > 0 { + log.Printf("Headscale archives:") + for _, file := range tarFiles { + log.Printf(" %s", file) + } + } +} diff --git a/cmd/hi/doctor.go b/cmd/hi/doctor.go new file mode 100644 index 00000000..e1b86099 --- /dev/null +++ b/cmd/hi/doctor.go @@ -0,0 +1,353 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os/exec" + "strings" + + "github.com/docker/docker/client" +) + +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 := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + 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("================================") +} diff --git a/cmd/hi/main.go b/cmd/hi/main.go new file mode 100644 index 00000000..baecc6f3 --- /dev/null +++ b/cmd/hi/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "os" + + "github.com/creachadair/command" + "github.com/creachadair/flax" +) + +var runConfig RunConfig + +func main() { + root := command.C{ + Name: "hi", + Help: "Headscale Integration test runner", + Commands: []*command.C{ + { + Name: "run", + Help: "Run integration tests", + Usage: "run [test-pattern] [flags]", + SetFlags: command.Flags(flax.MustBind, &runConfig), + Run: runIntegrationTest, + }, + { + Name: "doctor", + Help: "Check system requirements for running integration tests", + Run: func(env *command.Env) error { + return runDoctorCheck(env.Context()) + }, + }, + { + Name: "clean", + Help: "Clean Docker resources", + Commands: []*command.C{ + { + Name: "networks", + Help: "Prune unused Docker networks", + Run: func(env *command.Env) error { + return pruneDockerNetworks(env.Context()) + }, + }, + { + Name: "images", + Help: "Clean old test images", + Run: func(env *command.Env) error { + return cleanOldImages(env.Context()) + }, + }, + { + Name: "containers", + Help: "Kill all test containers", + Run: func(env *command.Env) error { + return killTestContainers(env.Context()) + }, + }, + { + Name: "cache", + Help: "Clean Go module cache volume", + Run: func(env *command.Env) error { + return cleanCacheVolume(env.Context()) + }, + }, + { + Name: "all", + Help: "Run all cleanup operations", + Run: func(env *command.Env) error { + return cleanAll(env.Context()) + }, + }, + }, + }, + command.HelpCommand(nil), + }, + } + + env := root.NewEnv(nil).MergeFlags(true) + command.RunOrFail(env, os.Args[1:]) +} + +func cleanAll(ctx context.Context) error { + if err := killTestContainers(ctx); err != nil { + return err + } + if err := pruneDockerNetworks(ctx); err != nil { + return err + } + if err := cleanOldImages(ctx); err != nil { + return err + } + + return cleanCacheVolume(ctx) +} diff --git a/cmd/hi/run.go b/cmd/hi/run.go new file mode 100644 index 00000000..f40f563d --- /dev/null +++ b/cmd/hi/run.go @@ -0,0 +1,122 @@ +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 +} diff --git a/flake.nix b/flake.nix index 21304ab9..17d52308 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to those files. - vendorHash = "sha256-dR8xmUIDMIy08lhm7r95GNNMAbXv4qSH3v9HR40HlNk="; + vendorHash = "sha256-8nRaQNwUDbHkp3q54R6eLDh1GkfwBlh4b9w0IkNj2sY="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index 260f3950..13867746 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,10 @@ require ( github.com/chasefleming/elem-go v0.30.0 github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.14.1 + github.com/creachadair/command v0.1.22 + github.com/creachadair/flax v0.0.5 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/docker/docker v28.1.1+incompatible github.com/fsnotify/fsnotify v1.9.0 github.com/glebarez/sqlite v1.11.0 github.com/go-gormigrate/gormigrate/v2 v2.1.4 @@ -40,13 +43,13 @@ require ( github.com/tailscale/tailsql v0.0.0-20250421235516-02f85f087b97 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/net v0.39.0 + golang.org/x/net v0.40.0 golang.org/x/oauth2 v0.29.0 - golang.org/x/sync v0.13.0 - google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 - google.golang.org/grpc v1.72.0 + golang.org/x/sync v0.14.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 + google.golang.org/grpc v1.72.1 google.golang.org/protobuf v1.36.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 @@ -114,18 +117,21 @@ require ( github.com/creachadair/mds v0.24.1 // indirect github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v28.1.1+incompatible // indirect - github.com/docker/docker v28.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect @@ -174,8 +180,10 @@ require ( github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -216,16 +224,23 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.32.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect ) diff --git a/go.sum b/go.sum index 2759bbb1..cce71c15 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,11 @@ github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxY github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -112,12 +115,18 @@ github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creachadair/command v0.1.22 h1:WmdrURwZdmPD1jm13SjKooaMoqo7mW1qI2BPCShs154= +github.com/creachadair/command v0.1.22/go.mod h1:YFc+OMGucqTpxwQg/iJnNg8BMNmRPDK60rYy8ckgKwE= +github.com/creachadair/flax v0.0.5 h1:zt+CRuXQASxwQ68e9GHAOnEgAU29nF0zYMHOCrL5wzE= +github.com/creachadair/flax v0.0.5/go.mod h1:F1PML0JZLXSNDMNiRGK2yjm5f+L9QCHchyHBldFymj8= github.com/creachadair/mds v0.24.1 h1:bzL4ItCtAUxxO9KkotP0PVzlw4tnJicAcjPu82v2mGs= github.com/creachadair/mds v0.24.1/go.mod h1:ArfS0vPHoLV/SzuIzoqTEZfoYmac7n9Cj8XPANHocvw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -132,6 +141,8 @@ github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 h1:vrC07UZcgPzu/Oj github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= @@ -153,6 +164,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -177,6 +190,7 @@ github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6 github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -347,10 +361,16 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -523,16 +543,24 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -548,8 +576,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= @@ -579,8 +607,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= @@ -592,8 +620,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -623,8 +651,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -632,8 +660,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -641,8 +669,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -671,17 +699,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE= -google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -702,8 +730,6 @@ gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= -gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k= @@ -715,19 +741,15 @@ honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= -modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= -modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= -modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= -modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= -modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= -modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= -modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= -modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=