diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ec0b652..cffe57fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,12 +17,12 @@ jobs: runs-on: ubuntu-latest permissions: write-all steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -31,9 +31,9 @@ jobs: - '**/*.go' - 'integration_test/' - 'config-example.yaml' - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} @@ -55,7 +55,7 @@ jobs: exit $BUILD_STATUS - name: Nix gosum diverging - uses: actions/github-script@v6 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 if: failure() && steps.build.outcome == 'failure' with: github-token: ${{secrets.GITHUB_TOKEN}} @@ -67,7 +67,7 @@ jobs: body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}' }) - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: steps.changed-files.outputs.files == 'true' with: name: headscale-linux @@ -86,16 +86,16 @@ jobs: - "GOARCH=arm64 GOOS=darwin" - "GOARCH=amd64 GOOS=darwin" steps: - - uses: actions/checkout@v4 - - uses: nixbuild/nix-quick-install-action@master - - uses: nix-community/cache-nix-action@main + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} - name: Run go cross compile run: env ${{ matrix.env }} nix develop --command -- go build -o "headscale" ./cmd/headscale - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: "headscale-${{ matrix.env }}" path: "headscale" diff --git a/.github/workflows/check-tests.yaml b/.github/workflows/check-tests.yaml index 84149088..a9b53fe7 100644 --- a/.github/workflows/check-tests.yaml +++ b/.github/workflows/check-tests.yaml @@ -10,12 +10,12 @@ jobs: check-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -24,9 +24,9 @@ jobs: - '**/*.go' - 'integration_test/' - 'config-example.yaml' - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 94b285e7..7d06b6a6 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -21,15 +21,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.x - name: Setup cache - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: key: ${{ github.ref }} path: .cache diff --git a/.github/workflows/docs-test.yml b/.github/workflows/docs-test.yml index a2b15324..63c547c8 100644 --- a/.github/workflows/docs-test.yml +++ b/.github/workflows/docs-test.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.x - name: Setup cache - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: key: ${{ github.ref }} path: .cache diff --git a/.github/workflows/gh-actions-updater.yaml b/.github/workflows/gh-actions-updater.yaml index f46fb67c..6bda3440 100644 --- a/.github/workflows/gh-actions-updater.yaml +++ b/.github/workflows/gh-actions-updater.yaml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} - name: Run GitHub Actions Version Updater - uses: saadmk11/github-actions-version-updater@v0.8.1 + uses: saadmk11/github-actions-version-updater@64be81ba69383f81f2be476703ea6570c4c8686e # v0.8.1 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 934876b1..43bec0fb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,12 +10,12 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -24,9 +24,9 @@ jobs: - '**/*.go' - 'integration_test/' - 'config-example.yaml' - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} @@ -39,12 +39,12 @@ jobs: prettier-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -58,9 +58,9 @@ jobs: - '**/*.css' - '**/*.scss' - '**/*.html' - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} @@ -73,9 +73,9 @@ jobs: proto-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: nixbuild/nix-quick-install-action@master - - uses: nix-community/cache-nix-action@main + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e43012bf..c06e31f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,25 +13,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: nixbuild/nix-quick-install-action@master - - uses: nix-community/cache-nix-action@main + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e6e5d511..8f9ea805 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: days-before-issue-stale: 90 days-before-issue-close: 7 diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 19020475..b20d1ad6 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -92,12 +92,12 @@ jobs: # that triggered the build. HAS_TAILSCALE_SECRET: ${{ secrets.TS_OAUTH_CLIENT_ID }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -108,7 +108,7 @@ jobs: - 'config-example.yaml' - name: Tailscale if: ${{ env.HAS_TAILSCALE_SECRET }} - uses: tailscale/github-action@v2 + uses: tailscale/github-action@6986d2c82a91fbac2949fe01f5bab95cf21b5102 # v3.2.2 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} @@ -116,18 +116,15 @@ jobs: - name: Setup SSH server for Actor if: ${{ env.HAS_TAILSCALE_SECRET }} uses: alexellis/setup-sshd-actor@master - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} - - uses: satackey/action-docker-layer-caching@main - if: steps.changed-files.outputs.files == 'true' - continue-on-error: true - name: Run Integration Test - uses: Wandalen/wretry.action@master + uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 if: steps.changed-files.outputs.files == 'true' with: # Our integration tests are started like a thundering herd, often @@ -142,15 +139,15 @@ jobs: attempt_delay: 300000 # 5 min attempt_limit: 10 command: | - nix develop --command -- go run ./cmd/hi run "^${{ matrix.test }}$" \ + nix develop --command -- hi run "^${{ matrix.test }}$" \ --timeout=120m \ --postgres=${{ matrix.database == 'postgres' && 'true' || 'false' }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() && steps.changed-files.outputs.files == 'true' with: name: ${{ matrix.test }}-${{matrix.database}}-logs path: "control_logs/*/*.log" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() && steps.changed-files.outputs.files == 'true' with: name: ${{ matrix.test }}-${{matrix.database}}-archives diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0384b6ec..9860390e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - name: Get changed files id: changed-files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | files: @@ -27,9 +27,9 @@ jobs: - 'integration_test/' - 'config-example.yaml' - - uses: nixbuild/nix-quick-install-action@master + - uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31 if: steps.changed-files.outputs.files == 'true' - - uses: nix-community/cache-nix-action@main + - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 if: steps.changed-files.outputs.files == 'true' with: primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 35067784..1c8b262e 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 - name: Update flake.lock - uses: DeterminateSystems/update-flake-lock@main + uses: DeterminateSystems/update-flake-lock@428c2b58a4b7414dabd372acb6a03dba1084d3ab # v25 with: pr-title: "Update flake.lock" diff --git a/Makefile b/Makefile index 7fff2724..563109a6 100644 --- a/Makefile +++ b/Makefile @@ -1,53 +1,130 @@ -# Calculate version -version ?= $(shell git describe --always --tags --dirty) +# Headscale Makefile +# Modern Makefile following best practices -rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d)) +# Version calculation +VERSION ?= $(shell git describe --always --tags --dirty) -# Determine if OS supports pie +# Build configuration GOOS ?= $(shell uname | tr '[:upper:]' '[:lower:]') -ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), ) - pieflags = -buildmode=pie -else +ifeq ($(filter $(GOOS), openbsd netbsd solaris plan9), ) + PIE_FLAGS = -buildmode=pie endif -# GO_SOURCES = $(wildcard *.go) -# PROTO_SOURCES = $(wildcard **/*.proto) -GO_SOURCES = $(call rwildcard,,*.go) -PROTO_SOURCES = $(call rwildcard,,*.proto) +# Tool availability check with nix warning +define check_tool + @command -v $(1) >/dev/null 2>&1 || { \ + echo "Warning: $(1) not found. Run 'nix develop' to ensure all dependencies are available."; \ + exit 1; \ + } +endef + +# Source file collections using shell find for better performance +GO_SOURCES := $(shell find . -name '*.go' -not -path './gen/*' -not -path './vendor/*') +PROTO_SOURCES := $(shell find . -name '*.proto' -not -path './gen/*' -not -path './vendor/*') +DOC_SOURCES := $(shell find . \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.ts' -o -name '*.js' -o -name '*.html' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -not -path './gen/*' -not -path './vendor/*' -not -path './node_modules/*') + +# Default target +.PHONY: all +all: lint test build + +# Dependency checking +.PHONY: check-deps +check-deps: + $(call check_tool,go) + $(call check_tool,golangci-lint) + $(call check_tool,gofumpt) + $(call check_tool,prettier) + $(call check_tool,clang-format) + $(call check_tool,buf) + +# Build targets +.PHONY: build +build: check-deps $(GO_SOURCES) go.mod go.sum + @echo "Building headscale..." + go build $(PIE_FLAGS) -ldflags "-X main.version=$(VERSION)" -o headscale ./cmd/headscale + +# Test targets +.PHONY: test +test: check-deps $(GO_SOURCES) go.mod go.sum + @echo "Running Go tests..." + go test -race ./... -build: - nix build - -dev: lint test build - -test: - gotestsum -- -short -race -coverprofile=coverage.out ./... - -lint: - golangci-lint run --fix --timeout 10m - +# Formatting targets +.PHONY: fmt fmt: fmt-go fmt-prettier fmt-proto -fmt-prettier: - prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}' - prettier --write --print-width 80 --prose-wrap always CHANGELOG.md - -fmt-go: - # TODO(kradalby): Reeval if we want to use 88 in the future. - # golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES) +.PHONY: fmt-go +fmt-go: check-deps $(GO_SOURCES) + @echo "Formatting Go code..." gofumpt -l -w . golangci-lint run --fix -fmt-proto: +.PHONY: fmt-prettier +fmt-prettier: check-deps $(DOC_SOURCES) + @echo "Formatting documentation and config files..." + prettier --write '**/*.{ts,js,md,yaml,yml,sass,css,scss,html}' + prettier --write --print-width 80 --prose-wrap always CHANGELOG.md + +.PHONY: fmt-proto +fmt-proto: check-deps $(PROTO_SOURCES) + @echo "Formatting Protocol Buffer files..." clang-format -i $(PROTO_SOURCES) -proto-lint: - cd proto/ && go run github.com/bufbuild/buf/cmd/buf lint +# Linting targets +.PHONY: lint +lint: lint-go lint-proto -compress: build - upx --brute headscale +.PHONY: lint-go +lint-go: check-deps $(GO_SOURCES) go.mod go.sum + @echo "Linting Go code..." + golangci-lint run --timeout 10m -generate: +.PHONY: lint-proto +lint-proto: check-deps $(PROTO_SOURCES) + @echo "Linting Protocol Buffer files..." + cd proto/ && buf lint + +# Code generation +.PHONY: generate +generate: check-deps $(PROTO_SOURCES) + @echo "Generating code from Protocol Buffers..." rm -rf gen buf generate proto + +# Clean targets +.PHONY: clean +clean: + rm -rf headscale gen + +# Development workflow +.PHONY: dev +dev: fmt lint test build + +# Help target +.PHONY: help +help: + @echo "Headscale Development Makefile" + @echo "" + @echo "Main targets:" + @echo " all - Run lint, test, and build (default)" + @echo " build - Build headscale binary" + @echo " test - Run Go tests" + @echo " fmt - Format all code (Go, docs, proto)" + @echo " lint - Lint all code (Go, proto)" + @echo " generate - Generate code from Protocol Buffers" + @echo " dev - Full development workflow (fmt + lint + test + build)" + @echo " clean - Clean build artifacts" + @echo "" + @echo "Specific targets:" + @echo " fmt-go - Format Go code only" + @echo " fmt-prettier - Format documentation only" + @echo " fmt-proto - Format Protocol Buffer files only" + @echo " lint-go - Lint Go code only" + @echo " lint-proto - Lint Protocol Buffer files only" + @echo "" + @echo "Dependencies:" + @echo " check-deps - Verify required tools are available" + @echo "" + @echo "Note: If not running in a nix shell, ensure dependencies are available:" + @echo " nix develop" \ No newline at end of file diff --git a/README.md b/README.md index 1114ae59..8bfd2586 100644 --- a/README.md +++ b/README.md @@ -138,16 +138,29 @@ make test To build the program: -```shell -nix build -``` - -or - ```shell make build ``` +### Development workflow + +We recommend using Nix for dependency management to ensure you have all required tools. If you prefer to manage dependencies yourself, you can use Make directly: + +**With Nix (recommended):** +```shell +nix develop +make test +make build +``` + +**With your own dependencies:** +```shell +make test +make build +``` + +The Makefile will warn you if any required tools are missing and suggest running `nix develop`. Run `make help` to see all available targets. + ## Contributors diff --git a/cmd/hi/cleanup.go b/cmd/hi/cleanup.go index d20fca73..080266d8 100644 --- a/cmd/hi/cleanup.go +++ b/cmd/hi/cleanup.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" ) // cleanupBeforeTest performs cleanup operations before running tests. @@ -32,7 +33,7 @@ func cleanupAfterTest(ctx context.Context, cli *client.Client, containerID strin }) } -// killTestContainers terminates all running test containers. +// killTestContainers terminates and removes all test containers. func killTestContainers(ctx context.Context) error { cli, err := createDockerClient() if err != nil { @@ -47,28 +48,67 @@ func killTestContainers(ctx context.Context) error { return fmt.Errorf("failed to list containers: %w", err) } - killed := 0 + removed := 0 for _, cont := range containers { - shouldKill := false + shouldRemove := false for _, name := range cont.Names { if strings.Contains(name, "headscale-test-suite") || strings.Contains(name, "hs-") || - strings.Contains(name, "ts-") { - shouldKill = true + strings.Contains(name, "ts-") || + strings.Contains(name, "derp-") { + shouldRemove = true break } } - if shouldKill { - if err := cli.ContainerKill(ctx, cont.ID, "KILL"); err == nil { - killed++ + if shouldRemove { + // First kill the container if it's running + if cont.State == "running" { + _ = cli.ContainerKill(ctx, cont.ID, "KILL") + } + + // Then remove the container with retry logic + if removeContainerWithRetry(ctx, cli, cont.ID) { + removed++ } } } + if removed > 0 { + fmt.Printf("Removed %d test containers\n", removed) + } else { + fmt.Println("No test containers found to remove") + } + return nil } +// removeContainerWithRetry attempts to remove a container with exponential backoff retry logic. +func removeContainerWithRetry(ctx context.Context, cli *client.Client, containerID string) bool { + maxRetries := 3 + baseDelay := 100 * time.Millisecond + + for attempt := 0; attempt < maxRetries; attempt++ { + err := cli.ContainerRemove(ctx, containerID, container.RemoveOptions{ + Force: true, + }) + if err == nil { + return true + } + + // If this is the last attempt, don't wait + if attempt == maxRetries-1 { + break + } + + // Wait with exponential backoff + delay := baseDelay * time.Duration(1< 0 { + fmt.Printf("Removed %d unused networks\n", len(report.NetworksDeleted)) + } else { + fmt.Println("No unused networks found to remove") + } + return nil } @@ -126,6 +172,12 @@ func cleanOldImages(ctx context.Context) error { } } + if removed > 0 { + fmt.Printf("Removed %d test images\n", removed) + } else { + fmt.Println("No test images found to remove") + } + return nil } @@ -138,7 +190,18 @@ func cleanCacheVolume(ctx context.Context) error { defer cli.Close() volumeName := "hs-integration-go-cache" - _ = cli.VolumeRemove(ctx, volumeName, true) + err = cli.VolumeRemove(ctx, volumeName, true) + if err != nil { + if errdefs.IsNotFound(err) { + fmt.Printf("Go module cache volume not found: %s\n", volumeName) + } else if errdefs.IsConflict(err) { + fmt.Printf("Go module cache volume is in use and cannot be removed: %s\n", volumeName) + } else { + fmt.Printf("Failed to remove Go module cache volume %s: %v\n", volumeName, err) + } + } else { + fmt.Printf("Removed Go module cache volume: %s\n", volumeName) + } return nil } diff --git a/cmd/hi/docker.go b/cmd/hi/docker.go index 8b22fa5e..284cc691 100644 --- a/cmd/hi/docker.go +++ b/cmd/hi/docker.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "errors" @@ -17,6 +18,8 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/juanfont/headscale/integration/dockertestutil" ) var ( @@ -33,7 +36,7 @@ func runTestContainer(ctx context.Context, config *RunConfig) error { } defer cli.Close() - runID := generateRunID() + runID := dockertestutil.GenerateRunID() containerName := "headscale-test-suite-" + runID logsDir := filepath.Join(config.LogsDir, runID) @@ -89,6 +92,19 @@ func runTestContainer(ctx context.Context, config *RunConfig) error { exitCode, err := streamAndWait(ctx, cli, resp.ID) + // Ensure all containers have finished and logs are flushed before extracting artifacts + if waitErr := waitForContainerFinalization(ctx, cli, resp.ID, config.Verbose); waitErr != nil && config.Verbose { + log.Printf("Warning: failed to wait for container finalization: %v", waitErr) + } + + // Extract artifacts from test containers before cleanup + if err := extractArtifactsFromContainers(ctx, resp.ID, logsDir, config.Verbose); err != nil && config.Verbose { + log.Printf("Warning: failed to extract artifacts from containers: %v", err) + } + + // Always list control files regardless of test outcome + listControlFiles(logsDir) + shouldCleanup := config.CleanAfter && (!config.KeepOnFailure || exitCode == 0) if shouldCleanup { if config.Verbose { @@ -108,7 +124,6 @@ func runTestContainer(ctx context.Context, config *RunConfig) error { } log.Printf("Test completed successfully!") - listControlFiles(logsDir) return nil } @@ -140,23 +155,36 @@ func createGoTestContainer(ctx context.Context, cli *client.Client, config *RunC projectRoot := findProjectRoot(pwd) + runID := dockertestutil.ExtractRunIDFromContainerName(containerName) + env := []string{ fmt.Sprintf("HEADSCALE_INTEGRATION_POSTGRES=%d", boolToInt(config.UsePostgres)), + fmt.Sprintf("HEADSCALE_INTEGRATION_RUN_ID=%s", runID), } - containerConfig := &container.Config{ Image: "golang:" + config.GoVersion, Cmd: goTestCmd, Env: env, WorkingDir: projectRoot + "/integration", Tty: true, + Labels: map[string]string{ + "hi.run-id": runID, + "hi.test-type": "test-runner", + }, + } + + // Get the correct Docker socket path from the current context + dockerSocketPath := getDockerSocketPath() + + if config.Verbose { + log.Printf("Using Docker socket: %s", dockerSocketPath) } 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", + fmt.Sprintf("%s:/var/run/docker.sock", dockerSocketPath), logsDir + ":/tmp/control", }, Mounts: []mount.Mount{ @@ -200,13 +228,69 @@ func streamAndWait(ctx context.Context, cli *client.Client, containerID string) return -1, ErrUnexpectedContainerWait } -// generateRunID creates a unique timestamp-based run identifier. -func generateRunID() string { - now := time.Now() - timestamp := now.Format("20060102-150405") - return timestamp +// waitForContainerFinalization ensures all test containers have properly finished and flushed their output. +func waitForContainerFinalization(ctx context.Context, cli *client.Client, testContainerID string, verbose bool) error { + // First, get all related test containers + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) + } + + testContainers := getCurrentTestContainers(containers, testContainerID, verbose) + + // Wait for all test containers to reach a final state + maxWaitTime := 10 * time.Second + checkInterval := 500 * time.Millisecond + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-timeout: + if verbose { + log.Printf("Timeout waiting for container finalization, proceeding with artifact extraction") + } + return nil + case <-ticker.C: + allFinalized := true + + for _, testCont := range testContainers { + inspect, err := cli.ContainerInspect(ctx, testCont.ID) + if err != nil { + if verbose { + log.Printf("Warning: failed to inspect container %s: %v", testCont.name, err) + } + continue + } + + // Check if container is in a final state + if !isContainerFinalized(inspect.State) { + allFinalized = false + if verbose { + log.Printf("Container %s still finalizing (state: %s)", testCont.name, inspect.State.Status) + } + break + } + } + + if allFinalized { + if verbose { + log.Printf("All test containers finalized, ready for artifact extraction") + } + return nil + } + } + } } +// isContainerFinalized checks if a container has reached a final state where logs are flushed. +func isContainerFinalized(state *container.State) bool { + // Container is finalized if it's not running and has a finish time + return !state.Running && state.FinishedAt != "" +} + + // findProjectRoot locates the project root by finding the directory containing go.mod. func findProjectRoot(startPath string) string { current := startPath @@ -288,6 +372,13 @@ func getCurrentDockerContext() (*DockerContext, error) { return nil, ErrNoDockerContext } +// getDockerSocketPath returns the correct Docker socket path for the current context. +func getDockerSocketPath() string { + // Always use the default socket path for mounting since Docker handles + // the translation to the actual socket (e.g., colima socket) internally + return "/var/run/docker.sock" +} + // 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 { @@ -325,24 +416,29 @@ func listControlFiles(logsDir string) { } var logFiles []string - var tarFiles []string + var dataFiles []string + var dataDirs []string for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - // Only show headscale (hs-*) files + // Only show headscale (hs-*) files and directories 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) + if entry.IsDir() { + // Include directories (pprof, mapresponses) + if strings.Contains(name, "-pprof") || strings.Contains(name, "-mapresponses") { + dataDirs = append(dataDirs, name) + } + } else { + // Include files + switch { + case strings.HasSuffix(name, ".stderr.log") || strings.HasSuffix(name, ".stdout.log"): + logFiles = append(logFiles, name) + case strings.HasSuffix(name, ".db"): + dataFiles = append(dataFiles, name) + } } } @@ -355,10 +451,244 @@ func listControlFiles(logsDir string) { } } - if len(tarFiles) > 0 { - log.Printf("Headscale archives:") - for _, file := range tarFiles { + if len(dataFiles) > 0 || len(dataDirs) > 0 { + log.Printf("Headscale data:") + for _, file := range dataFiles { log.Printf(" %s", file) } + for _, dir := range dataDirs { + log.Printf(" %s/", dir) + } } } + +// extractArtifactsFromContainers collects container logs and files from the specific test run. +func extractArtifactsFromContainers(ctx context.Context, testContainerID, logsDir string, verbose bool) error { + cli, err := createDockerClient() + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + defer cli.Close() + + // List all containers + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) + } + + // Get containers from the specific test run + currentTestContainers := getCurrentTestContainers(containers, testContainerID, verbose) + + extractedCount := 0 + for _, cont := range currentTestContainers { + // Extract container logs and tar files + if err := extractContainerArtifacts(ctx, cli, cont.ID, cont.name, logsDir, verbose); err != nil { + if verbose { + log.Printf("Warning: failed to extract artifacts from container %s (%s): %v", cont.name, cont.ID[:12], err) + } + } else { + if verbose { + log.Printf("Extracted artifacts from container %s (%s)", cont.name, cont.ID[:12]) + } + extractedCount++ + } + } + + if verbose && extractedCount > 0 { + log.Printf("Extracted artifacts from %d containers", extractedCount) + } + + return nil +} + +// testContainer represents a container from the current test run. +type testContainer struct { + ID string + name string +} + +// getCurrentTestContainers filters containers to only include those from the current test run. +func getCurrentTestContainers(containers []container.Summary, testContainerID string, verbose bool) []testContainer { + var testRunContainers []testContainer + + // Find the test container to get its run ID label + var runID string + for _, cont := range containers { + if cont.ID == testContainerID { + if cont.Labels != nil { + runID = cont.Labels["hi.run-id"] + } + break + } + } + + if runID == "" { + log.Printf("Error: test container %s missing required hi.run-id label", testContainerID[:12]) + return testRunContainers + } + + if verbose { + log.Printf("Looking for containers with run ID: %s", runID) + } + + // Find all containers with the same run ID + for _, cont := range containers { + for _, name := range cont.Names { + containerName := strings.TrimPrefix(name, "/") + if strings.HasPrefix(containerName, "hs-") || strings.HasPrefix(containerName, "ts-") { + // Check if container has matching run ID label + if cont.Labels != nil && cont.Labels["hi.run-id"] == runID { + testRunContainers = append(testRunContainers, testContainer{ + ID: cont.ID, + name: containerName, + }) + if verbose { + log.Printf("Including container %s (run ID: %s)", containerName, runID) + } + } + break + } + } + } + + return testRunContainers +} + +// extractContainerArtifacts saves logs and tar files from a container. +func extractContainerArtifacts(ctx context.Context, cli *client.Client, containerID, containerName, logsDir string, verbose bool) error { + // Ensure the logs directory exists + if err := os.MkdirAll(logsDir, 0755); err != nil { + return fmt.Errorf("failed to create logs directory: %w", err) + } + + // Extract container logs + if err := extractContainerLogs(ctx, cli, containerID, containerName, logsDir, verbose); err != nil { + return fmt.Errorf("failed to extract logs: %w", err) + } + + // Extract tar files for headscale containers only + if strings.HasPrefix(containerName, "hs-") { + if err := extractContainerFiles(ctx, cli, containerID, containerName, logsDir, verbose); err != nil { + if verbose { + log.Printf("Warning: failed to extract files from %s: %v", containerName, err) + } + // Don't fail the whole extraction if files are missing + } + } + + return nil +} + +// extractContainerLogs saves the stdout and stderr logs from a container to files. +func extractContainerLogs(ctx context.Context, cli *client.Client, containerID, containerName, logsDir string, verbose bool) error { + // Get container logs + logReader, err := cli.ContainerLogs(ctx, containerID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: false, + Follow: false, + Tail: "all", + }) + if err != nil { + return fmt.Errorf("failed to get container logs: %w", err) + } + defer logReader.Close() + + // Create log files following the headscale naming convention + stdoutPath := filepath.Join(logsDir, containerName+".stdout.log") + stderrPath := filepath.Join(logsDir, containerName+".stderr.log") + + // Create buffers to capture stdout and stderr separately + var stdoutBuf, stderrBuf bytes.Buffer + + // Demultiplex the Docker logs stream to separate stdout and stderr + _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, logReader) + if err != nil { + return fmt.Errorf("failed to demultiplex container logs: %w", err) + } + + // Write stdout logs + if err := os.WriteFile(stdoutPath, stdoutBuf.Bytes(), 0644); err != nil { + return fmt.Errorf("failed to write stdout log: %w", err) + } + + // Write stderr logs + if err := os.WriteFile(stderrPath, stderrBuf.Bytes(), 0644); err != nil { + return fmt.Errorf("failed to write stderr log: %w", err) + } + + if verbose { + log.Printf("Saved logs for %s: %s, %s", containerName, stdoutPath, stderrPath) + } + + return nil +} + +// extractContainerFiles extracts database file and directories from headscale containers. +// Note: The actual file extraction is now handled by the integration tests themselves +// via SaveProfile, SaveMapResponses, and SaveDatabase functions in hsic.go +func extractContainerFiles(ctx context.Context, cli *client.Client, containerID, containerName, logsDir string, verbose bool) error { + // Files are now extracted directly by the integration tests + // This function is kept for potential future use or other file types + return nil +} + +// logExtractionError logs extraction errors with appropriate level based on error type. +func logExtractionError(artifactType, containerName string, err error, verbose bool) { + if errors.Is(err, ErrFileNotFoundInTar) { + // File not found is expected and only logged in verbose mode + if verbose { + log.Printf("No %s found in container %s", artifactType, containerName) + } + } else { + // Other errors are actual failures and should be logged as warnings + log.Printf("Warning: failed to extract %s from %s: %v", artifactType, containerName, err) + } +} + +// extractSingleFile copies a single file from a container. +func extractSingleFile(ctx context.Context, cli *client.Client, containerID, sourcePath, fileName, logsDir string, verbose bool) error { + tarReader, _, err := cli.CopyFromContainer(ctx, containerID, sourcePath) + if err != nil { + return fmt.Errorf("failed to copy %s from container: %w", sourcePath, err) + } + defer tarReader.Close() + + // Extract the single file from the tar + filePath := filepath.Join(logsDir, fileName) + if err := extractFileFromTar(tarReader, filepath.Base(sourcePath), filePath); err != nil { + return fmt.Errorf("failed to extract file from tar: %w", err) + } + + if verbose { + log.Printf("Extracted %s from %s", fileName, containerID[:12]) + } + + return nil +} + +// extractDirectory copies a directory from a container and extracts its contents. +func extractDirectory(ctx context.Context, cli *client.Client, containerID, sourcePath, dirName, logsDir string, verbose bool) error { + tarReader, _, err := cli.CopyFromContainer(ctx, containerID, sourcePath) + if err != nil { + return fmt.Errorf("failed to copy %s from container: %w", sourcePath, err) + } + defer tarReader.Close() + + // Create target directory + targetDir := filepath.Join(logsDir, dirName) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetDir, err) + } + + // Extract the directory from the tar + if err := extractDirectoryFromTar(tarReader, targetDir); err != nil { + return fmt.Errorf("failed to extract directory from tar: %w", err) + } + + if verbose { + log.Printf("Extracted %s/ from %s", dirName, containerID[:12]) + } + + return nil +} diff --git a/cmd/hi/doctor.go b/cmd/hi/doctor.go index e1b86099..a45bfa8f 100644 --- a/cmd/hi/doctor.go +++ b/cmd/hi/doctor.go @@ -7,8 +7,6 @@ import ( "log" "os/exec" "strings" - - "github.com/docker/docker/client" ) var ErrSystemChecksFailed = errors.New("system checks failed") @@ -88,7 +86,7 @@ func checkDockerBinary() DoctorResult { // checkDockerDaemon verifies Docker daemon is running and accessible. func checkDockerDaemon(ctx context.Context) DoctorResult { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + cli, err := createDockerClient() if err != nil { return DoctorResult{ Name: "Docker Daemon", diff --git a/cmd/hi/tar_utils.go b/cmd/hi/tar_utils.go new file mode 100644 index 00000000..16fb8793 --- /dev/null +++ b/cmd/hi/tar_utils.go @@ -0,0 +1,101 @@ +package main + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +var ( + // ErrFileNotFoundInTar indicates a file was not found in the tar archive. + ErrFileNotFoundInTar = errors.New("file not found in tar") +) + +// extractFileFromTar extracts a single file from a tar reader. +func extractFileFromTar(tarReader io.Reader, fileName, outputPath string) error { + tr := tar.NewReader(tarReader) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + // Check if this is the file we're looking for + if filepath.Base(header.Name) == fileName { + if header.Typeflag == tar.TypeReg { + // Create the output file + outFile, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + + // Copy file contents + if _, err := io.Copy(outFile, tr); err != nil { + return fmt.Errorf("failed to copy file contents: %w", err) + } + return nil + } + } + } + + return fmt.Errorf("%w: %s", ErrFileNotFoundInTar, fileName) +} + +// extractDirectoryFromTar extracts all files from a tar reader to a target directory. +func extractDirectoryFromTar(tarReader io.Reader, targetDir string) error { + tr := tar.NewReader(tarReader) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + // Clean the path to prevent directory traversal + cleanName := filepath.Clean(header.Name) + if strings.Contains(cleanName, "..") { + continue // Skip potentially dangerous paths + } + + targetPath := filepath.Join(targetDir, filepath.Base(cleanName)) + + switch header.Typeflag { + case tar.TypeDir: + // Create directory + if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetPath, err) + } + case tar.TypeReg: + // Create file + outFile, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", targetPath, err) + } + + if _, err := io.Copy(outFile, tr); err != nil { + outFile.Close() + return fmt.Errorf("failed to copy file contents: %w", err) + } + outFile.Close() + + // Set file permissions + if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to set file permissions: %w", err) + } + } + } + + return nil +} \ No newline at end of file diff --git a/flake.nix b/flake.nix index 17d52308..d3a14b6b 100644 --- a/flake.nix +++ b/flake.nix @@ -19,6 +19,7 @@ overlay = _: prev: let pkgs = nixpkgs.legacyPackages.${prev.system}; buildGo = pkgs.buildGo124Module; + vendorHash = "sha256-9e+ngBkzRb3anSYtFHTJDxt/VMzrHdb5NWwOesJz+kY="; in { headscale = buildGo { pname = "headscale"; @@ -30,7 +31,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-8nRaQNwUDbHkp3q54R6eLDh1GkfwBlh4b9w0IkNj2sY="; + inherit vendorHash; subPackages = ["cmd/headscale"]; @@ -42,6 +43,17 @@ ]; }; + hi = buildGo { + pname = "hi"; + version = headscaleVersion; + src = pkgs.lib.cleanSource self; + + checkFlags = ["-short"]; + inherit vendorHash; + + subPackages = ["cmd/hi"]; + }; + protoc-gen-grpc-gateway = buildGo rec { pname = "grpc-gateway"; version = "2.24.0"; @@ -144,6 +156,9 @@ buf clang-tools # clang-format protobuf-language-server + + # Add hi to make it even easier to use ci runner. + hi ]; # Add entry to build a docker image with headscale diff --git a/go.mod b/go.mod index 13867746..d2fba386 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,14 @@ toolchain go1.24.2 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/arl/statsviz v0.6.0 - github.com/cenkalti/backoff/v4 v4.3.0 + github.com/cenkalti/backoff/v5 v5.0.2 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/docker/docker v28.2.2+incompatible github.com/fsnotify/fsnotify v1.9.0 github.com/glebarez/sqlite v1.11.0 github.com/go-gormigrate/gormigrate/v2 v2.1.4 @@ -22,7 +22,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 github.com/jagottsicher/termcolor v1.0.2 github.com/klauspost/compress v1.18.0 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 @@ -30,11 +30,11 @@ require ( github.com/philip-bui/grpc-zerolog v1.0.1 github.com/pkg/profile v1.7.0 github.com/prometheus/client_golang v1.22.0 - github.com/prometheus/common v0.63.0 - github.com/pterm/pterm v0.12.80 - github.com/puzpuzpuz/xsync/v3 v3.5.1 + github.com/prometheus/common v0.65.0 + github.com/pterm/pterm v0.12.81 + github.com/puzpuzpuz/xsync/v4 v4.1.0 github.com/rs/zerolog v1.34.0 - github.com/samber/lo v1.50.0 + github.com/samber/lo v1.51.0 github.com/sasha-s/go-deadlock v0.3.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 @@ -43,20 +43,20 @@ 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.38.0 + golang.org/x/crypto v0.39.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/net v0.40.0 - golang.org/x/oauth2 v0.29.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 + golang.org/x/net v0.41.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.15.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 + google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/postgres v1.5.11 - gorm.io/gorm v1.25.12 - tailscale.com v1.83.0-pre.0.20250331211809-96fe8a6db6c9 - zgo.at/zcache/v2 v2.1.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.30.0 + tailscale.com v1.84.2 + zgo.at/zcache/v2 v2.2.0 zombiezen.com/go/postgrestest v1.0.1 ) @@ -110,9 +110,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect github.com/aws/smithy-go v1.22.2 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/console v1.0.4 // indirect + github.com/containerd/console v1.0.5 // indirect github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/creachadair/mds v0.24.1 // indirect github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect @@ -154,7 +157,6 @@ require ( github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify/v3 v3.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect @@ -164,7 +166,6 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jsimonetti/rtnetlink v1.4.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -191,11 +192,10 @@ require ( github.com/opencontainers/runc v1.3.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -216,8 +216,7 @@ require ( github.com/tailscale/setec v0.0.0-20250305161714-445cadbbca3d // indirect github.com/tailscale/squibble v0.0.0-20250108170732-a4ca58afa694 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect - github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 // indirect - github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect + github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -233,14 +232,14 @@ require ( 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/mod v0.25.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/text v0.26.0 // indirect golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/tools v0.33.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-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect ) diff --git a/go.sum b/go.sum index cce71c15..3e456d8d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= @@ -85,7 +87,6 @@ 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= @@ -111,10 +112,14 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/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/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 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= @@ -129,6 +134,8 @@ github.com/creachadair/flax v0.0.5 h1:zt+CRuXQASxwQ68e9GHAOnEgAU29nF0zYMHOCrL5wz 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/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= +github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= @@ -147,8 +154,8 @@ 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= github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -202,6 +209,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -234,6 +243,8 @@ github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4r github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= +github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= @@ -260,8 +271,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -414,10 +425,10 @@ github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykU github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= @@ -427,10 +438,10 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= -github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= -github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= -github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= +github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= +github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U= +github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -447,8 +458,8 @@ github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= -github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= -github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -489,8 +500,8 @@ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33 h1:idh63uw+gsG05HwjZsAENCG4KZfyvjK03bpjxa5qRRk= @@ -509,8 +520,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw= -github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U= +github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= @@ -519,8 +530,8 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok= github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI= -github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= -github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -555,8 +566,8 @@ go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCRE 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/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 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= @@ -576,8 +587,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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 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= @@ -593,8 +604,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -607,11 +618,11 @@ 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 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= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -620,8 +631,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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.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= @@ -669,8 +680,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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 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= @@ -684,8 +695,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -699,17 +710,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-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/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/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.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 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= @@ -726,10 +737,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.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= @@ -766,9 +777,9 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.83.0-pre.0.20250331211809-96fe8a6db6c9 h1:mPTb8dGYSqzJhrrYNrLVP717Nh8DME85DWnhBATB/94= -tailscale.com v1.83.0-pre.0.20250331211809-96fe8a6db6c9/go.mod h1:iU6kohVzG+bP0/5XjqBAnW8/6nSG/Du++bO+x7VJZD0= -zgo.at/zcache/v2 v2.1.0 h1:USo+ubK+R4vtjw4viGzTe/zjXyPw6R7SK/RL3epBBxs= -zgo.at/zcache/v2 v2.1.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk= +tailscale.com v1.84.2 h1:v6aM4RWUgYiV52LRAx6ET+dlGnvO/5lnqPXb7/pMnR0= +tailscale.com v1.84.2/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= +zgo.at/zcache/v2 v2.2.0 h1:K29/IPjMniZfveYE+IRXfrl11tMzHkIPuyGrfVZ2fGo= +zgo.at/zcache/v2 v2.2.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk= zombiezen.com/go/postgrestest v1.0.1 h1:aXoADQAJmZDU3+xilYVut0pHhgc0sF8ZspPW9gFNwP4= zombiezen.com/go/postgrestest v1.0.1/go.mod h1:marlZezr+k2oSJrvXHnZUs1olHqpE9czlz8ZYkVxliQ= diff --git a/hscontrol/dns/extrarecords.go b/hscontrol/dns/extrarecords.go index e667c562..6ea3aa35 100644 --- a/hscontrol/dns/extrarecords.go +++ b/hscontrol/dns/extrarecords.go @@ -1,13 +1,14 @@ package dns import ( + "context" "crypto/sha256" "encoding/json" "fmt" "os" "sync" - "github.com/cenkalti/backoff/v4" + "github.com/cenkalti/backoff/v5" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" "tailscale.com/tailcfg" @@ -95,13 +96,13 @@ func (e *ExtraRecordsMan) Run() { // If a file is removed or renamed, fsnotify will loose track of it // and not watch it. We will therefore attempt to re-add it with a backoff. case fsnotify.Remove, fsnotify.Rename: - err := backoff.Retry(func() error { + _, err := backoff.Retry(context.Background(), func() (struct{}, error) { if _, err := os.Stat(e.path); err != nil { - return err + return struct{}{}, err } - return nil - }, backoff.NewExponentialBackOff()) + return struct{}{}, nil + }, backoff.WithBackOff(backoff.NewExponentialBackOff())) if err != nil { log.Error().Caller().Err(err).Msgf("extra records filewatcher retrying to find file after delete") diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 8b516c3e..7d31e2bb 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/puzpuzpuz/xsync/v3" + "github.com/puzpuzpuz/xsync/v4" "github.com/rs/zerolog/log" "github.com/samber/lo" "google.golang.org/grpc/codes" diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 8d66f182..2e6b9b0b 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -9,7 +9,7 @@ import ( "time" "github.com/juanfont/headscale/hscontrol/types" - "github.com/puzpuzpuz/xsync/v3" + "github.com/puzpuzpuz/xsync/v4" "github.com/rs/zerolog/log" "github.com/sasha-s/go-deadlock" "tailscale.com/envknob" diff --git a/integration/dockertestutil/config.go b/integration/dockertestutil/config.go index 8fae0ec1..f8bbde5f 100644 --- a/integration/dockertestutil/config.go +++ b/integration/dockertestutil/config.go @@ -1,44 +1,65 @@ package dockertestutil import ( + "fmt" "os" + "strings" + "time" - "github.com/ory/dockertest/v3/docker" + "github.com/juanfont/headscale/hscontrol/util" + "github.com/ory/dockertest/v3" ) +// GetIntegrationRunID returns the run ID for the current integration test session. +// This is set by the hi tool and passed through environment variables. +func GetIntegrationRunID() string { + return os.Getenv("HEADSCALE_INTEGRATION_RUN_ID") +} + +// DockerAddIntegrationLabels adds integration test labels to Docker RunOptions. +// This allows the hi tool to identify containers belonging to specific test runs. +// This function should be called before passing RunOptions to dockertest functions. +func DockerAddIntegrationLabels(opts *dockertest.RunOptions, testType string) { + runID := GetIntegrationRunID() + if runID == "" { + panic("HEADSCALE_INTEGRATION_RUN_ID environment variable is required") + } + + if opts.Labels == nil { + opts.Labels = make(map[string]string) + } + opts.Labels["hi.run-id"] = runID + opts.Labels["hi.test-type"] = testType +} + +// GenerateRunID creates a unique run identifier with timestamp and random hash. +// Format: YYYYMMDD-HHMMSS-HASH (e.g., 20250619-143052-a1b2c3) +func GenerateRunID() string { + now := time.Now() + timestamp := now.Format("20060102-150405") + + // Add a short random hash to ensure uniqueness + randomHash := util.MustGenerateRandomStringDNSSafe(6) + return fmt.Sprintf("%s-%s", timestamp, randomHash) +} + +// ExtractRunIDFromContainerName extracts the run ID from container name. +// Expects format: "prefix-YYYYMMDD-HHMMSS-HASH" +func ExtractRunIDFromContainerName(containerName string) string { + parts := strings.Split(containerName, "-") + if len(parts) >= 3 { + // Return the last three parts as the run ID (YYYYMMDD-HHMMSS-HASH) + return strings.Join(parts[len(parts)-3:], "-") + } + + panic(fmt.Sprintf("unexpected container name format: %s", containerName)) +} + +// IsRunningInContainer checks if the current process is running inside a Docker container. +// This is used by tests to determine if they should run integration tests. func IsRunningInContainer() bool { - if _, err := os.Stat("/.dockerenv"); err != nil { - return false - } - - return true -} - -func DockerRestartPolicy(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself on error *immediately*. - // when set to false, containers remain until the end of the integration test. - config.AutoRemove = false - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } -} - -func DockerAllowLocalIPv6(config *docker.HostConfig) { - if config.Sysctls == nil { - config.Sysctls = make(map[string]string, 1) - } - config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0" -} - -func DockerAllowNetworkAdministration(config *docker.HostConfig) { - // Needed since containerd (1.7.24) - // https://github.com/tailscale/tailscale/issues/14256 - // https://github.com/opencontainers/runc/commit/2ce40b6ad72b4bd4391380cafc5ef1bad1fa0b31 - config.CapAdd = append(config.CapAdd, "NET_ADMIN") - config.CapAdd = append(config.CapAdd, "NET_RAW") - config.Devices = append(config.Devices, docker.Device{ - PathOnHost: "/dev/net/tun", - PathInContainer: "/dev/net/tun", - CgroupPermissions: "rwm", - }) -} + // Check for the common indicator that we're in a container + // This could be improved with more robust detection if needed + _, err := os.Stat("/.dockerenv") + return err == nil +} \ No newline at end of file diff --git a/integration/dockertestutil/network.go b/integration/dockertestutil/network.go index 83fc08c4..86c1e046 100644 --- a/integration/dockertestutil/network.go +++ b/integration/dockertestutil/network.go @@ -126,3 +126,24 @@ func CleanImagesInCI(pool *dockertest.Pool) error { return nil } + +// DockerRestartPolicy sets the restart policy for containers. +func DockerRestartPolicy(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{ + Name: "unless-stopped", + } +} + +// DockerAllowLocalIPv6 allows IPv6 traffic within the container. +func DockerAllowLocalIPv6(config *docker.HostConfig) { + config.NetworkMode = "default" + config.Sysctls = map[string]string{ + "net.ipv6.conf.all.disable_ipv6": "0", + } +} + +// DockerAllowNetworkAdministration gives the container network administration capabilities. +func DockerAllowNetworkAdministration(config *docker.HostConfig) { + config.CapAdd = append(config.CapAdd, "NET_ADMIN") + config.Privileged = true +} diff --git a/integration/dsic/dsic.go b/integration/dsic/dsic.go index 9c5a3320..857a5def 100644 --- a/integration/dsic/dsic.go +++ b/integration/dsic/dsic.go @@ -159,6 +159,7 @@ func New( }, } + if dsic.workdir != "" { runOptions.WorkingDir = dsic.workdir } @@ -189,6 +190,9 @@ func New( Value: "v" + version, }) } + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(runOptions, "derp") + container, err = pool.BuildAndRunWithBuildOptions( buildOptions, runOptions, diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 35550c65..9c6816fa 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -1,6 +1,8 @@ package hsic import ( + "archive/tar" + "bytes" "cmp" "crypto/tls" "encoding/json" @@ -12,6 +14,7 @@ import ( "net/netip" "os" "path" + "path/filepath" "sort" "strconv" "strings" @@ -311,18 +314,22 @@ func New( hsic.env["HEADSCALE_DATABASE_POSTGRES_NAME"] = "headscale" delete(hsic.env, "HEADSCALE_DATABASE_SQLITE_PATH") - pg, err := pool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("postgres-%s", hash), - Repository: "postgres", - Tag: "latest", - Networks: networks, - Env: []string{ - "POSTGRES_USER=headscale", - "POSTGRES_PASSWORD=headscale", - "POSTGRES_DB=headscale", - }, - }) + pgRunOptions := &dockertest.RunOptions{ + Name: fmt.Sprintf("postgres-%s", hash), + Repository: "postgres", + Tag: "latest", + Networks: networks, + Env: []string{ + "POSTGRES_USER=headscale", + "POSTGRES_PASSWORD=headscale", + "POSTGRES_DB=headscale", + }, + } + + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(pgRunOptions, "postgres") + + pg, err := pool.RunWithOptions(pgRunOptions) if err != nil { return nil, fmt.Errorf("starting postgres container: %w", err) } @@ -366,6 +373,7 @@ func New( Env: env, } + if len(hsic.hostPortBindings) > 0 { runOptions.PortBindings = map[docker.Port][]docker.PortBinding{} for port, hostPorts := range hsic.hostPortBindings { @@ -386,6 +394,9 @@ func New( return nil, err } + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(runOptions, "headscale") + container, err := pool.BuildAndRunWithBuildOptions( headscaleBuildOptions, runOptions, @@ -553,22 +564,67 @@ func (t *HeadscaleInContainer) SaveMetrics(savePath string) error { return nil } +// extractTarToDirectory extracts a tar archive to a directory. +func extractTarToDirectory(tarData []byte, targetDir string) error { + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetDir, err) + } + + tarReader := tar.NewReader(bytes.NewReader(tarData)) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + // Clean the path to prevent directory traversal + cleanName := filepath.Clean(header.Name) + if strings.Contains(cleanName, "..") { + continue // Skip potentially dangerous paths + } + + targetPath := filepath.Join(targetDir, filepath.Base(cleanName)) + + switch header.Typeflag { + case tar.TypeDir: + // Create directory + if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetPath, err) + } + case tar.TypeReg: + // Create file + outFile, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", targetPath, err) + } + + if _, err := io.Copy(outFile, tarReader); err != nil { + outFile.Close() + return fmt.Errorf("failed to copy file contents: %w", err) + } + outFile.Close() + + // Set file permissions + if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to set file permissions: %w", err) + } + } + } + + return nil +} + func (t *HeadscaleInContainer) SaveProfile(savePath string) error { tarFile, err := t.FetchPath("/tmp/profile") if err != nil { return err } - err = os.WriteFile( - path.Join(savePath, t.hostname+".pprof.tar"), - tarFile, - os.ModePerm, - ) - if err != nil { - return err - } - - return nil + targetDir := path.Join(savePath, t.hostname+"-pprof") + return extractTarToDirectory(tarFile, targetDir) } func (t *HeadscaleInContainer) SaveMapResponses(savePath string) error { @@ -577,34 +633,101 @@ func (t *HeadscaleInContainer) SaveMapResponses(savePath string) error { return err } - err = os.WriteFile( - path.Join(savePath, t.hostname+".maps.tar"), - tarFile, - os.ModePerm, - ) - if err != nil { - return err - } - - return nil + targetDir := path.Join(savePath, t.hostname+"-mapresponses") + return extractTarToDirectory(tarFile, targetDir) } func (t *HeadscaleInContainer) SaveDatabase(savePath string) error { + // If using PostgreSQL, skip database file extraction + if t.postgres { + return nil + } + + // First, let's see what files are actually in /tmp + tmpListing, err := t.Execute([]string{"ls", "-la", "/tmp/"}) + if err != nil { + log.Printf("Warning: could not list /tmp directory: %v", err) + } else { + log.Printf("Contents of /tmp in container %s:\n%s", t.hostname, tmpListing) + } + + // Also check for any .sqlite files + sqliteFiles, err := t.Execute([]string{"find", "/tmp", "-name", "*.sqlite*", "-type", "f"}) + if err != nil { + log.Printf("Warning: could not find sqlite files: %v", err) + } else { + log.Printf("SQLite files found in %s:\n%s", t.hostname, sqliteFiles) + } + + // Check if the database file exists and has a schema + dbPath := "/tmp/integration_test_db.sqlite3" + fileInfo, err := t.Execute([]string{"ls", "-la", dbPath}) + if err != nil { + return fmt.Errorf("database file does not exist at %s: %w", dbPath, err) + } + log.Printf("Database file info: %s", fileInfo) + + // Check if the database has any tables (schema) + schemaCheck, err := t.Execute([]string{"sqlite3", dbPath, ".schema"}) + if err != nil { + return fmt.Errorf("failed to check database schema (sqlite3 command failed): %w", err) + } + + if strings.TrimSpace(schemaCheck) == "" { + return fmt.Errorf("database file exists but has no schema (empty database)") + } + + // Show a preview of the schema (first 500 chars) + schemaPreview := schemaCheck + if len(schemaPreview) > 500 { + schemaPreview = schemaPreview[:500] + "..." + } + log.Printf("Database schema preview:\n%s", schemaPreview) + tarFile, err := t.FetchPath("/tmp/integration_test_db.sqlite3") if err != nil { - return err + return fmt.Errorf("failed to fetch database file: %w", err) } - err = os.WriteFile( - path.Join(savePath, t.hostname+".db.tar"), - tarFile, - os.ModePerm, - ) - if err != nil { - return err + // For database, extract the first regular file (should be the SQLite file) + tarReader := tar.NewReader(bytes.NewReader(tarFile)) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + log.Printf("Found file in tar: %s (type: %d, size: %d)", header.Name, header.Typeflag, header.Size) + + // Extract the first regular file we find + if header.Typeflag == tar.TypeReg { + dbPath := path.Join(savePath, t.hostname+".db") + outFile, err := os.Create(dbPath) + if err != nil { + return fmt.Errorf("failed to create database file: %w", err) + } + + written, err := io.Copy(outFile, tarReader) + outFile.Close() + if err != nil { + return fmt.Errorf("failed to copy database file: %w", err) + } + + log.Printf("Extracted database file: %s (%d bytes written, header claimed %d bytes)", dbPath, written, header.Size) + + // Check if we actually wrote something + if written == 0 { + return fmt.Errorf("database file is empty (size: %d, header size: %d)", written, header.Size) + } + + return nil + } } - return nil + return fmt.Errorf("no regular file found in database tar archive") } // Execute runs a command inside the Headscale container and returns the diff --git a/integration/scenario.go b/integration/scenario.go index 0af1956b..358291ff 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -32,7 +32,7 @@ import ( "github.com/oauth2-proxy/mockoidc" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" - "github.com/puzpuzpuz/xsync/v3" + "github.com/puzpuzpuz/xsync/v4" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1102,6 +1102,7 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse }, } + headscaleBuildOptions := &dockertest.BuildOptions{ Dockerfile: hsic.IntegrationTestDockerFileName, ContextDir: dockerContextPath, @@ -1114,6 +1115,9 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse s.mockOIDC = scenarioOIDC{} + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(mockOidcOptions, "oidc") + if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions( headscaleBuildOptions, mockOidcOptions, @@ -1198,6 +1202,9 @@ func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) { Env: []string{}, } + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(webOpts, "web") + webBOpts := &dockertest.BuildOptions{ Dockerfile: hsic.IntegrationTestDockerFileName, ContextDir: dockerContextPath, diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 0bbd8711..cf08613d 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -17,7 +17,9 @@ import ( func isSSHNoAccessStdError(stderr string) bool { return strings.Contains(stderr, "Permission denied (tailscale)") || // Since https://github.com/tailscale/tailscale/pull/14853 - strings.Contains(stderr, "failed to evaluate SSH policy") + strings.Contains(stderr, "failed to evaluate SSH policy") || + // Since https://github.com/tailscale/tailscale/pull/16127 + strings.Contains(stderr, "tailnet policy does not permit you to SSH to this node") } var retry = func(times int, sleepInterval time.Duration, diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 28de2527..d2738c55 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -251,6 +251,7 @@ func New( Env: []string{}, } + if tsic.withWebsocketDERP { if version != VersionHead { return tsic, errInvalidClientConfig @@ -279,6 +280,9 @@ func New( return nil, err } + // Add integration test labels if running under hi tool + dockertestutil.DockerAddIntegrationLabels(tailscaleOptions, "tailscale") + var container *dockertest.Resource if version != VersionHead { diff --git a/integration/utils.go b/integration/utils.go index 18721cad..bcf488e2 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -3,6 +3,7 @@ package integration import ( "bufio" "bytes" + "context" "fmt" "io" "net/netip" @@ -11,7 +12,7 @@ import ( "testing" "time" - "github.com/cenkalti/backoff/v4" + "github.com/cenkalti/backoff/v5" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/tsic" @@ -310,20 +311,18 @@ func assertValidNetcheck(t *testing.T, client TailscaleClient) { func assertCommandOutputContains(t *testing.T, c TailscaleClient, command []string, contains string) { t.Helper() - err := backoff.Retry(func() error { + _, err := backoff.Retry(context.Background(), func() (struct{}, error) { stdout, stderr, err := c.Execute(command) if err != nil { - return fmt.Errorf("executing command, stdout: %q stderr: %q, err: %w", stdout, stderr, err) + return struct{}{}, fmt.Errorf("executing command, stdout: %q stderr: %q, err: %w", stdout, stderr, err) } if !strings.Contains(stdout, contains) { - return fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout) + return struct{}{}, fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout) } - return nil - }, backoff.NewExponentialBackOff( - backoff.WithMaxElapsedTime(10*time.Second)), - ) + return struct{}{}, nil + }, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxElapsedTime(10*time.Second)) assert.NoError(t, err) }