mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-17 16:36:02 +00:00
Compare commits
235 commits
sshtests-w
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
048308511c | ||
|
|
a84945f134 | ||
|
|
fef80f3eb0 | ||
|
|
01ef350d5f | ||
|
|
f4fba32dc6 | ||
|
|
fc6f216b61 | ||
|
|
de9db9c811 | ||
|
|
4946d1c88d | ||
|
|
c497612c99 | ||
|
|
4e4512c4b7 | ||
|
|
08956d51a4 | ||
|
|
d528686f14 | ||
|
|
6f317c7576 | ||
|
|
518b497be9 | ||
|
|
8d91e42a9f | ||
|
|
99fd62477d | ||
|
|
c7b162509c | ||
|
|
a4ec76605e | ||
|
|
1867213b69 | ||
|
|
622e08f5e6 | ||
|
|
79bf201ccb | ||
|
|
af46fe8867 | ||
|
|
b05f8875c3 | ||
|
|
84715e976c | ||
|
|
ee95cf58d9 | ||
|
|
7b1776a87e | ||
|
|
f6865fb40c | ||
|
|
6e0814a845 | ||
|
|
6e6ea3758d | ||
|
|
3846c3f148 | ||
|
|
fe536dd799 | ||
|
|
5aeeff98d0 | ||
|
|
66937040f2 | ||
|
|
036760df02 | ||
|
|
83d3828ba1 | ||
|
|
dd9f0a3f4f | ||
|
|
961b15313f | ||
|
|
475a5ead0c | ||
|
|
ec0cb6f63a | ||
|
|
a066a06bef | ||
|
|
33a65052c9 | ||
|
|
13ffd958ed | ||
|
|
d5555c6b8c | ||
|
|
8898c69d0a | ||
|
|
e2e9fe3081 | ||
|
|
3cddc07026 | ||
|
|
b3472bb8dd | ||
|
|
6b413fe237 | ||
|
|
ff7dd807fe | ||
|
|
fe08d43978 | ||
|
|
2ff342764f | ||
|
|
3d811f29a6 | ||
|
|
4fa6af0102 | ||
|
|
96d736ec23 | ||
|
|
339cb392a9 | ||
|
|
24dfcf178f | ||
|
|
8f314797ce | ||
|
|
2632006f5f | ||
|
|
07b3a8ebd3 | ||
|
|
3c504af2be | ||
|
|
7af39a6798 | ||
|
|
49d6949639 | ||
|
|
d7150755d4 | ||
|
|
88234d4046 | ||
|
|
15781ed6d8 | ||
|
|
72adc5ff2a | ||
|
|
560b6d81ad | ||
|
|
8efa5ad1fe | ||
|
|
ba90048cfb | ||
|
|
1572ac551d | ||
|
|
c9bbb5bdec | ||
|
|
97eff90ebe | ||
|
|
a00de89c85 | ||
|
|
f5198841bd | ||
|
|
dfc25f06e1 | ||
|
|
74928c0241 | ||
|
|
9aba06ec7d | ||
|
|
00afce77b1 | ||
|
|
9f1886f587 | ||
|
|
ea5165e325 | ||
|
|
368b9e7edd | ||
|
|
abfd5c9236 | ||
|
|
e4cd846075 | ||
|
|
4b693a1320 | ||
|
|
4f4e95fc80 | ||
|
|
27468f944b | ||
|
|
ac725f27e4 | ||
|
|
60f0544b78 | ||
|
|
ea5e52f662 | ||
|
|
722a2f1e18 | ||
|
|
75b09cb991 | ||
|
|
0320b56911 | ||
|
|
62869f01d2 | ||
|
|
19d5d9deeb | ||
|
|
c3cfbf1cc0 | ||
|
|
fdc7e26c8a | ||
|
|
45e6e90d11 | ||
|
|
468f70a9e5 | ||
|
|
8c10a96921 | ||
|
|
3979887d8d | ||
|
|
03e2d24c79 | ||
|
|
e6ef1dda4d | ||
|
|
145672f0b6 | ||
|
|
77f289a627 | ||
|
|
55b9e3cfda | ||
|
|
b56326427c | ||
|
|
0b7c8aa270 | ||
|
|
b88a3cb7b2 | ||
|
|
ed6596f9d7 | ||
|
|
f49f27adf8 | ||
|
|
43ba20bebb | ||
|
|
c8bd4a0947 | ||
|
|
84168fb90a | ||
|
|
b0c221f3a1 | ||
|
|
68a6d3cf17 | ||
|
|
1689478485 | ||
|
|
a1d3e98255 | ||
|
|
b83bf3f993 | ||
|
|
96d2e6ed60 | ||
|
|
9b8949727d | ||
|
|
bff216a184 | ||
|
|
fd08b8fa8c | ||
|
|
a73d38bb3f | ||
|
|
e759d9fc90 | ||
|
|
0961e79e16 | ||
|
|
a5ef3aff15 | ||
|
|
4da06925d0 | ||
|
|
21058d1142 | ||
|
|
c5f3d5c28d | ||
|
|
40ed210521 | ||
|
|
f4eeb94b1c | ||
|
|
7918187e7a | ||
|
|
f497b4efd7 | ||
|
|
759381ad78 | ||
|
|
71a4ce3c9f | ||
|
|
f585f8a94d | ||
|
|
88044f43ff | ||
|
|
5e05652a78 | ||
|
|
020560fc5f | ||
|
|
fad8f2a729 | ||
|
|
0121083b53 | ||
|
|
9fc88e308f | ||
|
|
c483bebba8 | ||
|
|
4914f9f2fd | ||
|
|
8237ac662a | ||
|
|
cd1c208980 | ||
|
|
0fdff0c79b | ||
|
|
eb57a3a62b | ||
|
|
56cd3eb24d | ||
|
|
8f75ee5647 | ||
|
|
efdd9463e9 | ||
|
|
a518a5076a | ||
|
|
2c9164b1c4 | ||
|
|
0e7b154617 | ||
|
|
ad2693ff13 | ||
|
|
7d845ef65e | ||
|
|
9f0c74e73a | ||
|
|
e413919810 | ||
|
|
0921972f96 | ||
|
|
99ad555d64 | ||
|
|
10696fa634 | ||
|
|
84c99023e5 | ||
|
|
ba54349176 | ||
|
|
08f186f22a | ||
|
|
017162dac1 | ||
|
|
bb06b90543 | ||
|
|
5a70a72988 | ||
|
|
ec94573258 | ||
|
|
4c165ae5e7 | ||
|
|
06d6816dc9 | ||
|
|
f61753e737 | ||
|
|
4f67300005 | ||
|
|
2e2401833b | ||
|
|
29f87e5eaa | ||
|
|
f61d21b4a7 | ||
|
|
b892b8f254 | ||
|
|
6777a82ee6 | ||
|
|
5228cb1a40 | ||
|
|
cffdb77c8b | ||
|
|
7706552c99 | ||
|
|
bceac495f9 | ||
|
|
171fd7a3c5 | ||
|
|
ea8fc72570 | ||
|
|
77ba225cdb | ||
|
|
4483fd0cad | ||
|
|
66a5f99bfa | ||
|
|
2e49f3dc67 | ||
|
|
79562b9782 | ||
|
|
58a85b68b3 | ||
|
|
eb23c125fa | ||
|
|
570735f204 | ||
|
|
78570c754f | ||
|
|
25adfaf341 | ||
|
|
be90910d33 | ||
|
|
575d8ecbfd | ||
|
|
e4e742c776 | ||
|
|
4cca63155d | ||
|
|
17236fd284 | ||
|
|
3e2aa5814e | ||
|
|
f905d58292 | ||
|
|
e00c899219 | ||
|
|
64c398f2c2 | ||
|
|
7f02210863 | ||
|
|
e07b39108f | ||
|
|
e285f3c932 | ||
|
|
355733342f | ||
|
|
f3f84a5a63 | ||
|
|
4eb5899154 | ||
|
|
e2f2f9211f | ||
|
|
c7630b505b | ||
|
|
de6be71a86 | ||
|
|
fb8eecae25 | ||
|
|
a345a22a3b | ||
|
|
dfcc96d808 | ||
|
|
78fd6efb38 | ||
|
|
eec3844f24 | ||
|
|
98e9ff4d36 | ||
|
|
4d3b567149 | ||
|
|
963daf8908 | ||
|
|
c6c29c05e5 | ||
|
|
1f48ebb376 | ||
|
|
b5b786f519 | ||
|
|
2cb914df59 | ||
|
|
e5fcd01ee6 | ||
|
|
af7e7a4560 | ||
|
|
b1196baf6d | ||
|
|
7a20db9f49 | ||
|
|
e78a24b892 | ||
|
|
574a61852a | ||
|
|
92a9accfcb | ||
|
|
26eebcea5a | ||
|
|
013dea4f40 | ||
|
|
6a0a297c7f | ||
|
|
d600090f2c | ||
|
|
4ad200ab73 |
582 changed files with 2180508 additions and 24585 deletions
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
|
|
@ -10,6 +10,10 @@ concurrency:
|
|||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
build-nix:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -29,20 +33,16 @@ jobs:
|
|||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
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 }}
|
||||
|
||||
- name: Check vendor hash
|
||||
id: vendorhash
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
nix develop --command -- go run ./cmd/vendorhash check | tee check-result
|
||||
go run ./cmd/vendorhash check | tee check-result
|
||||
{
|
||||
grep '^expected_sri=' check-result || true
|
||||
grep '^actual_sri=' check-result || true
|
||||
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
|
||||
- name: Run nix build
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix build
|
||||
run: nix build --fallback
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
|
|
@ -81,17 +81,13 @@ jobs:
|
|||
- "GOARCH=amd64 GOOS=darwin"
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Run go cross compile
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: env ${{ matrix.env }} nix develop --command -- go build -o "headscale"
|
||||
run: env ${{ matrix.env }} go build -o "headscale"
|
||||
./cmd/headscale
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
|
|
|
|||
15
.github/workflows/check-generated.yml
vendored
15
.github/workflows/check-generated.yml
vendored
|
|
@ -12,6 +12,10 @@ concurrency:
|
|||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
check-generated:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -28,20 +32,15 @@ jobs:
|
|||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- '**/*.proto'
|
||||
- 'buf.gen.yaml'
|
||||
- 'tools/**'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
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 }}
|
||||
|
||||
- name: Run make generate
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix develop --command -- make generate
|
||||
run: make generate
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
|
|
|
|||
14
.github/workflows/check-tests.yaml
vendored
14
.github/workflows/check-tests.yaml
vendored
|
|
@ -6,6 +6,10 @@ concurrency:
|
|||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
check-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -24,19 +28,15 @@ jobs:
|
|||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
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 }}
|
||||
|
||||
- name: Generate and check integration tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
nix develop --command bash -c "cd .github/workflows && go generate"
|
||||
(cd .github/workflows && go generate)
|
||||
git diff --exit-code .github/workflows/test-integration.yaml
|
||||
|
||||
- name: Show missing tests
|
||||
|
|
|
|||
26
.github/workflows/container-main.yml
vendored
26
.github/workflows/container-main.yml
vendored
|
|
@ -16,6 +16,10 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
container:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
|
|
@ -40,12 +44,8 @@ jobs:
|
|||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Set commit timestamp
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> $GITHUB_ENV
|
||||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
|
||||
CGO_ENABLED: "0"
|
||||
run: |
|
||||
nix develop --command -- ko build \
|
||||
ko build \
|
||||
--bare \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--tags=main-${GITHUB_SHA::7},development \
|
||||
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
|
||||
CGO_ENABLED: "0"
|
||||
run: |
|
||||
nix develop --command -- ko build \
|
||||
ko build \
|
||||
--bare \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--tags=main-${GITHUB_SHA::7},development \
|
||||
|
|
@ -92,19 +92,15 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: nix develop --command -- go build -o headscale ./cmd/headscale
|
||||
run: go build -o headscale ./cmd/headscale
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
|
|
|
|||
39
.github/workflows/gc.yml
vendored
Normal file
39
.github/workflows/gc.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: Cache GC
|
||||
|
||||
# Garbage collection for the hestia binary cache. Must run on the default
|
||||
# branch: a PR job's cache scope is read-only towards the default branch and
|
||||
# dies with the PR, but the default-branch scope grows forever without GC.
|
||||
|
||||
concurrency:
|
||||
group: hestia-gc
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily, off-peak (UTC).
|
||||
- cron: "23 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: Plan only; do not repack, touch, or delete anything.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
gc:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# REST cache deletes need actions:write.
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: Run garbage collection
|
||||
run: '"${HESTIA_BIN}" gc ${{ inputs.dry-run && ''--dry-run'' || '''' }}'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
// Key is the test function name, value is a list of subtest prefixes.
|
||||
// Each prefix becomes a separate CI job as "TestName/prefix".
|
||||
//
|
||||
// Example: TestAutoApproveMultiNetwork has subtests like:
|
||||
// Example: [TestAutoApproveMultiNetwork] has subtests like:
|
||||
// - TestAutoApproveMultiNetwork/authkey-tag-advertiseduringup-false-pol-database
|
||||
// - TestAutoApproveMultiNetwork/webauth-user-advertiseduringup-true-pol-file
|
||||
//
|
||||
|
|
|
|||
46
.github/workflows/integration-test-template.yml
vendored
46
.github/workflows/integration-test-template.yml
vendored
|
|
@ -51,6 +51,11 @@ jobs:
|
|||
with:
|
||||
name: tailscale-head-image
|
||||
path: /tmp/artifacts
|
||||
- name: Download tailscale released images
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: tailscale-released-images
|
||||
path: /tmp/artifacts
|
||||
- name: Download hi binary
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
|
|
@ -67,28 +72,33 @@ jobs:
|
|||
with:
|
||||
name: postgres-image
|
||||
path: /tmp/artifacts
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
run: |
|
||||
# Docker 29 breaks docker build via Go client libraries and
|
||||
# docker load/save with certain tarball formats.
|
||||
# Pin to Docker 28.x until our tooling is updated.
|
||||
# https://github.com/actions/runner-images/issues/13474
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update -qq
|
||||
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
|
||||
sudo apt-get install -y --allow-downgrades \
|
||||
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
|
||||
sudo mkdir -p /etc/docker
|
||||
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
- name: Load br_netfilter for in-cluster service routing
|
||||
if: inputs.test == 'TestK8sOperator'
|
||||
# TestK8sOperator runs k3s in a container; without br_netfilter on the
|
||||
# host, bridged pod-to-pod traffic skips kube-proxy's ClusterIP DNAT and
|
||||
# in-cluster DNS (kube-dns) is unreachable. The module cannot be loaded
|
||||
# from inside the unprivileged-module rancher/k3s image, so load it here.
|
||||
run: sudo modprobe br_netfilter
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
|
||||
if: env.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
- name: Load Docker images, Go cache, and prepare binary
|
||||
run: |
|
||||
gunzip -c /tmp/artifacts/headscale-image.tar.gz | docker load
|
||||
gunzip -c /tmp/artifacts/tailscale-head-image.tar.gz | docker load
|
||||
gunzip -c /tmp/artifacts/tailscale-released-images.tar.gz | docker load
|
||||
if [ -f /tmp/artifacts/postgres-image.tar.gz ]; then
|
||||
gunzip -c /tmp/artifacts/postgres-image.tar.gz | docker load
|
||||
fi
|
||||
|
|
@ -105,6 +115,12 @@ jobs:
|
|||
HEADSCALE_INTEGRATION_POSTGRES_IMAGE: ${{ inputs.postgres_flag == '--postgres=1' && format('postgres:{0}', github.sha) || '' }}
|
||||
HEADSCALE_INTEGRATION_GO_CACHE: /tmp/go-cache/go
|
||||
HEADSCALE_INTEGRATION_GO_BUILD_CACHE: /tmp/go-cache/.cache/go-build
|
||||
# Mirror the docker/login-action secrets into env so the
|
||||
# dockertestutil.Credentials resolver picks them up directly
|
||||
# (otherwise it falls back to parsing ~/.docker/config.json,
|
||||
# which works but is one step further from the source).
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
|
||||
run: /tmp/artifacts/hi run --stats --ts-memory-limit=300 --hs-memory-limit=1500 "^${{ inputs.test }}$" \
|
||||
--timeout=120m \
|
||||
${{ inputs.postgres_flag }}
|
||||
|
|
|
|||
93
.github/workflows/lint.yml
vendored
93
.github/workflows/lint.yml
vendored
|
|
@ -1,93 +0,0 @@
|
|||
name: Lint
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- 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 }}
|
||||
|
||||
- name: golangci-lint
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix develop --command -- golangci-lint run
|
||||
--new-from-rev=${{github.event.pull_request.base.sha}}
|
||||
--output.text.path=stdout
|
||||
--output.text.print-linter-name
|
||||
--output.text.print-issued-lines
|
||||
--output.text.colors
|
||||
|
||||
prettier-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
- '**/*.ts'
|
||||
- '**/*.js'
|
||||
- '**/*.sass'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.html'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- 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 }}
|
||||
|
||||
- name: Prettify code
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix develop --command -- prettier --no-error-on-unmatched-pattern
|
||||
--ignore-unknown --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
|
||||
|
||||
proto-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- 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: Buf lint
|
||||
run: nix develop --command -- buf lint proto
|
||||
56
.github/workflows/nix-checks.yml
vendored
Normal file
56
.github/workflows/nix-checks.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: Nix Flake Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Each job only runs `nix build .#checks.<system>.<name>`; the check logic lives
|
||||
# in flake.nix via the flake-checks library. The fileset-filtered checks hit the
|
||||
# hestia cache when their inputs are unchanged, so no changed-files gating.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: build
|
||||
run: nix build -L .#checks.x86_64-linux.build
|
||||
|
||||
gotest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: gotest
|
||||
run: nix build -L .#checks.x86_64-linux.gotest
|
||||
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: golangci-lint
|
||||
run: nix build -L .#checks.x86_64-linux.golangci-lint
|
||||
|
||||
formatting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: formatting
|
||||
run: nix build -L .#checks.x86_64-linux.formatting
|
||||
10
.github/workflows/nix-module-test.yml
vendored
10
.github/workflows/nix-module-test.yml
vendored
|
|
@ -38,18 +38,14 @@ jobs:
|
|||
- 'cmd/**'
|
||||
- 'hscontrol/**'
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
|
||||
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Run NixOS module tests
|
||||
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
|
||||
run: |
|
||||
echo "Running NixOS module integration test..."
|
||||
nix build .#checks.x86_64-linux.headscale -L
|
||||
nix build .#checks.x86_64-linux.headscale -L --fallback
|
||||
|
|
|
|||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
|
|
@ -7,6 +7,10 @@ on:
|
|||
- "*" # triggers only if push new tag version
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
|
|
@ -17,25 +21,6 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
run: |
|
||||
# Docker 29 breaks docker build via Go client libraries and
|
||||
# docker load/save with certain tarball formats.
|
||||
# Pin to Docker 28.x until our tooling is updated.
|
||||
# https://github.com/actions/runner-images/issues/13474
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update -qq
|
||||
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
|
||||
sudo apt-get install -y --allow-downgrades \
|
||||
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
|
|
@ -49,14 +34,10 @@ jobs:
|
|||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Run goreleaser
|
||||
run: nix develop --command -- goreleaser release --clean
|
||||
run: goreleaser release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
36
.github/workflows/servertest.yml
vendored
Normal file
36
.github/workflows/servertest.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: Server Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# hscontrol/servertest is excluded from the sandboxed gotest flake check: it is
|
||||
# slow (10s+ convergence cases) and timing-sensitive (race/stress/HA property
|
||||
# tests), so it runs here in the devShell with a generous timeout instead.
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
servertest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: go test ./hscontrol/servertest
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
run: go test -timeout=20m ./hscontrol/servertest/...
|
||||
179
.github/workflows/test-integration.yaml
vendored
179
.github/workflows/test-integration.yaml
vendored
|
|
@ -6,9 +6,15 @@ on: [pull_request]
|
|||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
jobs:
|
||||
# build: Builds binaries and Docker images once, uploads as artifacts for reuse.
|
||||
# build-postgres: Pulls postgres image separately to avoid Docker Hub rate limits.
|
||||
# build-tailscale-released: Pre-pulls released Tailscale images from ghcr.io
|
||||
# so fork PRs (no DOCKERHUB_USERNAME secret) don't hit Docker Hub rate
|
||||
# limits at test time.
|
||||
# sqlite: Runs all integration tests with SQLite backend.
|
||||
# postgres: Runs a subset of tests with PostgreSQL to verify database compatibility.
|
||||
build:
|
||||
|
|
@ -33,23 +39,18 @@ jobs:
|
|||
- '.github/workflows/test-integration.yaml'
|
||||
- '.github/workflows/integration-test-template.yml'
|
||||
- 'Dockerfile.*'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
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 }}
|
||||
- name: Build binaries and warm Go cache
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
# Build all Go binaries in one nix shell to maximize cache reuse
|
||||
nix develop --command -- bash -c '
|
||||
go build -o hi ./cmd/hi
|
||||
CGO_ENABLED=0 GOOS=linux go build -o headscale ./cmd/headscale
|
||||
# Build integration test binary to warm the cache with all dependencies
|
||||
go test -c ./integration -o /dev/null 2>/dev/null || true
|
||||
'
|
||||
go build -o hi ./cmd/hi
|
||||
CGO_ENABLED=0 GOOS=linux go build -o headscale ./cmd/headscale
|
||||
# Build integration test binary to warm the cache with all dependencies
|
||||
go test -c ./integration -o /dev/null 2>/dev/null || true
|
||||
- name: Upload hi binary
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
|
|
@ -69,25 +70,26 @@ jobs:
|
|||
name: go-cache
|
||||
path: go-cache.tar.gz
|
||||
retention-days: 10
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
# Docker 29 breaks docker build via Go client libraries and
|
||||
# docker load/save with certain tarball formats.
|
||||
# Pin to Docker 28.x until our tooling is updated.
|
||||
# Docker 29 runner images default to overlayfs, which breaks
|
||||
# docker build via Go SDK libraries and docker save/load
|
||||
# tarball formats. overlay2 is the long-standing default.
|
||||
# https://github.com/actions/runner-images/issues/13474
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update -qq
|
||||
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
|
||||
sudo apt-get install -y --allow-downgrades \
|
||||
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
|
||||
sudo mkdir -p /etc/docker
|
||||
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
|
||||
if: env.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
- name: Build headscale image
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
|
|
@ -123,25 +125,24 @@ jobs:
|
|||
needs: build
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
steps:
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
shell: bash
|
||||
run: |
|
||||
# Docker 29 breaks docker build via Go client libraries and
|
||||
# docker load/save with certain tarball formats.
|
||||
# Pin to Docker 28.x until our tooling is updated.
|
||||
# https://github.com/actions/runner-images/issues/13474
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update -qq
|
||||
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
|
||||
sudo apt-get install -y --allow-downgrades \
|
||||
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
|
||||
sudo mkdir -p /etc/docker
|
||||
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
|
||||
if: env.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
- name: Pull and save postgres image
|
||||
shell: bash
|
||||
run: |
|
||||
docker pull postgres:latest
|
||||
docker tag postgres:latest postgres:${{ github.sha }}
|
||||
|
|
@ -152,9 +153,68 @@ jobs:
|
|||
name: postgres-image
|
||||
path: postgres-image.tar.gz
|
||||
retention-days: 10
|
||||
sqlite:
|
||||
build-tailscale-released:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: build
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: Force overlay2 storage driver
|
||||
run: |
|
||||
sudo mkdir -p /etc/docker
|
||||
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
|
||||
if: env.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
- name: List Tailscale versions to pre-pull
|
||||
id: versions
|
||||
run: |
|
||||
versions=$(go run ./cmd/hi list-versions --set=must --exclude=head)
|
||||
echo "versions=${versions}" >> "$GITHUB_OUTPUT"
|
||||
echo "Pre-pulling: ${versions}"
|
||||
- name: Pull Tailscale images
|
||||
run: |
|
||||
# Releases come from ghcr.io (anonymous, unmetered). The
|
||||
# "unstable" floating tag on ghcr.io has been stale since 2022,
|
||||
# so it still needs to come from Docker Hub. xargs -P 0 fans
|
||||
# out one process per tag and returns non-zero if any pull
|
||||
# fails.
|
||||
refs=""
|
||||
for v in ${{ steps.versions.outputs.versions }}; do
|
||||
if [ "${v}" = "unstable" ]; then
|
||||
refs="${refs} tailscale/tailscale:${v}"
|
||||
else
|
||||
refs="${refs} ghcr.io/tailscale/tailscale:${v}"
|
||||
fi
|
||||
done
|
||||
echo "${refs}" | tr ' ' '\n' | grep -v '^$' \
|
||||
| xargs -P 0 -I{} docker pull "{}"
|
||||
echo "REFS=${refs}" >> "$GITHUB_ENV"
|
||||
- name: Save Tailscale images to tarball
|
||||
run: |
|
||||
# Single docker save with all refs: one consistent snapshot, no
|
||||
# parallel-daemon race.
|
||||
docker save ${REFS} | gzip > tailscale-released-images.tar.gz
|
||||
ls -lh tailscale-released-images.tar.gz
|
||||
- name: Upload Tailscale released images
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: tailscale-released-images
|
||||
path: tailscale-released-images.tar.gz
|
||||
retention-days: 10
|
||||
sqlite:
|
||||
needs: [build, build-tailscale-released]
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -181,7 +241,7 @@ jobs:
|
|||
- TestACLDynamicUnknownUserRemoval
|
||||
- TestAPIAuthenticationBypass
|
||||
- TestAPIAuthenticationBypassCurl
|
||||
- TestGRPCAuthenticationBypass
|
||||
- TestRemoteCLIAuthenticationBypass
|
||||
- TestCLIWithConfigAuthenticationBypass
|
||||
- TestAuthKeyLogoutAndReloginSameUser
|
||||
- TestAuthKeyLogoutAndReloginNewUser
|
||||
|
|
@ -202,19 +262,34 @@ jobs:
|
|||
- TestAuthWebFlowAuthenticationPingAll
|
||||
- TestAuthWebFlowLogoutAndReloginSameUser
|
||||
- TestAuthWebFlowLogoutAndReloginNewUser
|
||||
- TestPolicyCheckCommand
|
||||
- TestUserCommand
|
||||
- TestPreAuthKeyCommand
|
||||
- TestPreAuthKeyCommandWithoutExpiry
|
||||
- TestPreAuthKeyCommandReusableEphemeral
|
||||
- TestPreAuthKeyCorrectUserLoggedInCommand
|
||||
- TestTaggedNodesCLIOutput
|
||||
- TestApiKeyCommand
|
||||
- TestApiKeyCommandValidation
|
||||
- TestAuthCommandValidation
|
||||
- TestNodeCommand
|
||||
- TestNodeExpireCommand
|
||||
- TestNodeRenameCommand
|
||||
- TestPreAuthKeyCorrectUserLoggedInCommand
|
||||
- TestTaggedNodesCLIOutput
|
||||
- TestNodeExpireFlagsCommand
|
||||
- TestNodeCommandValidation
|
||||
- TestNodeTagCommand
|
||||
- TestNodeRouteCommands
|
||||
- TestNodeBackfillIPsCommand
|
||||
- TestOAuthClientCommand
|
||||
- TestOAuthClientCommandValidation
|
||||
- TestPolicyCheckCommand
|
||||
- TestSSHTestsRejectFailingPolicy
|
||||
- TestPolicyCommand
|
||||
- TestPolicyBrokenConfigCommand
|
||||
- TestPreAuthKeyCommand
|
||||
- TestPreAuthKeyCommandWithoutExpiry
|
||||
- TestPreAuthKeyCommandReusableEphemeral
|
||||
- TestPreAuthKeyDeleteCommand
|
||||
- TestPreAuthKeyCommandValidation
|
||||
- TestServerInfoCommands
|
||||
- TestUserCommand
|
||||
- TestUserCreateCommand
|
||||
- TestUserCommandValidation
|
||||
- TestDERPVerifyEndpoint
|
||||
- TestResolveMagicDNS
|
||||
- TestResolveMagicDNSExtraRecordsPath
|
||||
|
|
@ -236,6 +311,7 @@ jobs:
|
|||
- Test2118DeletingOnlineNodePanics
|
||||
- TestGrantCapRelay
|
||||
- TestGrantCapDrive
|
||||
- TestK8sOperator
|
||||
- TestEnablingRoutes
|
||||
- TestHASubnetRouterFailover
|
||||
- TestSubnetRouteACL
|
||||
|
|
@ -268,6 +344,7 @@ jobs:
|
|||
- TestSSHCheckModeUnapprovedTimeout
|
||||
- TestSSHCheckModeCheckPeriodCLI
|
||||
- TestSSHCheckModeAutoApprove
|
||||
- TestSSHCheckModeSessionLossReDelegates
|
||||
- TestSSHCheckModeNegativeCLI
|
||||
- TestSSHLocalpart
|
||||
- TestTagsAuthKeyWithTagRequestDifferentTag
|
||||
|
|
@ -302,6 +379,8 @@ jobs:
|
|||
- TestTagsAuthKeyWithoutUserInheritsTags
|
||||
- TestTagsAuthKeyWithoutUserRejectsAdvertisedTags
|
||||
- TestTagsAuthKeyConvertToUserViaCLIRegister
|
||||
- TestTS2021WebSocketGET
|
||||
- TestTS2021WASMClientUnderNode
|
||||
- TestTailscaleRustAxum
|
||||
uses: ./.github/workflows/integration-test-template.yml
|
||||
secrets: inherit
|
||||
|
|
@ -310,7 +389,7 @@ jobs:
|
|||
postgres_flag: "--postgres=0"
|
||||
database_name: "sqlite"
|
||||
postgres:
|
||||
needs: [build, build-postgres]
|
||||
needs: [build, build-postgres, build-tailscale-released]
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
47
.github/workflows/test.yml
vendored
47
.github/workflows/test.yml
vendored
|
|
@ -1,47 +0,0 @@
|
|||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- 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 }}
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
env:
|
||||
# As of 2025-01-06, these env vars was not automatically
|
||||
# set anymore which breaks the initdb for postgres on
|
||||
# some of the database migration tests.
|
||||
LC_ALL: "en_US.UTF-8"
|
||||
LC_CTYPE: "en_US.UTF-8"
|
||||
run: nix develop --command -- gotestsum
|
||||
19
.github/workflows/update-flake.yml
vendored
19
.github/workflows/update-flake.yml
vendored
|
|
@ -1,19 +0,0 @@
|
|||
name: update-flake-lock
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # runs weekly on Sunday at 00:00
|
||||
|
||||
jobs:
|
||||
lockfile:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17
|
||||
- name: Update flake.lock
|
||||
uses: DeterminateSystems/update-flake-lock@428c2b58a4b7414dabd372acb6a03dba1084d3ab # v25
|
||||
with:
|
||||
pr-title: "Update flake.lock"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -46,6 +46,9 @@ result
|
|||
|
||||
integration_test/etc/config.dump.yaml
|
||||
|
||||
# OpenAPI spec is served live from the code and emitted on demand, not committed
|
||||
/openapi/v1/headscale.yaml
|
||||
|
||||
# MkDocs
|
||||
.cache
|
||||
/site
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ linters:
|
|||
- gochecknoinits
|
||||
- gocognit
|
||||
- godox
|
||||
- gomodguard
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
|
|
@ -30,6 +31,17 @@ linters:
|
|||
- wrapcheck
|
||||
- wsl
|
||||
settings:
|
||||
goconst:
|
||||
# Test fixtures repeat strings (IPs, tags, hostnames) by their
|
||||
# nature; extracting them obscures the test rather than helping.
|
||||
# Production code stays strict.
|
||||
ignore-tests: true
|
||||
# Default is 3. Bump so "happens thrice" cases that are not part
|
||||
# of a shared vocabulary do not get extracted.
|
||||
min-occurrences: 5
|
||||
# Default is 3. Short literals ("set", "get", "new") read better
|
||||
# at call sites than behind a named constant.
|
||||
min-len: 6
|
||||
forbidigo:
|
||||
forbid:
|
||||
# Forbid time.Sleep everywhere with context-appropriate alternatives
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
# See: https://prek.j178.dev/quickstart/
|
||||
# See: https://prek.j178.dev/builtin/
|
||||
|
||||
# Global exclusions - ignore generated code
|
||||
exclude: ^gen/
|
||||
# Global exclusions - ignore generated code (proto output and emitted OpenAPI)
|
||||
# and recorded golden fixtures.
|
||||
exclude: ^(gen|openapi)/|^hscontrol/testdata/apiv1_golden/
|
||||
|
||||
repos:
|
||||
# Built-in hooks from pre-commit/pre-commit-hooks
|
||||
|
|
|
|||
|
|
@ -275,6 +275,11 @@ Key reminders:
|
|||
`e = e.Str("k", v)`. Forgetting to reassign silently drops the field.
|
||||
- **Tests**: prefer `hscontrol/servertest/` for server-level tests that
|
||||
don't need Docker — faster than full integration tests.
|
||||
- **View types in read paths**: response serializers must read through
|
||||
`NodeView`/`UserView`/`PreAuthKeyView` accessors. `AsStruct()` clones the
|
||||
whole record on every read — it is only for DB-write/merge clones and mutable
|
||||
working copies, never to build an API response. `grep AsStruct hscontrol/api`
|
||||
must come back empty.
|
||||
|
||||
## Gotchas
|
||||
|
||||
|
|
|
|||
166
CHANGELOG.md
166
CHANGELOG.md
|
|
@ -1,8 +1,71 @@
|
|||
# CHANGELOG
|
||||
|
||||
## 0.29.0 (202x-xx-xx)
|
||||
## 0.30.0 (202x-xx-xx)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.76.0**
|
||||
**Minimum supported Tailscale client version: v1.xx.0**
|
||||
|
||||
### v1 REST API replaced; gRPC and Protobuf removed
|
||||
|
||||
The v1 REST API now provides an OpenAPI 3.1 specification at
|
||||
`/api/v1/openapi.yaml`, with interactive documentation at `/api/v1/docs`. This
|
||||
replaces the Swagger 2.0 document and the `/swagger` UI. The Protobuf, gRPC and
|
||||
grpc-gateway stack behind it is gone, and the `headscale` CLI now talks to the
|
||||
HTTP API directly.
|
||||
|
||||
[#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
### OAuth clients and scopes for the v2 API
|
||||
|
||||
The v2 API now authenticates with OAuth 2.0 client-credentials, the way the
|
||||
Tailscale ecosystem does. An OAuth client mints short-lived access tokens whose
|
||||
scopes limit which operations they may perform and whose tags limit the devices
|
||||
they may create, so a credential can be issued with only the access it needs.
|
||||
The `headscale oauth-clients` command manages them. This lets the Tailscale
|
||||
Terraform provider and Kubernetes operator drive Headscale unchanged; admin API
|
||||
keys remain all-access.
|
||||
|
||||
[#3334](https://github.com/juanfont/headscale/pull/3334)
|
||||
|
||||
### BREAKING
|
||||
|
||||
#### API
|
||||
|
||||
- The gRPC API is removed; all programmatic access now goes through the HTTP API at `/api/v1` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- API errors are now RFC 7807 `application/problem+json`, including authentication failures, instead of the previous gRPC-status JSON shape [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- Errors that previously returned HTTP 500 — unknown users or nodes, malformed input, duplicate names — now return the correct 404, 400 or 409 [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- The OpenAPI document is OpenAPI 3.1 at `/api/v1/openapi.yaml` (docs at `/api/v1/docs`), replacing Swagger 2.0 at `/swagger` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
#### CLI
|
||||
|
||||
- `--output json` / `--output yaml` now emit the API's shape — camelCase fields, string-encoded IDs, RFC3339 timestamps — instead of the old Protobuf encoding [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- `headscale policy` renames the database-bypass flag from `--bypass-grpc-and-access-database-directly` to `--bypass-server-and-access-database-directly` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
### Changes
|
||||
|
||||
- Expiring or deleting a non-existent pre-auth key now returns an error instead of silently succeeding [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
|
||||
|
||||
## 0.29.2 (2026-07-01)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.80.0**
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix map generation serializing on the policy lock, so a mass reconnect on `autogroup:self`, via or relay policies no longer stalls clients into `unexpected EOF` retry loops [#3358](https://github.com/juanfont/headscale/pull/3358)
|
||||
- Fix `/ts2021` rejecting the WebSocket `GET` upgrade with 405, which prevented Tailscale JS/WASM control clients from connecting [#3359](https://github.com/juanfont/headscale/pull/3359)
|
||||
- Gracefully handle nodes with an invalid FQDN (empty or too long) instead of failing map delivery; offending names are logged at startup with the fix command [#3349](https://github.com/juanfont/headscale/pull/3349)
|
||||
|
||||
## 0.29.1 (2026-06-18)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.80.0**
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix nodes with `tags='null'` losing their assigned user on upgrade [#3325](https://github.com/juanfont/headscale/pull/3325)
|
||||
|
||||
## 0.29.0 (2026-06-17)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.80.0**
|
||||
|
||||
### Tailscale ACL compatibility improvements
|
||||
|
||||
|
|
@ -44,6 +107,33 @@ This feature is **beta** while behavioural coverage against Tailscale SaaS broad
|
|||
|
||||
[#3229](https://github.com/juanfont/headscale/pull/3229)
|
||||
|
||||
### SSH policy tests (beta)
|
||||
|
||||
Headscale now evaluates the `sshTests` block in a policy file. Each entry names a source, one or
|
||||
more destination hosts, and three optional user lists: `accept` asserts the listed login users
|
||||
reach every destination via an accept- or check-action SSH rule, `deny` asserts none of them
|
||||
reach any destination, and `check` requires reachability specifically through a check-action
|
||||
rule. Tests run on `headscale policy set`, on SIGHUP reload (`systemctl reload headscale` /
|
||||
`kill -HUP $(pidof headscale)`), and on `headscale policy check`. A failing test rejects the
|
||||
write before it is applied, with the same error message Tailscale SaaS would return for the same
|
||||
policy.
|
||||
|
||||
At boot a stored policy whose sshTests no longer pass — for example because a referenced user was
|
||||
deleted while the server was offline — logs a warning and the server keeps running. Fix the
|
||||
policy and reload.
|
||||
|
||||
This feature is **beta** while behavioural coverage against Tailscale SaaS broadens.
|
||||
|
||||
[#3263](https://github.com/juanfont/headscale/pull/3263)
|
||||
|
||||
### SSH rule validation
|
||||
|
||||
SSH rule parsing now trims surrounding whitespace on `action`, `users`, `src`, and `dst`,
|
||||
rejects empty or wildcard entries in `users`, rejects empty `acceptEnv`, and rejects negative
|
||||
`checkPeriod`. `hosts:` aliases are rejected as SSH destinations, non-ASCII tag names are
|
||||
rejected at parse time, and the wording for group-nesting cycles matches Tailscale SaaS.
|
||||
[#3263](https://github.com/juanfont/headscale/pull/3263)
|
||||
|
||||
### Grants
|
||||
|
||||
We now support [Tailscale grants](https://tailscale.com/docs/features/access-control/grants)
|
||||
|
|
@ -53,9 +143,9 @@ field steers traffic through specific tagged subnet routers or exit nodes. The `
|
|||
an ACL rule. Grants can be mixed with ACLs in the same policy file.
|
||||
[#2180](https://github.com/juanfont/headscale/pull/2180)
|
||||
|
||||
As part of this, we added `autogroup:danger-all`. It resolves to `0.0.0.0/0` and `::/0` — all IP
|
||||
As part of this, we added `autogroup:danger-all`. It resolves to `0.0.0.0/0` and `::/0`, all IP
|
||||
addresses, including those outside the tailnet. This replaces the old behaviour where `*` matched
|
||||
all IPs (see BREAKING below). The name is intentionally scary: accepting traffic from the entire
|
||||
all IPs (see BREAKING below). The name is intentional: accepting traffic from the entire
|
||||
internet is a security-sensitive choice. `autogroup:danger-all` can only be used as a source.
|
||||
|
||||
### Node attributes (`nodeAttrs`)
|
||||
|
|
@ -79,7 +169,7 @@ Frequently requested capabilities this unlocks include `magicdns-aaaa`,
|
|||
`disable-relay-server`, `disable-captive-portal-detection`,
|
||||
`nextdns:<profile>` / `nextdns:no-device-info`, `randomize-client-port`,
|
||||
and the Taildrive `drive:share` / `drive:access` pair. The set is not
|
||||
limited to these — any string-only cap an operator places in policy
|
||||
limited to these, any string-only cap an operator places in policy
|
||||
reaches clients unchanged.
|
||||
|
||||
`randomizeClientPort` also lands as a top-level policy field that toggles
|
||||
|
|
@ -126,28 +216,9 @@ mode:
|
|||
A wildcard `nodeAttrs` (`"target": ["*"]`) hands the caps to every
|
||||
node when fine-grained control is not needed.
|
||||
|
||||
### Hostname handling (cleanroom rewrite)
|
||||
### Hostname sanitisation
|
||||
|
||||
The hostname ingest pipeline has been rewritten to match Tailscale SaaS byte-for-byte.
|
||||
Headscale previously had three overlapping regexes and two disagreeing entry points
|
||||
(registration vs map-request update), which caused a recurring class of bugs: names
|
||||
containing apostrophes, spaces, dots, or non-ASCII characters were alternately rejected
|
||||
(dropping updates with log spam) or stored as `invalid-<rand>` surrogates
|
||||
([#3188](https://github.com/juanfont/headscale/issues/3188),
|
||||
[#2926](https://github.com/juanfont/headscale/issues/2926),
|
||||
[#2343](https://github.com/juanfont/headscale/issues/2343),
|
||||
[#2762](https://github.com/juanfont/headscale/issues/2762),
|
||||
[#2177](https://github.com/juanfont/headscale/issues/2177),
|
||||
[#2121](https://github.com/juanfont/headscale/issues/2121),
|
||||
[#2449](https://github.com/juanfont/headscale/issues/2449),
|
||||
[#363](https://github.com/juanfont/headscale/issues/363)).
|
||||
|
||||
What changed:
|
||||
|
||||
- Sanitisation and validation now come directly from
|
||||
`tailscale.com/util/dnsname.SanitizeHostname` / `ValidLabel`.
|
||||
- Admin rename (`headscale nodes rename`) now validates via `dnsname.ValidLabel` and
|
||||
rejects labels already held by another node (previously coerced invalid input silently).
|
||||
Hostnames are now santised using Tailscales `magicdns` sanitisation rules, matching Tailscale SaaS behavior. This means that hostnames with non-ASCII characters, special characters, or reserved DNS label characters are now transformed into valid DNS labels for MagicDNS. This improves our previously too strict sanitisation that rejected hostnames based on our guesswork and not based on the Tailscale upstream behaviour.
|
||||
|
||||
Examples that previously regressed and now work:
|
||||
|
||||
|
|
@ -160,11 +231,24 @@ Examples that previously regressed and now work:
|
|||
| `My-PC!` | `My-PC!` | `my-pc` |
|
||||
| `我的电脑` | `我的电脑` | `node` |
|
||||
|
||||
[#3202](https://github.com/juanfont/headscale/pull/3202)
|
||||
|
||||
### HA subnet router health probing
|
||||
|
||||
Headscale now actively probes HA subnet routers to detect nodes that are connected but not
|
||||
forwarding traffic. The control plane periodically pings HA subnet routers via the Noise
|
||||
control channel and fails over to a healthy standby if the primary stops responding. This is
|
||||
enabled by default (`node.routes.ha.probe_interval: 10s`, `probe_timeout: 5s`) and only
|
||||
active when HA routes exist (2+ nodes advertising the same prefix). Set `probe_interval` to
|
||||
`0` to disable. This complements the existing disconnect-based failover, catching "zombie
|
||||
connected" routers that maintain their control session but cannot route packets.
|
||||
[#3194](https://github.com/juanfont/headscale/pull/3194)
|
||||
|
||||
### BREAKING
|
||||
|
||||
#### Hostname handling
|
||||
|
||||
- The `GivenName` collision policy changed from an 8-char random hash suffix (`laptop-abc12xyz`) to a monotonic numeric suffix (`laptop`, `laptop-1`, `laptop-2`, …), matching Tailscale SaaS. Empty / all-non-ASCII hostnames now fall back to the literal `node` instead of `invalid-<rand>`. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the raw `Hostname` column is unchanged.
|
||||
- The `GivenName` collision policy changed from an 8-char random hash suffix (`laptop-abc12xyz`) to a monotonic numeric suffix (`laptop`, `laptop-1`, `laptop-2`, …), matching Tailscale SaaS. Empty / all-non-ASCII hostnames now fall back to the literal `node` instead of `invalid-<rand>`. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the raw `Hostname` column is unchanged. [#3202](https://github.com/juanfont/headscale/pull/3202)
|
||||
|
||||
#### ACL Policy
|
||||
|
||||
|
|
@ -173,8 +257,8 @@ Examples that previously regressed and now work:
|
|||
- Policies that need to match all IP addresses including non-Tailscale IPs should use `autogroup:danger-all` as a source, or explicit CIDR ranges as destinations [#2180](https://github.com/juanfont/headscale/pull/2180)
|
||||
- `autogroup:danger-all` can only be used as a source; it cannot be used as a destination
|
||||
- **Note**: Users with non-standard IP ranges configured in `prefixes.ipv4` or `prefixes.ipv6` (which is unsupported and produces a warning) will need to explicitly specify their CIDR ranges in ACL rules instead of using `*`
|
||||
- Validate autogroup:self source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for autogroup:self destinations [#3036](https://github.com/juanfont/headscale/pull/3036)
|
||||
- Policies using tags, hosts, or IP addresses as sources for autogroup:self destinations will now fail validation
|
||||
- Validate `autogroup:self` source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for `autogroup:self` destinations [#3036](https://github.com/juanfont/headscale/pull/3036)
|
||||
- Policies using tags, hosts, or IP addresses as sources for `autogroup:self` destinations will now fail validation
|
||||
- The `proto:icmp` protocol name now only includes ICMPv4 (protocol 1), matching Tailscale behavior [#3036](https://github.com/juanfont/headscale/pull/3036)
|
||||
- Previously, `proto:icmp` included both ICMPv4 and ICMPv6
|
||||
- Use `proto:ipv6-icmp` or protocol number `58` explicitly for ICMPv6
|
||||
|
|
@ -190,10 +274,9 @@ Examples that previously regressed and now work:
|
|||
|
||||
- The `randomize_client_port` server-config key was removed; the
|
||||
toggle now lives in the policy file as a top-level
|
||||
`randomizeClientPort` field, matching the Tailscale-hosted schema.
|
||||
`randomizeClientPort` field, matching the Tailscale-hosted schema. [#3251](https://github.com/juanfont/headscale/pull/3251)
|
||||
Headscale refuses to start when the old key is set. Move it to the
|
||||
policy file referenced by `policy.path` (defaults to
|
||||
`/etc/headscale/policy.hujson`):
|
||||
policy file referenced by `policy.path`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
|
|
@ -213,16 +296,6 @@ Examples that previously regressed and now work:
|
|||
- `headscale nodes register` is deprecated in favour of `headscale auth register --auth-id <id> --user <user>` [#1850](https://github.com/juanfont/headscale/pull/1850)
|
||||
- The old command continues to work but will be removed in a future release
|
||||
|
||||
### HA subnet router health probing
|
||||
|
||||
Headscale now actively probes HA subnet routers to detect nodes that are connected but not
|
||||
forwarding traffic. The control plane periodically pings HA subnet routers via the Noise
|
||||
control channel and fails over to a healthy standby if the primary stops responding. This is
|
||||
enabled by default (`node.routes.ha.probe_interval: 10s`, `probe_timeout: 5s`) and only
|
||||
active when HA routes exist (2+ nodes advertising the same prefix). Set `probe_interval` to
|
||||
`0` to disable. This complements the existing disconnect-based failover, catching "zombie
|
||||
connected" routers that maintain their control session but cannot route packets.
|
||||
|
||||
### Changes
|
||||
|
||||
#### ACL Policy
|
||||
|
|
@ -263,7 +336,7 @@ connected" routers that maintain their control session but cannot route packets.
|
|||
- `headscale policy check --bypass-grpc-and-access-database-directly` validates `user@` tokens against the live user database [#3160](https://github.com/juanfont/headscale/issues/3160)
|
||||
- Remove deprecated `--namespace` flag from `nodes list`, `nodes register`, and `debug create-node` commands (use `--user` instead) [#3093](https://github.com/juanfont/headscale/pull/3093)
|
||||
- Remove deprecated `namespace`/`ns` command aliases for `users` and `machine`/`machines` aliases for `nodes` [#3093](https://github.com/juanfont/headscale/pull/3093)
|
||||
- **User deletion**: Fix `DestroyUser` deleting all pre-auth keys in the database instead of only the target user's keys [#3155](https://github.com/juanfont/headscale/pull/3155)
|
||||
- Fix `DestroyUser` deleting all pre-auth keys in the database instead of only the target user's keys [#3155](https://github.com/juanfont/headscale/pull/3155)
|
||||
- `headscale policy check` evaluates the `tests` block when invoked with `--bypass-grpc-and-access-database-directly`; without the flag it warns instead of running the tests against empty data [#1803](https://github.com/juanfont/headscale/issues/1803)
|
||||
|
||||
#### API
|
||||
|
|
@ -283,6 +356,7 @@ connected" routers that maintain their control session but cannot route packets.
|
|||
- Tagged nodes (registered with tagged pre-auth keys) are exempt from default expiry
|
||||
- `oidc.expiry` has been removed; use `node.expiry` instead (applies to all registration methods including OIDC)
|
||||
- `ephemeral_node_inactivity_timeout` is deprecated in favour of `node.ephemeral.inactivity_timeout`
|
||||
- Add `trusted_proxies` to gate `True-Client-IP` / `X-Real-IP` / `X-Forwarded-For` (previously honoured from any client) [#3268](https://github.com/juanfont/headscale/pull/3268)
|
||||
|
||||
#### Debug
|
||||
|
||||
|
|
@ -294,8 +368,10 @@ connected" routers that maintain their control session but cannot route packets.
|
|||
|
||||
- Remove old migrations for the debian package [#3185](https://github.com/juanfont/headscale/pull/3185)
|
||||
- Install `config-example.yaml` as example for the debian package [#3186](https://github.com/juanfont/headscale/pull/3186)
|
||||
- **Node Expiry**: Fix user owned re registration with zero client expiry and no default storing `0001-01-01 00:00:00` in the database instead of NULL [#3199](https://github.com/juanfont/headscale/pull/3199)
|
||||
- Pre-existing rows with `0001-01-01 00:00:00` are not backfilled; they clear themselves the next time the node re-registers
|
||||
- Fix user-owned re-registration with zero client expiry and no default storing `0001-01-01 00:00:00` in the database instead of `NULL` [#3199](https://github.com/juanfont/headscale/pull/3199)
|
||||
- Fix `tailscaled` restart on a node with no expiry resetting `NULL` to `0001-01-01 00:00:00` in the database, affecting both tagged and untagged nodes [#3197](https://github.com/juanfont/headscale/pull/3197)
|
||||
- Backfill `nodes.expiry` rows persisted by older versions as `0001-01-01 00:00:00` to `NULL`, so nodes upgraded from <0.28 stop reporting as expired [#3284](https://github.com/juanfont/headscale/issues/3284)
|
||||
- Update reverse proxy documentation for `trusted_proxies` configuration option [#3292](https://github.com/juanfont/headscale/pull/3292)
|
||||
|
||||
## 0.28.0 (2026-02-04)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# For testing purposes only
|
||||
|
||||
FROM golang:1.26.3-alpine AS build-env
|
||||
FROM golang:1.26.4-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ WORKDIR /go/src/tailscale
|
|||
ARG TARGETARCH
|
||||
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# and are in no way endorsed by Headscale's maintainers as an
|
||||
# official nor supported release or distribution.
|
||||
|
||||
FROM docker.io/golang:1.26.2-trixie AS builder
|
||||
FROM docker.io/golang:1.26.4-trixie AS builder
|
||||
ARG VERSION=dev
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# This Dockerfile is more or less lifted from tailscale/tailscale
|
||||
# to ensure a similar build process when testing the HEAD of tailscale.
|
||||
|
||||
FROM golang:1.26.3-alpine AS build-env
|
||||
FROM golang:1.26.4-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ RUN GOARCH=$TARGETARCH go install -tags="${BUILD_TAGS}" -ldflags="\
|
|||
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
# Upstream: ca-certificates ip6tables iptables iproute2
|
||||
# Tests: curl python3 (traceroute via BusyBox)
|
||||
RUN apk add --no-cache ca-certificates curl ip6tables iptables iproute2 python3
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM rust:1.94-bookworm AS builder
|
||||
FROM rust:1.95-trixie AS builder
|
||||
|
||||
ARG TAILSCALE_RS_REPO=https://github.com/tailscale/tailscale-rs.git
|
||||
ARG TAILSCALE_RS_REF=main
|
||||
|
|
@ -15,7 +15,7 @@ RUN sed -i '/^axum = \["dep:axum"\]/a insecure-keyfetch = ["ts_control/insecure-
|
|||
|
||||
RUN cargo build --release --features axum,insecure-keyfetch --example axum
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
|
|
|||
30
Dockerfile.wasmclient
Normal file
30
Dockerfile.wasmclient
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# For integration testing only.
|
||||
#
|
||||
# Builds the Tailscale control client (integration/wasmic/wasmclient) for
|
||||
# GOOS=js/GOARCH=wasm and packages it with Go's wasm_exec Node runner. The
|
||||
# container idles; the integration test execs
|
||||
# node /app/wasm_exec_node.js /app/client.wasm <control-url>
|
||||
# to drive a real browser-style WebSocket GET against headscale's /ts2021,
|
||||
# guarding the regression in issue #3357.
|
||||
|
||||
FROM golang:1.26.4-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Only the module metadata and the wasm client package are needed to build the
|
||||
# js/wasm binary; its imports (tailscale.com/control/controlhttp, ...) resolve
|
||||
# from the module proxy.
|
||||
COPY go.mod go.sum ./
|
||||
COPY integration/wasmic/wasmclient ./integration/wasmic/wasmclient
|
||||
|
||||
RUN GOOS=js GOARCH=wasm go build -o /out/client.wasm ./integration/wasmic/wasmclient \
|
||||
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" /out/wasm_exec.js \
|
||||
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec_node.js" /out/wasm_exec_node.js
|
||||
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/ /app/
|
||||
|
||||
# Idle; the test execs the client on demand with the headscale control URL.
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
51
Makefile
51
Makefile
|
|
@ -20,7 +20,6 @@ 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/*')
|
||||
PRETTIER_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
|
||||
|
|
@ -35,8 +34,6 @@ check-deps:
|
|||
$(call check_tool,gofumpt)
|
||||
$(call check_tool,mdformat)
|
||||
$(call check_tool,prettier)
|
||||
$(call check_tool,clang-format)
|
||||
$(call check_tool,buf)
|
||||
|
||||
# Build targets
|
||||
.PHONY: build
|
||||
|
|
@ -53,7 +50,7 @@ test: check-deps $(GO_SOURCES) go.mod go.sum
|
|||
|
||||
# Formatting targets
|
||||
.PHONY: fmt
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier fmt-proto
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier
|
||||
|
||||
.PHONY: fmt-go
|
||||
fmt-go: check-deps $(GO_SOURCES)
|
||||
|
|
@ -71,35 +68,49 @@ fmt-prettier: check-deps $(PRETTIER_SOURCES)
|
|||
@echo "Formatting markup and config files..."
|
||||
prettier --write '**/*.{ts,js,md,yaml,yml,sass,css,scss,html}'
|
||||
|
||||
.PHONY: fmt-proto
|
||||
fmt-proto: check-deps $(PROTO_SOURCES)
|
||||
@echo "Formatting Protocol Buffer files..."
|
||||
clang-format -i $(PROTO_SOURCES)
|
||||
|
||||
# Linting targets
|
||||
.PHONY: lint
|
||||
lint: lint-go lint-proto
|
||||
lint: lint-go
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go: check-deps $(GO_SOURCES) go.mod go.sum
|
||||
@echo "Linting Go code..."
|
||||
golangci-lint run --timeout 10m
|
||||
|
||||
.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
|
||||
@echo "Generating code..."
|
||||
go generate ./...
|
||||
$(MAKE) client
|
||||
|
||||
# Emit the OpenAPI spec on demand. The server serves it live at /openapi.yaml;
|
||||
# this is for external consumers or inspection and is not committed.
|
||||
.PHONY: openapi
|
||||
openapi:
|
||||
@echo "Emitting OpenAPI spec from code..."
|
||||
go run ./cmd/gen-openapi
|
||||
|
||||
# Generate the strongly-typed Go HTTP clients (v1 and v2). The served specs are
|
||||
# OpenAPI 3.1, but oapi-codegen v2 does not yet read 3.1, so each client is
|
||||
# generated from a transient 3.0.3 downgrade of its document. Pinned so the
|
||||
# committed clients are reproducible.
|
||||
.PHONY: client
|
||||
client:
|
||||
@echo "Generating API clients..."
|
||||
@tmp=$$(mktemp -t headscale-openapi-3.0.XXXXXX.yaml); \
|
||||
go run ./cmd/gen-openapi -downgrade "$$tmp" && \
|
||||
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
|
||||
-generate types,client -package clientv1 -o gen/client/v1/client.gen.go "$$tmp" && \
|
||||
go run ./cmd/gen-openapi -api v2 -downgrade "$$tmp" && \
|
||||
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
|
||||
-generate types,client -package clientv2 -o gen/client/v2/client.gen.go "$$tmp"; \
|
||||
status=$$?; rm -f "$$tmp"; exit $$status
|
||||
|
||||
# Clean targets
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf headscale gen
|
||||
rm -rf headscale gen/client
|
||||
|
||||
# Development workflow
|
||||
.PHONY: dev
|
||||
|
|
@ -119,9 +130,9 @@ help:
|
|||
@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 " fmt - Format all code (Go, docs, markup)"
|
||||
@echo " lint - Lint all code (Go)"
|
||||
@echo " generate - Generate code (go generate + client)"
|
||||
@echo " dev - Full development workflow (fmt + lint + test + build)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
|
|
@ -129,9 +140,7 @@ help:
|
|||
@echo " fmt-go - Format Go code only"
|
||||
@echo " fmt-mdformat - Format documentation only"
|
||||
@echo " fmt-prettier - Format markup and config files 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"
|
||||
|
|
|
|||
21
buf.gen.yaml
21
buf.gen.yaml
|
|
@ -1,21 +0,0 @@
|
|||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- name: go-grpc
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- name: grpc-gateway
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- generate_unbound_methods=true
|
||||
# - name: gorm
|
||||
# out: gen/go
|
||||
# opt:
|
||||
# - paths=source_relative,enums=string,gateway=true
|
||||
- name: openapiv2
|
||||
out: gen/openapiv2
|
||||
|
|
@ -42,8 +42,7 @@ go tool mts node1 status
|
|||
| `--port` | 8080 | Headscale listen port |
|
||||
| `--keep` | false | Keep state directory on exit |
|
||||
|
||||
The metrics/debug port is `port + 1010` (default 9090) and the gRPC
|
||||
port is `port + 42363` (default 50443).
|
||||
The metrics/debug port is `port + 1010` (default 9090).
|
||||
|
||||
## What it does
|
||||
|
||||
|
|
|
|||
|
|
@ -28,17 +28,14 @@ var errHealthTimeout = errors.New("health check timed out")
|
|||
|
||||
var errEmptyAuthKey = errors.New("empty auth key in response")
|
||||
|
||||
// maxDevPort is the highest --port value that keeps both the derived
|
||||
// metrics port (port+1010) and gRPC port (port+42363) inside the valid
|
||||
// 1..65535 TCP range.
|
||||
const maxDevPort = 23172
|
||||
// maxDevPort is the highest --port value that keeps the derived metrics
|
||||
// port (port+1010) inside the valid 1..65535 TCP range.
|
||||
const maxDevPort = 64525
|
||||
|
||||
const devConfig = `---
|
||||
server_url: http://127.0.0.1:%d
|
||||
listen_addr: 127.0.0.1:%d
|
||||
metrics_listen_addr: 127.0.0.1:%d
|
||||
grpc_listen_addr: 127.0.0.1:%d
|
||||
grpc_allow_insecure: true
|
||||
|
||||
noise:
|
||||
private_key_path: %s/noise_private.key
|
||||
|
|
@ -83,7 +80,7 @@ func main() {
|
|||
|
||||
if *port < 1 || *port > maxDevPort {
|
||||
log.Fatalf(
|
||||
"--port must be in 1..%d (higher values overflow the derived gRPC port); got %d",
|
||||
"--port must be in 1..%d (higher values overflow the derived metrics port); got %d",
|
||||
maxDevPort, *port,
|
||||
)
|
||||
}
|
||||
|
|
@ -101,7 +98,6 @@ func main() {
|
|||
|
||||
func run() error {
|
||||
metricsPort := *port + 1010 // default 9090
|
||||
grpcPort := *port + 42363 // default 50443
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "headscale-dev-")
|
||||
if err != nil {
|
||||
|
|
@ -114,8 +110,9 @@ func run() error {
|
|||
|
||||
// Write config.
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
configContent := fmt.Sprintf(devConfig,
|
||||
*port, *port, metricsPort, grpcPort,
|
||||
configContent := fmt.Sprintf(
|
||||
devConfig,
|
||||
*port, *port, metricsPort,
|
||||
tmpDir, tmpDir, tmpDir,
|
||||
)
|
||||
|
||||
|
|
@ -193,7 +190,8 @@ func run() error {
|
|||
}
|
||||
|
||||
// Print banner.
|
||||
fmt.Printf(`
|
||||
fmt.Printf(
|
||||
`
|
||||
=== Headscale Dev Environment ===
|
||||
Server: http://127.0.0.1:%d
|
||||
Metrics: http://127.0.0.1:%d
|
||||
|
|
|
|||
75
cmd/gen-openapi/main.go
Normal file
75
cmd/gen-openapi/main.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Command gen-openapi emits a Headscale OpenAPI document from the authoritative
|
||||
// Huma definitions in hscontrol/api/v1 and hscontrol/api/v2. The server also
|
||||
// serves each spec live (at /openapi.yaml and /api/v2/openapi); this tool emits
|
||||
// them on demand, and with -downgrade the 3.0.3 form used to generate the typed
|
||||
// client. The output is not committed.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/gen-openapi # write the v1 3.1 spec to its default path
|
||||
// go run ./cmd/gen-openapi -api v2 # write the v2 3.1 spec to its default path
|
||||
// go run ./cmd/gen-openapi -downgrade <path> # write the v1 3.0.3 downgrade (for client gen)
|
||||
// go run ./cmd/gen-openapi -api v2 -downgrade <path> # the v2 3.0.3 downgrade
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/hscontrol/api/v1"
|
||||
apiv2 "github.com/juanfont/headscale/hscontrol/api/v2"
|
||||
)
|
||||
|
||||
// spec bundles a version's full (3.1) and downgraded (3.0.3) generators with the
|
||||
// committed output path. outPath is relative to the repository root.
|
||||
type spec struct {
|
||||
full func() ([]byte, error)
|
||||
down func() ([]byte, error)
|
||||
outPath string
|
||||
}
|
||||
|
||||
// specs maps the -api value to its generators.
|
||||
var specs = map[string]spec{
|
||||
"v1": {apiv1.Spec, apiv1.Spec30, "openapi/v1/headscale.yaml"},
|
||||
"v2": {apiv2.Spec, apiv2.Spec30, "openapi/v2/headscale.yaml"},
|
||||
}
|
||||
|
||||
func main() {
|
||||
api := flag.String("api", "v1", "which API spec to emit: v1 or v2")
|
||||
downgrade := flag.String("downgrade", "", "write the OpenAPI 3.0.3 downgrade to this path instead of the committed 3.1 spec")
|
||||
flag.Parse()
|
||||
|
||||
s, ok := specs[*api]
|
||||
if !ok {
|
||||
log.Fatalf("unknown -api %q (want v1 or v2)", *api)
|
||||
}
|
||||
|
||||
if *downgrade != "" {
|
||||
writeSpec(*downgrade, s.down)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writeSpec(s.outPath, s.full)
|
||||
}
|
||||
|
||||
func writeSpec(path string, gen func() ([]byte, error)) {
|
||||
spec, err := gen()
|
||||
if err != nil {
|
||||
log.Fatalf("generating OpenAPI spec: %v", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("creating output directory: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, spec, 0o600)
|
||||
if err != nil {
|
||||
log.Fatalf("writing %s: %v", path, err)
|
||||
}
|
||||
|
||||
log.Printf("wrote %s", path)
|
||||
}
|
||||
|
|
@ -3,11 +3,11 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -41,36 +41,43 @@ var apiKeysCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var listAPIKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List the Api keys for headscale",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{"ID", "Prefix", "Expiration", "Created"},
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
for _, key := range response.GetApiKeys() {
|
||||
apiKeys := resp.JSON200.ApiKeys
|
||||
|
||||
return printListOutput(cmd, apiKeys, func() error {
|
||||
rows := make([][]string, 0, len(apiKeys))
|
||||
for _, key := range apiKeys {
|
||||
expiration := "-"
|
||||
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
if key.Expiration != nil {
|
||||
expiration = ColourTime(*key.Expiration)
|
||||
}
|
||||
|
||||
tableData = append(tableData, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetPrefix(),
|
||||
var created string
|
||||
if key.CreatedAt != nil {
|
||||
created = key.CreatedAt.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
key.Id,
|
||||
key.Prefix,
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
created,
|
||||
})
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{"ID", "Prefix", colExpiration, colCreated}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
@ -82,21 +89,25 @@ var createAPIKeyCmd = &cobra.Command{
|
|||
Creates a new Api key, the Api key is only visible on creation
|
||||
and cannot be retrieved again.
|
||||
If you lose a key, create a new one and revoke (expire) the old one.`,
|
||||
Aliases: []string{"c", "new"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
expiryTime, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
|
||||
Expiration: expiration,
|
||||
resp, err := client.CreateApiKeyWithResponse(ctx, clientv1.CreateApiKeyJSONRequestBody{
|
||||
Expiration: &expiryTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.ApiKey, resp.JSON200.ApiKey)
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
@ -117,45 +128,96 @@ func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {
|
|||
}
|
||||
|
||||
var expireAPIKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire an ApiKey",
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
body := clientv1.ExpireApiKeyJSONRequestBody{}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
body.Id = &idStr
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
body.Prefix = &prefix
|
||||
}
|
||||
|
||||
resp, err := client.ExpireApiKeyWithResponse(ctx, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
var deleteAPIKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an ApiKey",
|
||||
Aliases: []string{"remove", "del"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
// The DELETE route addresses the key by its prefix in the path. When the
|
||||
// user deletes by --id we resolve the id to its (masked) prefix first,
|
||||
// since the path segment is required and a query-only id cannot be routed.
|
||||
if prefix == "" {
|
||||
prefix, err = apiKeyPrefixForID(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.DeleteApiKeyWithResponse(ctx, prefix, &clientv1.DeleteApiKeyParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
// apiKeyPrefixForID resolves an API key id to its display prefix by listing the
|
||||
// keys. The DELETE endpoint addresses keys by prefix in the URL path, so a
|
||||
// delete by --id needs the prefix; the returned masked prefix is accepted by
|
||||
// the server's lookup.
|
||||
func apiKeyPrefixForID(
|
||||
ctx context.Context,
|
||||
client *clientv1.ClientWithResponses,
|
||||
id uint64,
|
||||
) (string, error) {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return "", apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
for _, key := range resp.JSON200.ApiKeys {
|
||||
if key.Id == idStr {
|
||||
return key.Prefix, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: api key %d not found", errMissingParameter, id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -33,61 +34,66 @@ var authCmd = &cobra.Command{
|
|||
var authRegisterCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a node to your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRegisterRequest{
|
||||
AuthId: authID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
response, err := client.AuthRegister(ctx, request)
|
||||
resp, err := client.AuthRegisterWithResponse(ctx, clientv1.AuthRegisterJSONRequestBody{
|
||||
AuthId: &authID,
|
||||
User: &user,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
node := resp.JSON200.Node
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
|
||||
node,
|
||||
fmt.Sprintf("Node %s registered", node.GivenName),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
var authApproveCmd = &cobra.Command{
|
||||
Use: "approve",
|
||||
Short: "Approve a pending authentication request",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthApproveRequest{
|
||||
AuthId: authID,
|
||||
}
|
||||
|
||||
response, err := client.AuthApprove(ctx, request)
|
||||
resp, err := client.AuthApproveWithResponse(ctx, clientv1.AuthApproveJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("approving auth request: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Auth request approved")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request approved")
|
||||
}),
|
||||
}
|
||||
|
||||
var authRejectCmd = &cobra.Command{
|
||||
Use: "reject",
|
||||
Short: "Reject a pending authentication request",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRejectRequest{
|
||||
AuthId: authID,
|
||||
}
|
||||
|
||||
response, err := client.AuthReject(ctx, request)
|
||||
resp, err := client.AuthRejectWithResponse(ctx, clientv1.AuthRejectJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("rejecting auth request: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Auth request rejected")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request rejected")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -32,7 +33,7 @@ var debugCmd = &cobra.Command{
|
|||
var createNodeCmd = &cobra.Command{
|
||||
Use: "create-node",
|
||||
Short: "Create a node that can be registered with `auth register <>` command",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
|
|
@ -44,18 +45,20 @@ var createNodeCmd = &cobra.Command{
|
|||
|
||||
routes, _ := cmd.Flags().GetStringSlice("route")
|
||||
|
||||
request := &v1.DebugCreateNodeRequest{
|
||||
Key: registrationID,
|
||||
Name: name,
|
||||
User: user,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
response, err := client.DebugCreateNode(ctx, request)
|
||||
resp, err := client.DebugCreateNodeWithResponse(ctx, clientv1.DebugCreateNodeJSONRequestBody{
|
||||
Key: ®istrationID,
|
||||
Name: &name,
|
||||
User: &user,
|
||||
Routes: &routes,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node created")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node created")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -16,12 +17,16 @@ var healthCmd = &cobra.Command{
|
|||
Use: "health",
|
||||
Short: "Check the health of the Headscale server",
|
||||
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.Health(ctx, &v1.HealthRequest{})
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.HealthWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking health: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
|
@ -16,19 +17,15 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
||||
type Error string
|
||||
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
|
||||
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
|
||||
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
|
||||
errMockOidcUsersNotDefined = Error("MOCKOIDC_USERS not defined")
|
||||
refreshTTL = 60 * time.Minute
|
||||
var (
|
||||
errMockOidcClientIDNotDefined = errors.New("MOCKOIDC_CLIENT_ID not defined")
|
||||
errMockOidcClientSecretNotDefined = errors.New("MOCKOIDC_CLIENT_SECRET not defined")
|
||||
errMockOidcPortNotDefined = errors.New("MOCKOIDC_PORT not defined")
|
||||
errMockOidcUsersNotDefined = errors.New("MOCKOIDC_USERS not defined")
|
||||
)
|
||||
|
||||
const refreshTTL = 60 * time.Minute
|
||||
|
||||
var accessTTL = 2 * time.Minute
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
|
|
@ -67,41 +67,59 @@ var registerNodeCmd = &cobra.Command{
|
|||
Use: "register",
|
||||
Short: "Registers a node to your network",
|
||||
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
|
||||
request := &v1.RegisterNodeRequest{
|
||||
Key: registrationID,
|
||||
User: user,
|
||||
params := &clientv1.RegisterNodeParams{
|
||||
User: &user,
|
||||
Key: ®istrationID,
|
||||
}
|
||||
|
||||
response, err := client.RegisterNode(ctx, request)
|
||||
resp, err := client.RegisterNodeWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
node := resp.JSON200.Node
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
|
||||
node,
|
||||
fmt.Sprintf("Node %s registered", node.GivenName),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
var listNodesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List nodes",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user})
|
||||
params := &clientv1.ListNodesParams{}
|
||||
if user != "" {
|
||||
params.User = &user
|
||||
}
|
||||
|
||||
resp, err := client.ListNodesWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetNodes(), func() error {
|
||||
tableData, err := nodesToPtables(user, response.GetNodes())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
nodes := resp.JSON200.Nodes
|
||||
|
||||
return printListOutput(cmd, nodes, func() error {
|
||||
tableData, err := nodesToPtables(nodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting to table: %w", err)
|
||||
}
|
||||
|
|
@ -115,27 +133,33 @@ var listNodeRoutesCmd = &cobra.Command{
|
|||
Use: "list-routes",
|
||||
Short: "List routes available on nodes",
|
||||
Aliases: []string{"lsr", "routes"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{})
|
||||
resp, err := client.ListNodesWithResponse(ctx, &clientv1.ListNodesParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
nodes := response.GetNodes()
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
nodes := resp.JSON200.Nodes
|
||||
|
||||
if identifier != 0 {
|
||||
for _, node := range response.GetNodes() {
|
||||
if node.GetId() == identifier {
|
||||
nodes = []*v1.Node{node}
|
||||
idStr := strconv.FormatUint(identifier, util.Base10)
|
||||
for _, node := range nodes {
|
||||
if node.Id == idStr {
|
||||
nodes = []clientv1.Node{node}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes = lo.Filter(nodes, func(n *v1.Node, _ int) bool {
|
||||
return (n.GetSubnetRoutes() != nil && len(n.GetSubnetRoutes()) > 0) || (n.GetApprovedRoutes() != nil && len(n.GetApprovedRoutes()) > 0) || (n.GetAvailableRoutes() != nil && len(n.GetAvailableRoutes()) > 0)
|
||||
nodes = lo.Filter(nodes, func(n clientv1.Node, _ int) bool {
|
||||
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
|
||||
})
|
||||
|
||||
return printListOutput(cmd, nodes, func() error {
|
||||
|
|
@ -145,29 +169,33 @@ var listNodeRoutesCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var expireNodeCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire (log out) a node in your network",
|
||||
Long: `Expiring a node will keep the node in the database and force it to reauthenticate.
|
||||
|
||||
Use --disable to disable key expiry (node will never expire).`,
|
||||
Aliases: []string{"logout", "exp", "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"logout", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
||||
nodeID := strconv.FormatUint(identifier, util.Base10)
|
||||
|
||||
// Handle disable expiry - node will never expire.
|
||||
if disableExpiry {
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
DisableExpiry: true,
|
||||
}
|
||||
disable := true
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
|
||||
DisableExpiry: &disable,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disabling node expiry: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiry disabled")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node expiry disabled")
|
||||
}
|
||||
|
||||
expiry, _ := cmd.Flags().GetString("expiry")
|
||||
|
|
@ -185,28 +213,31 @@ Use --disable to disable key expiry (node will never expire).`,
|
|||
}
|
||||
}
|
||||
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
Expiry: timestamppb.New(expiryTime),
|
||||
}
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
|
||||
Expiry: &expiryTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring node: %w", err)
|
||||
}
|
||||
|
||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||
return printOutput(cmd, response.GetNode(), "Node expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiration updated")
|
||||
node := resp.JSON200.Node
|
||||
|
||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||
return printOutput(cmd, node, "Node expired")
|
||||
}
|
||||
|
||||
return printOutput(cmd, node, "Node expiration updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var renameNodeCmd = &cobra.Command{
|
||||
Use: "rename NEW_NAME",
|
||||
Short: "Renames a node in your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
newName := ""
|
||||
|
|
@ -214,55 +245,55 @@ var renameNodeCmd = &cobra.Command{
|
|||
newName = args[0]
|
||||
}
|
||||
|
||||
request := &v1.RenameNodeRequest{
|
||||
NodeId: identifier,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameNode(ctx, request)
|
||||
resp, err := client.RenameNodeWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node renamed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
var deleteNodeCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a node",
|
||||
Aliases: []string{"del"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{aliasDel},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
nodeID := strconv.FormatUint(identifier, util.Base10)
|
||||
|
||||
getRequest := &v1.GetNodeRequest{
|
||||
NodeId: identifier,
|
||||
}
|
||||
|
||||
getResponse, err := client.GetNode(ctx, getRequest)
|
||||
getResponse, err := client.GetNodeWithResponse(ctx, nodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting node: %w", err)
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteNodeRequest{
|
||||
NodeId: identifier,
|
||||
if getResponse.StatusCode() != http.StatusOK {
|
||||
return apiError(getResponse.StatusCode(), getResponse.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the node %s?",
|
||||
getResponse.GetNode().GetName(),
|
||||
getResponse.JSON200.Node.Name,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted")
|
||||
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
|
||||
}
|
||||
|
||||
_, err = client.DeleteNode(ctx, deleteRequest)
|
||||
deleteResponse, err := client.DeleteNodeWithResponse(ctx, nodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting node: %w", err)
|
||||
}
|
||||
|
||||
if deleteResponse.StatusCode() != http.StatusOK {
|
||||
return apiError(deleteResponse.StatusCode(), deleteResponse.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
map[string]string{"Result": "Node deleted"},
|
||||
map[string]string{colResult: "Node deleted"},
|
||||
"Node deleted",
|
||||
)
|
||||
}),
|
||||
|
|
@ -288,26 +319,26 @@ be assigned to nodes.`,
|
|||
return nil
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
return withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
confirmed := true
|
||||
|
||||
changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfilling IPs: %w", err)
|
||||
}
|
||||
resp, err := client.BackfillNodeIPsWithResponse(ctx, &clientv1.BackfillNodeIPsParams{
|
||||
Confirmed: &confirmed,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfilling IPs: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, changes, "Node IPs backfilled successfully")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Node IPs backfilled successfully")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(
|
||||
currentUser string,
|
||||
nodes []*v1.Node,
|
||||
) (pterm.TableData, error) {
|
||||
func nodesToPtables(nodes []clientv1.Node) (pterm.TableData, error) {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
"Hostname",
|
||||
|
|
@ -319,88 +350,57 @@ func nodesToPtables(
|
|||
"IP addresses",
|
||||
"Ephemeral",
|
||||
"Last seen",
|
||||
"Expiration",
|
||||
colExpiration,
|
||||
"Connected",
|
||||
"Expired",
|
||||
}
|
||||
tableData := pterm.TableData{tableHeader}
|
||||
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||
tableData[0] = tableHeader
|
||||
|
||||
for _, node := range nodes {
|
||||
var ephemeral bool
|
||||
if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
|
||||
ephemeral = true
|
||||
// An absent pre-auth key decodes into a zero NodePreAuthKey, so guard
|
||||
// on Id before reading its flags.
|
||||
ephemeral := node.PreAuthKey.Id != "" && node.PreAuthKey.Ephemeral
|
||||
|
||||
var lastSeenTime string
|
||||
if node.LastSeen != nil {
|
||||
lastSeenTime = node.LastSeen.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var (
|
||||
lastSeen time.Time
|
||||
lastSeenTime string
|
||||
)
|
||||
|
||||
if node.GetLastSeen() != nil {
|
||||
lastSeen = node.GetLastSeen().AsTime()
|
||||
lastSeenTime = lastSeen.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var (
|
||||
expiry time.Time
|
||||
expiryTime string
|
||||
)
|
||||
|
||||
if node.GetExpiry() != nil {
|
||||
expiry = node.GetExpiry().AsTime()
|
||||
expiryTime = expiry.Format(HeadscaleDateTimeFormat)
|
||||
} else {
|
||||
expiryTime = "N/A"
|
||||
expiryTime := "N/A"
|
||||
if node.Expiry != nil {
|
||||
expiryTime = node.Expiry.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var machineKey key.MachinePublic
|
||||
|
||||
err := machineKey.UnmarshalText(
|
||||
[]byte(node.GetMachineKey()),
|
||||
)
|
||||
err := machineKey.UnmarshalText([]byte(node.MachineKey))
|
||||
if err != nil {
|
||||
machineKey = key.MachinePublic{}
|
||||
}
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
|
||||
err = nodeKey.UnmarshalText(
|
||||
[]byte(node.GetNodeKey()),
|
||||
)
|
||||
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var online string
|
||||
if node.GetOnline() {
|
||||
online := pterm.LightRed("offline")
|
||||
if node.Online {
|
||||
online = pterm.LightGreen("online")
|
||||
} else {
|
||||
online = pterm.LightRed("offline")
|
||||
}
|
||||
|
||||
var expired string
|
||||
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
|
||||
expired := pterm.LightGreen("no")
|
||||
if node.Expiry != nil && node.Expiry.Before(time.Now()) {
|
||||
expired = pterm.LightRed("yes")
|
||||
} else {
|
||||
expired = pterm.LightGreen("no")
|
||||
}
|
||||
|
||||
var tagsBuilder strings.Builder
|
||||
|
||||
for _, tag := range node.GetTags() {
|
||||
tagsBuilder.WriteString("\n" + tag)
|
||||
}
|
||||
|
||||
tags := strings.TrimLeft(tagsBuilder.String(), "\n")
|
||||
|
||||
var user string
|
||||
if node.GetUser() != nil {
|
||||
user = node.GetUser().GetName()
|
||||
}
|
||||
tags := strings.Join(node.Tags, "\n")
|
||||
|
||||
var ipBuilder strings.Builder
|
||||
|
||||
for _, addr := range node.GetIpAddresses() {
|
||||
for _, addr := range node.IpAddresses {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil {
|
||||
if ipBuilder.Len() > 0 {
|
||||
|
|
@ -414,12 +414,12 @@ func nodesToPtables(
|
|||
ipAddresses := ipBuilder.String()
|
||||
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetName(),
|
||||
node.GetGivenName(),
|
||||
node.Id,
|
||||
node.Name,
|
||||
node.GivenName,
|
||||
machineKey.ShortString(),
|
||||
nodeKey.ShortString(),
|
||||
user,
|
||||
node.User.Name,
|
||||
tags,
|
||||
ipAddresses,
|
||||
strconv.FormatBool(ephemeral),
|
||||
|
|
@ -438,7 +438,7 @@ func nodesToPtables(
|
|||
}
|
||||
|
||||
func nodeRoutesToPtables(
|
||||
nodes []*v1.Node,
|
||||
nodes []clientv1.Node,
|
||||
) pterm.TableData {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
|
|
@ -447,15 +447,16 @@ func nodeRoutesToPtables(
|
|||
"Available",
|
||||
"Serving (Primary)",
|
||||
}
|
||||
tableData := pterm.TableData{tableHeader}
|
||||
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||
tableData[0] = tableHeader
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetGivenName(),
|
||||
strings.Join(node.GetApprovedRoutes(), "\n"),
|
||||
strings.Join(node.GetAvailableRoutes(), "\n"),
|
||||
strings.Join(node.GetSubnetRoutes(), "\n"),
|
||||
node.Id,
|
||||
node.GivenName,
|
||||
strings.Join(node.ApprovedRoutes, "\n"),
|
||||
strings.Join(node.AvailableRoutes, "\n"),
|
||||
strings.Join(node.SubnetRoutes, "\n"),
|
||||
}
|
||||
tableData = append(
|
||||
tableData,
|
||||
|
|
@ -470,43 +471,43 @@ var tagCmd = &cobra.Command{
|
|||
Use: "tag",
|
||||
Short: "Manage the tags of a node",
|
||||
Aliases: []string{"tags", "t"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
|
||||
|
||||
// Sending tags to node
|
||||
request := &v1.SetTagsRequest{
|
||||
NodeId: identifier,
|
||||
Tags: tagsToSet,
|
||||
}
|
||||
|
||||
resp, err := client.SetTags(ctx, request)
|
||||
resp, err := client.SetTagsWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetTagsJSONRequestBody{
|
||||
Tags: &tagsToSet,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting tags: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var approveRoutesCmd = &cobra.Command{
|
||||
Use: "approve-routes",
|
||||
Short: "Manage the approved routes of a node",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
routes, _ := cmd.Flags().GetStringSlice("routes")
|
||||
|
||||
// Sending routes to node
|
||||
request := &v1.SetApprovedRoutesRequest{
|
||||
NodeId: identifier,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
resp, err := client.SetApprovedRoutes(ctx, request)
|
||||
resp, err := client.SetApprovedRoutesWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetApprovedRoutesJSONRequestBody{
|
||||
Routes: &routes,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting approved routes: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
271
cmd/headscale/cli/oauth_client.go
Normal file
271
cmd/headscale/cli/oauth_client.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
clientv2 "github.com/juanfont/headscale/gen/client/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// oauthTailnet is the single Headscale tailnet the v2 API addresses as "-".
|
||||
const oauthTailnet = "-"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(oauthClientsCmd)
|
||||
|
||||
oauthClientsCmd.AddCommand(listOAuthClientsCmd)
|
||||
|
||||
createOAuthClientCmd.Flags().
|
||||
StringArrayP("scope", "s", nil, "Scope the client's tokens are granted (repeatable): auth_keys, oauth_keys, devices:core, devices:routes, policy_file, feature_settings (each with a :read variant), or all/all:read")
|
||||
createOAuthClientCmd.Flags().
|
||||
StringArrayP("tag", "t", nil, "Tag the client's tokens may assign to devices (repeatable), e.g. tag:k8s-operator")
|
||||
createOAuthClientCmd.Flags().StringP("description", "d", "", "Human-readable description")
|
||||
oauthClientsCmd.AddCommand(createOAuthClientCmd)
|
||||
|
||||
deleteOAuthClientCmd.Flags().StringP("id", "i", "", "OAuth client id")
|
||||
oauthClientsCmd.AddCommand(deleteOAuthClientCmd)
|
||||
}
|
||||
|
||||
var oauthClientsCmd = &cobra.Command{
|
||||
Use: "oauth-clients",
|
||||
Short: "Manage OAuth clients",
|
||||
Aliases: []string{"oauth-client", "oauthclients", "oauthclient", "oauth"},
|
||||
}
|
||||
|
||||
var createOAuthClientCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create an OAuth client",
|
||||
Long: `Create a general-purpose OAuth client. It authenticates with the OAuth 2.0
|
||||
client-credentials grant and mints short-lived, scope-limited access tokens.
|
||||
The wire format is compatible with Tailscale tooling (the Terraform provider,
|
||||
the Kubernetes operator, tscli, ...), so those can drive Headscale unchanged.
|
||||
|
||||
The client secret is shown ONCE on creation and cannot be retrieved again; if
|
||||
you lose it, delete the client and create a new one.
|
||||
|
||||
Scopes gate what the client's tokens may do; tags are the device tags those
|
||||
tokens may assign and are required when the scopes include devices:core or
|
||||
auth_keys.`,
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
scopes, _ := cmd.Flags().GetStringArray("scope")
|
||||
tags, _ := cmd.Flags().GetStringArray("tag")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
|
||||
if len(scopes) == 0 {
|
||||
return fmt.Errorf("at least one --scope is required: %w", errMissingParameter)
|
||||
}
|
||||
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
keyType := "client"
|
||||
|
||||
resp, err := client.CreateKeyWithResponse(ctx, oauthTailnet, clientv2.CreateKeyRequest{
|
||||
KeyType: &keyType,
|
||||
Scopes: &scopes,
|
||||
Tags: &tags,
|
||||
Description: &description,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating oauth client: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := resp.JSON200
|
||||
|
||||
return printOutput(cmd, key,
|
||||
fmt.Sprintf("OAuth client %s created.\nSecret (shown once, store it now): %s", key.Id, ptrStr(key.Key)))
|
||||
},
|
||||
}
|
||||
|
||||
var listOAuthClientsCmd = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List OAuth clients",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ListKeysWithResponse(ctx, oauthTailnet, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing oauth clients: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The keys endpoint is multiplexed; keep only OAuth clients.
|
||||
clients := make([]clientv2.Key, 0, len(resp.JSON200.Keys))
|
||||
|
||||
for _, k := range resp.JSON200.Keys {
|
||||
if k.KeyType == "client" {
|
||||
clients = append(clients, k)
|
||||
}
|
||||
}
|
||||
|
||||
return printListOutput(cmd, clients, func() error {
|
||||
rows := make([][]string, 0, len(clients))
|
||||
for _, c := range clients {
|
||||
rows = append(rows, []string{
|
||||
c.Id,
|
||||
strings.Join(ptrStrs(c.Scopes), ","),
|
||||
strings.Join(ptrStrs(c.Tags), ","),
|
||||
ptrStr(c.Description),
|
||||
c.Created.Format(HeadscaleDateTimeFormat),
|
||||
})
|
||||
}
|
||||
|
||||
return renderTable([]string{"ID", "Scopes", "Tags", "Description", colCreated}, rows)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var deleteOAuthClientCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an OAuth client",
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
id, _ := cmd.Flags().GetString("id")
|
||||
if id == "" {
|
||||
return fmt.Errorf("--id is required: %w", errMissingParameter)
|
||||
}
|
||||
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.DeleteKeyWithResponse(ctx, oauthTailnet, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting oauth client: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printOutput(cmd, map[string]string{"id": id}, "OAuth client "+id+" deleted")
|
||||
},
|
||||
}
|
||||
|
||||
// newV2Client builds a generated v2 API client, selecting the transport the same
|
||||
// way the v1 client does: over the local unix socket it is unauthenticated
|
||||
// (local trust); a remote address injects the configured API key as a bearer
|
||||
// token.
|
||||
func newV2Client() (context.Context, *clientv2.ClientWithResponses, context.CancelFunc, error) {
|
||||
cfg, err := types.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
||||
|
||||
if cfg.CLI.Address == "" {
|
||||
socketPath := cfg.UnixSocket
|
||||
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialHeadscaleSocket(ctx, socketPath)
|
||||
},
|
||||
}}
|
||||
|
||||
client, err := clientv2.NewClientWithResponses("http://local", clientv2.WithHTTPClient(httpClient))
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
if cfg.CLI.APIKey == "" {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
if cfg.CLI.Insecure {
|
||||
//nolint:gosec // intentionally honouring the insecure flag
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
apiKey := cfg.CLI.APIKey
|
||||
|
||||
client, err := clientv2.NewClientWithResponses(
|
||||
clientBaseURL(cfg.CLI.Address),
|
||||
clientv2.WithHTTPClient(&http.Client{Transport: transport}),
|
||||
clientv2.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
// v2Error turns a non-2xx v2 response into an error. The v2 API emits the
|
||||
// Tailscale error body ({"message":...}) rather than RFC 7807, so it reads the
|
||||
// "message" field instead of the generated problem+json types.
|
||||
func v2Error(status int, body []byte) error {
|
||||
if status >= http.StatusOK && status < http.StatusMultipleChoices {
|
||||
return nil
|
||||
}
|
||||
|
||||
var e struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if json.Unmarshal(body, &e) == nil && e.Message != "" {
|
||||
//nolint:err113 // surfacing the server's message
|
||||
return fmt.Errorf("api error (%d): %s", status, e.Message)
|
||||
}
|
||||
|
||||
//nolint:err113 // surfacing the server's body
|
||||
return fmt.Errorf("api error (%d): %s", status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
func ptrStr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return *s
|
||||
}
|
||||
|
||||
func ptrStrs(s *[]string) []string {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return *s
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
|
|
@ -14,14 +16,13 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
bypassFlag = "bypass-grpc-and-access-database-directly" //nolint:gosec // not a credential
|
||||
bypassFlag = "bypass-server-and-access-database-directly" //nolint:gosec // not a credential
|
||||
)
|
||||
|
||||
var errAborted = errors.New("command aborted by user")
|
||||
|
||||
// bypassDatabase loads the server config and opens the database directly,
|
||||
// bypassing the gRPC server. The caller is responsible for closing the
|
||||
// returned database handle.
|
||||
// bypassDatabase opens the database directly, bypassing the running server.
|
||||
// The caller must close the returned handle.
|
||||
func bypassDatabase() (*db.HSDatabase, error) {
|
||||
cfg, err := types.LoadServerConfig()
|
||||
if err != nil {
|
||||
|
|
@ -36,19 +37,29 @@ func bypassDatabase() (*db.HSDatabase, error) {
|
|||
return d, nil
|
||||
}
|
||||
|
||||
// openBypassDB confirms the destructive bypass action and opens the database
|
||||
// directly. The caller is responsible for closing the returned handle.
|
||||
func openBypassDB(cmd *cobra.Command) (*db.HSDatabase, error) {
|
||||
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
|
||||
return nil, errAborted
|
||||
}
|
||||
|
||||
return bypassDatabase()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(policyCmd)
|
||||
|
||||
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
|
||||
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
|
||||
policyCmd.AddCommand(getPolicy)
|
||||
|
||||
setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
|
||||
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
|
||||
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
|
||||
mustMarkRequired(setPolicy, "file")
|
||||
policyCmd.AddCommand(setPolicy)
|
||||
|
||||
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
|
||||
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no gRPC, no running server) to validate user@ token references and to evaluate the policy's tests block. Required when those checks are needed.")
|
||||
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no running server required) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
|
||||
mustMarkRequired(checkPolicy, "file")
|
||||
policyCmd.AddCommand(checkPolicy)
|
||||
}
|
||||
|
|
@ -61,16 +72,12 @@ var policyCmd = &cobra.Command{
|
|||
var getPolicy = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Print the current ACL Policy",
|
||||
Aliases: []string{"show", "view", "fetch"},
|
||||
Aliases: []string{cmdShow, "view", "fetch"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var policyData string
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
|
||||
return errAborted
|
||||
}
|
||||
|
||||
d, err := bypassDatabase()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -83,19 +90,23 @@ var getPolicy = &cobra.Command{
|
|||
|
||||
policyData = pol.Data
|
||||
} else {
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.GetPolicyWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading ACL policy: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading ACL policy: %w", err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
policyData = response.GetPolicy()
|
||||
policyData = resp.JSON200.Policy
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// This does not pass output format as we don't support yaml, json or
|
||||
|
|
@ -122,17 +133,13 @@ var setPolicy = &cobra.Command{
|
|||
}
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
|
||||
return errAborted
|
||||
}
|
||||
|
||||
d, err := bypassDatabase()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
users, err := d.ListUsers()
|
||||
users, err := d.ListUsers(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading users for policy validation: %w", err)
|
||||
}
|
||||
|
|
@ -147,18 +154,24 @@ var setPolicy = &cobra.Command{
|
|||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
} else {
|
||||
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
|
||||
policyStr := string(policyBytes)
|
||||
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.SetPolicyWithResponse(ctx, clientv1.SetPolicyJSONRequestBody{
|
||||
Policy: &policyStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
|
||||
_, err = client.SetPolicy(ctx, request)
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +186,8 @@ var checkPolicy = &cobra.Command{
|
|||
Short: "Check the Policy file for errors",
|
||||
Long: `
|
||||
Check validates the policy against the server's live users and nodes,
|
||||
running any "tests" block. By default the command is a thin frontend
|
||||
for a gRPC call to a running headscale; pass --` + bypassFlag + ` to
|
||||
running any "tests" or "sshTests" block. By default the command calls a
|
||||
running headscale over its API; pass --` + bypassFlag + ` to
|
||||
open the database directly when headscale is not running.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
policyPath, _ := cmd.Flags().GetString("file")
|
||||
|
|
@ -185,17 +198,13 @@ var checkPolicy = &cobra.Command{
|
|||
}
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
|
||||
return errAborted
|
||||
}
|
||||
|
||||
d, err := bypassDatabase()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
users, err := d.ListUsers()
|
||||
users, err := d.ListUsers(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading users: %w", err)
|
||||
}
|
||||
|
|
@ -205,10 +214,10 @@ var checkPolicy = &cobra.Command{
|
|||
return fmt.Errorf("loading nodes: %w", err)
|
||||
}
|
||||
|
||||
// NewPolicyManager validates structure and user references
|
||||
// [policy.NewPolicyManager] validates structure and user references
|
||||
// but intentionally skips test evaluation (boot path).
|
||||
// SetPolicy is the user-write boundary and is what runs the
|
||||
// tests block.
|
||||
// [policy.PolicyManager.SetPolicy] is the user-write boundary and is what runs the
|
||||
// tests and sshTests blocks.
|
||||
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing policy file: %w", err)
|
||||
|
|
@ -224,14 +233,22 @@ var checkPolicy = &cobra.Command{
|
|||
return nil
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
policyStr := string(policyBytes)
|
||||
|
||||
_, err = client.CheckPolicy(ctx, &v1.CheckPolicyRequest{Policy: string(policyBytes)})
|
||||
err = withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.CheckPolicyWithResponse(ctx, clientv1.CheckPolicyJSONRequestBody{
|
||||
Policy: &policyStr,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -42,57 +42,57 @@ var preauthkeysCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
var listPreAuthKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all preauthkeys",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListPreAuthKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing preauthkeys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
"Expiration",
|
||||
"Created",
|
||||
"Owner",
|
||||
},
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
for _, key := range response.GetPreAuthKeys() {
|
||||
expiration := "-"
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
preAuthKeys := resp.JSON200.PreAuthKeys
|
||||
|
||||
return printListOutput(cmd, preAuthKeys, func() error {
|
||||
rows := make([][]string, 0, len(preAuthKeys))
|
||||
for _, key := range preAuthKeys {
|
||||
expiration := ColourTime(key.Expiration)
|
||||
|
||||
owner := "-"
|
||||
|
||||
switch {
|
||||
case len(key.AclTags) > 0:
|
||||
owner = strings.Join(key.AclTags, "\n")
|
||||
case key.User.Id != "":
|
||||
owner = key.User.Name
|
||||
}
|
||||
|
||||
var owner string
|
||||
if len(key.GetAclTags()) > 0 {
|
||||
owner = strings.Join(key.GetAclTags(), "\n")
|
||||
} else if key.GetUser() != nil {
|
||||
owner = key.GetUser().GetName()
|
||||
} else {
|
||||
owner = "-"
|
||||
}
|
||||
|
||||
tableData = append(tableData, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetKey(),
|
||||
strconv.FormatBool(key.GetReusable()),
|
||||
strconv.FormatBool(key.GetEphemeral()),
|
||||
strconv.FormatBool(key.GetUsed()),
|
||||
rows = append(rows, []string{
|
||||
key.Id,
|
||||
key.Key,
|
||||
strconv.FormatBool(key.Reusable),
|
||||
strconv.FormatBool(key.Ephemeral),
|
||||
strconv.FormatBool(key.Used),
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
key.CreatedAt.Format(HeadscaleDateTimeFormat),
|
||||
owner,
|
||||
})
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
colExpiration,
|
||||
colCreated,
|
||||
"Owner",
|
||||
}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
@ -100,79 +100,103 @@ var listPreAuthKeys = &cobra.Command{
|
|||
var createPreAuthKeyCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey",
|
||||
Aliases: []string{"c", "new"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetUint64("user")
|
||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||
tags, _ := cmd.Flags().GetStringSlice("tags")
|
||||
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
expiryTime, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.CreatePreAuthKeyRequest{
|
||||
User: user,
|
||||
Reusable: reusable,
|
||||
Ephemeral: ephemeral,
|
||||
AclTags: tags,
|
||||
Expiration: expiration,
|
||||
userStr := strconv.FormatUint(user, util.Base10)
|
||||
|
||||
request := clientv1.CreatePreAuthKeyJSONRequestBody{
|
||||
User: &userStr,
|
||||
Reusable: &reusable,
|
||||
Ephemeral: &ephemeral,
|
||||
AclTags: &tags,
|
||||
Expiration: &expiryTime,
|
||||
}
|
||||
|
||||
response, err := client.CreatePreAuthKey(ctx, request)
|
||||
resp, err := client.CreatePreAuthKeyWithResponse(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
preAuthKey := resp.JSON200.PreAuthKey
|
||||
|
||||
return printOutput(cmd, preAuthKey, preAuthKey.Key)
|
||||
}),
|
||||
}
|
||||
|
||||
// preAuthKeyID reads the required --id flag for preauthkey commands.
|
||||
func preAuthKeyID(cmd *cobra.Command) (uint64, error) {
|
||||
id, _ := cmd.Flags().GetUint64("id")
|
||||
if id == 0 {
|
||||
return 0, fmt.Errorf("missing --id parameter: %w", errMissingParameter)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
var expirePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire a preauthkey",
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, _ := cmd.Flags().GetUint64("id")
|
||||
|
||||
if id == 0 {
|
||||
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.ExpirePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
|
||||
response, err := client.ExpirePreAuthKey(ctx, request)
|
||||
resp, err := client.ExpirePreAuthKeyWithResponse(ctx, clientv1.ExpirePreAuthKeyJSONRequestBody{
|
||||
Id: &idStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
var deletePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a preauthkey",
|
||||
Aliases: []string{"del", "rm", "d"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, _ := cmd.Flags().GetUint64("id")
|
||||
|
||||
if id == 0 {
|
||||
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
|
||||
Aliases: []string{aliasDel, "rm", "d"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.DeletePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
|
||||
response, err := client.DeletePreAuthKey(ctx, request)
|
||||
resp, err := client.DeletePreAuthKeyWithResponse(ctx, &clientv1.DeletePreAuthKeyParams{
|
||||
Id: &idStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cli
|
|||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
|
|
@ -30,7 +31,7 @@ func init() {
|
|||
Bool("force", false, "Disable prompts and forces the execution")
|
||||
|
||||
// Re-enable usage output only for flag-parsing errors; runtime errors
|
||||
// from RunE should never dump usage text.
|
||||
// from [cobra.Command.RunE] should never dump usage text.
|
||||
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
cmd.SilenceUsage = false
|
||||
|
||||
|
|
@ -95,13 +96,9 @@ func initConfig() {
|
|||
var prereleases = []string{"alpha", "beta", "rc", "dev"}
|
||||
|
||||
func isPreReleaseVersion(version string) bool {
|
||||
for _, unstable := range prereleases {
|
||||
if strings.Contains(version, unstable) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.ContainsFunc(prereleases, func(unstable string) bool {
|
||||
return strings.Contains(version, unstable)
|
||||
})
|
||||
}
|
||||
|
||||
// filterPreReleasesIfStable returns a function that filters out
|
||||
|
|
@ -120,13 +117,7 @@ func filterPreReleasesIfStable(versionFunc func() string) func(string) bool {
|
|||
}
|
||||
|
||||
// If we are on a stable release, filter out pre-releases.
|
||||
for _, ignore := range prereleases {
|
||||
if strings.Contains(tag, ignore) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return isPreReleaseVersion(tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,19 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
v23 = "0.23.0"
|
||||
v23Alpha1 = "0.23.0-alpha.1"
|
||||
v23Beta1 = "0.23.0-beta.1"
|
||||
v23RC1 = "0.23.0-rc.1"
|
||||
v23Dev = "0.23.0-dev"
|
||||
v231 = "0.23.1"
|
||||
|
||||
v24Alpha1Tag = "v0.24.0-alpha.1"
|
||||
v24RCTag = "v0.24.0-rc.1"
|
||||
v24Tag = "v0.24.0"
|
||||
)
|
||||
|
||||
func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -14,64 +27,64 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "stable version filters alpha tag",
|
||||
currentVersion: "0.23.0",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, alpha tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version filters beta tag",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v0.24.0-beta.2",
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, beta tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version filters rc tag",
|
||||
currentVersion: "0.23.0",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, rc tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version allows stable tag",
|
||||
currentVersion: "0.23.0",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on stable release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows alpha tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: "v0.24.0-alpha.2",
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, alpha tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows beta tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: "v0.24.0-beta.1",
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, beta tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows rc tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, rc tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows stable tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "beta version allows alpha tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, alpha tags should not be filtered",
|
||||
},
|
||||
|
|
@ -84,28 +97,28 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "beta version allows rc tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, rc tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "beta version allows stable tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "rc version allows alpha tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23RC1,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, alpha tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "rc version allows beta tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
currentVersion: v23RC1,
|
||||
tag: "v0.24.0-beta.1",
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, beta tags should not be filtered",
|
||||
|
|
@ -119,78 +132,78 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "rc version allows stable tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23RC1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version with patch filters alpha",
|
||||
currentVersion: "0.23.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v231,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: true,
|
||||
description: "Stable version with patch number should filter alpha tags",
|
||||
},
|
||||
{
|
||||
name: "stable version with patch allows stable",
|
||||
currentVersion: "0.23.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v231,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "Stable version with patch number should allow stable tags",
|
||||
},
|
||||
{
|
||||
name: "tag with alpha substring in version number",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v1.0.0-alpha.1",
|
||||
expectedFilter: true,
|
||||
description: "Tags with alpha in version string should be filtered on stable",
|
||||
},
|
||||
{
|
||||
name: "tag with beta substring in version number",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v1.0.0-beta.1",
|
||||
expectedFilter: true,
|
||||
description: "Tags with beta in version string should be filtered on stable",
|
||||
},
|
||||
{
|
||||
name: "tag with rc substring in version number",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v1.0.0-rc.1",
|
||||
expectedFilter: true,
|
||||
description: "Tags with rc in version string should be filtered on stable",
|
||||
},
|
||||
{
|
||||
name: "empty tag on stable version",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "",
|
||||
expectedFilter: false,
|
||||
description: "Empty tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "dev version allows all tags",
|
||||
currentVersion: "0.23.0-dev",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23Dev,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "Dev versions should not filter any tags (pre-release allows all)",
|
||||
},
|
||||
{
|
||||
name: "stable version filters dev tag",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v0.24.0-dev",
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, dev tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "dev version allows dev tag",
|
||||
currentVersion: "0.23.0-dev",
|
||||
currentVersion: v23Dev,
|
||||
tag: "v0.24.0-dev.1",
|
||||
expectedFilter: false,
|
||||
description: "When on dev release, dev tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "dev version allows stable tag",
|
||||
currentVersion: "0.23.0-dev",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Dev,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on dev release, stable tags should not be filtered",
|
||||
},
|
||||
|
|
@ -200,7 +213,8 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
|
||||
if result != tt.expectedFilter {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expectedFilter,
|
||||
|
|
@ -222,25 +236,25 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "stable version",
|
||||
version: "0.23.0",
|
||||
version: v23,
|
||||
expected: false,
|
||||
description: "Stable version should not be pre-release",
|
||||
},
|
||||
{
|
||||
name: "alpha version",
|
||||
version: "0.23.0-alpha.1",
|
||||
version: v23Alpha1,
|
||||
expected: true,
|
||||
description: "Alpha version should be pre-release",
|
||||
},
|
||||
{
|
||||
name: "beta version",
|
||||
version: "0.23.0-beta.1",
|
||||
version: v23Beta1,
|
||||
expected: true,
|
||||
description: "Beta version should be pre-release",
|
||||
},
|
||||
{
|
||||
name: "rc version",
|
||||
version: "0.23.0-rc.1",
|
||||
version: v23RC1,
|
||||
expected: true,
|
||||
description: "RC version should be pre-release",
|
||||
},
|
||||
|
|
@ -258,7 +272,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "dev version",
|
||||
version: "0.23.0-dev",
|
||||
version: v23Dev,
|
||||
expected: true,
|
||||
description: "Dev version should be pre-release",
|
||||
},
|
||||
|
|
@ -270,7 +284,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "version with patch number",
|
||||
version: "0.23.1",
|
||||
version: v231,
|
||||
expected: false,
|
||||
description: "Stable version with patch should not be pre-release",
|
||||
},
|
||||
|
|
@ -280,7 +294,8 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isPreReleaseVersion(tt.version)
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expected,
|
||||
|
|
|
|||
23
cmd/headscale/cli/strings.go
Normal file
23
cmd/headscale/cli/strings.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package cli
|
||||
|
||||
// Shared CLI vocabulary used across multiple command definitions in this
|
||||
// package. Centralising the strings prevents goconst drift and ensures a
|
||||
// typo in a subcommand name fails to compile rather than silently
|
||||
// breaking the binding.
|
||||
const (
|
||||
// Subcommand verbs (cobra Use field).
|
||||
cmdList = "list"
|
||||
cmdShow = "show"
|
||||
cmdNew = "new"
|
||||
cmdDelete = "delete"
|
||||
cmdExpire = "expire"
|
||||
|
||||
// Subcommand aliases.
|
||||
aliasDel = "del"
|
||||
aliasExp = "exp"
|
||||
|
||||
// Output table column headers and printOutput map keys.
|
||||
colResult = "Result"
|
||||
colCreated = "Created"
|
||||
colExpiration = "Expiration"
|
||||
)
|
||||
|
|
@ -4,13 +4,13 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -37,13 +37,50 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
|
|||
|
||||
// Normalise unset/negative identifiers to 0 so the uint64
|
||||
// conversion does not produce a bogus large value.
|
||||
if identifier < 0 {
|
||||
identifier = 0
|
||||
}
|
||||
identifier = max(identifier, 0)
|
||||
|
||||
return uint64(identifier), username, nil //nolint:gosec // identifier is clamped to >= 0 above
|
||||
}
|
||||
|
||||
// resolveSingleUser resolves exactly one user from the --name/--id flags,
|
||||
// returning the raw flag id and the matched user.
|
||||
func resolveSingleUser(
|
||||
ctx context.Context,
|
||||
client *clientv1.ClientWithResponses,
|
||||
cmd *cobra.Command,
|
||||
) (uint64, *clientv1.User, error) {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
params := &clientv1.ListUsersParams{}
|
||||
if username != "" {
|
||||
params.Name = &username
|
||||
}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
}
|
||||
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return 0, nil, apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
if len(users) != 1 {
|
||||
return 0, nil, errMultipleUsersMatch
|
||||
}
|
||||
|
||||
return id, &users[0], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(userCmd)
|
||||
userCmd.AddCommand(createUserCmd)
|
||||
|
|
@ -70,7 +107,7 @@ var userCmd = &cobra.Command{
|
|||
var createUserCmd = &cobra.Command{
|
||||
Use: "create NAME",
|
||||
Short: "Creates a new user",
|
||||
Aliases: []string{"c", "new"},
|
||||
Aliases: []string{"c", cmdNew},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errMissingParameter
|
||||
|
|
@ -78,19 +115,19 @@ var createUserCmd = &cobra.Command{
|
|||
|
||||
return nil
|
||||
},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
userName := args[0]
|
||||
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained API client")
|
||||
|
||||
request := &v1.CreateUserRequest{Name: userName}
|
||||
request := clientv1.CreateUserJSONRequestBody{Name: &userName}
|
||||
|
||||
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
|
||||
request.DisplayName = displayName
|
||||
request.DisplayName = &displayName
|
||||
}
|
||||
|
||||
if email, _ := cmd.Flags().GetString("email"); email != "" {
|
||||
request.Email = email
|
||||
request.Email = &email
|
||||
}
|
||||
|
||||
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||
|
|
@ -98,70 +135,60 @@ var createUserCmd = &cobra.Command{
|
|||
return fmt.Errorf("invalid picture URL: %w", err)
|
||||
}
|
||||
|
||||
request.PictureUrl = pictureURL
|
||||
request.PictureUrl = &pictureURL
|
||||
}
|
||||
|
||||
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
|
||||
|
||||
response, err := client.CreateUser(ctx, request)
|
||||
resp, err := client.CreateUserWithResponse(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User created")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User created")
|
||||
}),
|
||||
}
|
||||
|
||||
var destroyUserCmd = &cobra.Command{
|
||||
Use: "destroy --identifier ID or --name NAME",
|
||||
Short: "Destroys a user",
|
||||
Aliases: []string{"delete"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
Aliases: []string{cmdDelete},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
_, user, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.ListUsersRequest{
|
||||
Name: username,
|
||||
Id: id,
|
||||
}
|
||||
|
||||
users, err := client.ListUsers(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if len(users.GetUsers()) != 1 {
|
||||
return errMultipleUsersMatch
|
||||
}
|
||||
|
||||
user := users.GetUsers()[0]
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
||||
user.GetName(), user.GetId(),
|
||||
"Do you want to remove the user %q (%s) and any associated preauthkeys?",
|
||||
user.Name, user.Id,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
|
||||
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
|
||||
|
||||
response, err := client.DeleteUser(ctx, deleteRequest)
|
||||
resp, err := client.DeleteUserWithResponse(ctx, user.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("destroying user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "User destroyed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "User destroyed")
|
||||
}),
|
||||
}
|
||||
|
||||
var listUsersCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all the users",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
request := &v1.ListUsersRequest{}
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
params := &clientv1.ListUsersParams{}
|
||||
|
||||
id, _ := cmd.Flags().GetInt64("identifier")
|
||||
username, _ := cmd.Flags().GetString("name")
|
||||
|
|
@ -170,34 +197,41 @@ var listUsersCmd = &cobra.Command{
|
|||
// filter by one param at most
|
||||
switch {
|
||||
case id > 0:
|
||||
request.Id = uint64(id)
|
||||
idStr := strconv.FormatInt(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
case username != "":
|
||||
request.Name = username
|
||||
params.Name = &username
|
||||
case email != "":
|
||||
request.Email = email
|
||||
params.Email = &email
|
||||
}
|
||||
|
||||
response, err := client.ListUsers(ctx, request)
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
||||
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
|
||||
for _, user := range response.GetUsers() {
|
||||
tableData = append(
|
||||
tableData,
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
|
||||
return printListOutput(cmd, users, func() error {
|
||||
rows := make([][]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
rows = append(
|
||||
rows,
|
||||
[]string{
|
||||
strconv.FormatUint(user.GetId(), util.Base10),
|
||||
user.GetDisplayName(),
|
||||
user.GetName(),
|
||||
user.GetEmail(),
|
||||
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
user.Id,
|
||||
user.DisplayName,
|
||||
user.Name,
|
||||
user.Email,
|
||||
user.CreatedAt.Format(HeadscaleDateTimeFormat),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{"ID", "Name", "Username", "Email", colCreated}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
@ -206,38 +240,23 @@ var renameUserCmd = &cobra.Command{
|
|||
Use: "rename",
|
||||
Short: "Renames a user",
|
||||
Aliases: []string{"mv"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, _, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listReq := &v1.ListUsersRequest{
|
||||
Name: username,
|
||||
Id: id,
|
||||
}
|
||||
|
||||
users, err := client.ListUsers(ctx, listReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if len(users.GetUsers()) != 1 {
|
||||
return errMultipleUsersMatch
|
||||
}
|
||||
|
||||
newName, _ := cmd.Flags().GetString("new-name")
|
||||
|
||||
renameReq := &v1.RenameUserRequest{
|
||||
OldId: id,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameUser(ctx, renameReq)
|
||||
resp, err := client.RenameUserWithResponse(ctx, strconv.FormatUint(id, util.Base10), newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User renamed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User renamed")
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,23 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -36,11 +38,45 @@ const (
|
|||
var (
|
||||
errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set")
|
||||
errMissingParameter = errors.New("missing parameters")
|
||||
errResponseStatus = errors.New("unexpected response status")
|
||||
)
|
||||
|
||||
// mustMarkRequired marks the named flags as required on cmd, panicking
|
||||
// if any name does not match a registered flag. This is only called
|
||||
// from init() where a failure indicates a programming error.
|
||||
// apiError turns a non-2xx response into an error, surfacing the server's
|
||||
// RFC7807 problem detail. detail holds the operation context and errors[] the
|
||||
// wrapped cause (e.g. "name is too long"); both are joined so the server's
|
||||
// message text is not lost.
|
||||
func apiError(statusCode int, problem *clientv1.ErrorModel) error {
|
||||
if problem == nil {
|
||||
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
parts := make([]string, 0, 2)
|
||||
|
||||
if problem.Detail != nil && *problem.Detail != "" {
|
||||
parts = append(parts, *problem.Detail)
|
||||
}
|
||||
|
||||
if problem.Errors != nil {
|
||||
for _, e := range *problem.Errors {
|
||||
if e.Message != nil && *e.Message != "" {
|
||||
parts = append(parts, *e.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 && problem.Title != nil && *problem.Title != "" {
|
||||
parts = append(parts, *problem.Title)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", errResponseStatus, strings.Join(parts, ": "))
|
||||
}
|
||||
|
||||
// mustMarkRequired marks the named flags as required, panicking on an unknown
|
||||
// flag. Only called from init(), where a failure is a programming error.
|
||||
func mustMarkRequired(cmd *cobra.Command, names ...string) {
|
||||
for _, n := range names {
|
||||
err := cmd.MarkFlagRequired(n)
|
||||
|
|
@ -67,28 +103,47 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
|||
return app, nil
|
||||
}
|
||||
|
||||
// grpcRunE wraps a cobra RunE func, injecting a ready gRPC client and
|
||||
// context. Connection lifecycle is managed by the wrapper — callers
|
||||
// never see the underlying conn or cancel func.
|
||||
func grpcRunE(
|
||||
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
|
||||
// clientRunE wraps a [cobra.Command.RunE] func, injecting a ready API client
|
||||
// and a context whose timeout/cancel the wrapper owns.
|
||||
func clientRunE(
|
||||
fn func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error,
|
||||
) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
return fn(ctx, client, cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) {
|
||||
// withClient runs fn with an API client. For commands that branch on a flag
|
||||
// before talking to the server, where clientRunE's whole-RunE wrapping does
|
||||
// not fit.
|
||||
func withClient(
|
||||
fn func(ctx context.Context, client *clientv1.ClientWithResponses) error,
|
||||
) error {
|
||||
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, client)
|
||||
}
|
||||
|
||||
// newHeadscaleCLIWithConfig builds an HTTP client for the Headscale v1 API.
|
||||
//
|
||||
// When cfg.CLI.Address is unset the CLI is assumed to run on the server host
|
||||
// and talks to the unix socket over HTTP without authentication (local trust).
|
||||
// Otherwise it talks to the remote TCP address over HTTPS and injects the
|
||||
// configured API key as a bearer token.
|
||||
func newHeadscaleCLIWithConfig() (context.Context, *clientv1.ClientWithResponses, context.CancelFunc, error) {
|
||||
cfg, err := types.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
|
|
@ -97,89 +152,131 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
|||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
||||
|
||||
grpcOptions := []grpc.DialOption{
|
||||
grpc.WithBlock(), //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
}
|
||||
|
||||
address := cfg.CLI.Address
|
||||
|
||||
// If the address is not set, we assume that we are on the server hosting hscontrol.
|
||||
// If the address is not set, we assume that we are on the server hosting [hscontrol].
|
||||
if address == "" {
|
||||
log.Debug().
|
||||
Str("socket", cfg.UnixSocket).
|
||||
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
|
||||
|
||||
address = cfg.UnixSocket
|
||||
|
||||
// Try to give the user better feedback if we cannot write to the headscale
|
||||
// socket. Note: os.OpenFile on a Unix domain socket returns ENXIO on
|
||||
// Linux which is expected — only permission errors are actionable here.
|
||||
// The actual gRPC connection uses net.Dial which handles sockets properly.
|
||||
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
client, err := newSocketClient(cfg.UnixSocket)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, fmt.Errorf(
|
||||
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
|
||||
cfg.UnixSocket,
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
socket.Close()
|
||||
}
|
||||
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(util.GrpcSocketDialer),
|
||||
)
|
||||
} else {
|
||||
// If we are not connecting to a local server, require an API key for authentication
|
||||
apiKey := cfg.CLI.APIKey
|
||||
if apiKey == "" {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, errAPIKeyNotSet
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
grpcOptions = append(grpcOptions,
|
||||
grpc.WithPerRPCCredentials(tokenAuth{
|
||||
token: apiKey,
|
||||
}),
|
||||
)
|
||||
log.Trace().Caller().Str(zf.Address, cfg.UnixSocket).Msg("connecting via unix socket")
|
||||
|
||||
if cfg.CLI.Insecure {
|
||||
tlsConfig := &tls.Config{
|
||||
// turn of gosec as we are intentionally setting
|
||||
// insecure.
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
grpcOptions = append(grpcOptions,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
)
|
||||
} else {
|
||||
grpcOptions = append(grpcOptions,
|
||||
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
|
||||
)
|
||||
}
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via gRPC")
|
||||
// Remote connections require an API key for authentication.
|
||||
apiKey := cfg.CLI.APIKey
|
||||
if apiKey == "" {
|
||||
cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
return nil, nil, nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
client, err := newRemoteClient(address, apiKey, cfg.CLI.Insecure)
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
client := v1.NewHeadscaleServiceClient(conn)
|
||||
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via HTTPS")
|
||||
|
||||
return ctx, client, conn, cancel, nil
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
// newSocketClient builds an API client that dials the local unix socket. The
|
||||
// base-URL host is irrelevant; the custom dialer routes every request to the
|
||||
// socket.
|
||||
func newSocketClient(socketPath string) (*clientv1.ClientWithResponses, error) {
|
||||
// Probe for a clearer permission error up front. [os.OpenFile] on a unix
|
||||
// socket returns ENXIO on Linux (expected); only permission errors are
|
||||
// actionable. The real connection goes through [net.Dial].
|
||||
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return nil, fmt.Errorf(
|
||||
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
|
||||
socketPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
socket.Close()
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialHeadscaleSocket(ctx, socketPath)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return clientv1.NewClientWithResponses(
|
||||
"http://local",
|
||||
clientv1.WithHTTPClient(httpClient),
|
||||
)
|
||||
}
|
||||
|
||||
// dialHeadscaleSocket connects to the unix socket, retrying until it appears or
|
||||
// ctx (the CLI timeout) expires. The socket is created late in startup (after
|
||||
// noise key, database, migrations), so a command run right after the server
|
||||
// starts can race its creation; retrying preserves the old gRPC client's
|
||||
// blocking-dial tolerance rather than failing on a not-yet-present socket.
|
||||
func dialHeadscaleSocket(ctx context.Context, socketPath string) (net.Conn, error) {
|
||||
b := backoff.NewExponentialBackOff()
|
||||
b.InitialInterval = 50 * time.Millisecond
|
||||
b.MaxInterval = 1 * time.Second
|
||||
|
||||
return backoff.Retry(ctx, func() (net.Conn, error) {
|
||||
return util.SocketDialer(ctx, socketPath)
|
||||
}, backoff.WithBackOff(b))
|
||||
}
|
||||
|
||||
// clientBaseURL turns a configured CLI address into a client base URL. A bare
|
||||
// host[:port] (the historical form) defaults to https; an address that already
|
||||
// carries a scheme is used as-is, so an explicit http:// or https:// is honoured
|
||||
// rather than doubled into https://https://...
|
||||
func clientBaseURL(address string) string {
|
||||
if strings.Contains(address, "://") {
|
||||
return address
|
||||
}
|
||||
|
||||
return "https://" + address
|
||||
}
|
||||
|
||||
// newRemoteClient builds an API client for a remote Headscale over HTTPS,
|
||||
// honouring insecure (skip TLS verification) and injecting the API key as a
|
||||
// bearer token on every request.
|
||||
func newRemoteClient(address, apiKey string, insecure bool) (*clientv1.ClientWithResponses, error) {
|
||||
transport := &http.Transport{}
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
// turn off gosec as we are intentionally setting insecure.
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
|
||||
return clientv1.NewClientWithResponses(
|
||||
clientBaseURL(address),
|
||||
clientv1.WithHTTPClient(httpClient),
|
||||
clientv1.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// formatOutput serialises result into the requested format. For the
|
||||
|
|
@ -228,16 +325,16 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
|
|||
}
|
||||
|
||||
// expirationFromFlag parses the --expiration flag as a Prometheus-style
|
||||
// duration (e.g. "90d", "1h") and returns an absolute timestamp.
|
||||
func expirationFromFlag(cmd *cobra.Command) (*timestamppb.Timestamp, error) {
|
||||
// duration (e.g. "90d", "1h") and returns an absolute time.
|
||||
func expirationFromFlag(cmd *cobra.Command) (time.Time, error) {
|
||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||
|
||||
duration, err := model.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing duration: %w", err)
|
||||
return time.Time{}, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return timestamppb.New(time.Now().UTC().Add(time.Duration(duration))), nil
|
||||
return time.Now().UTC().Add(time.Duration(duration)), nil
|
||||
}
|
||||
|
||||
// confirmAction returns true when the user confirms a prompt, or when
|
||||
|
|
@ -251,9 +348,19 @@ func confirmAction(cmd *cobra.Command, prompt string) bool {
|
|||
return util.YesNo(prompt)
|
||||
}
|
||||
|
||||
// renderTable prints a human-readable pterm table with the given header row
|
||||
// and data rows, using the shared header styling.
|
||||
func renderTable(header []string, rows [][]string) error {
|
||||
tableData := make(pterm.TableData, 0, 1+len(rows))
|
||||
tableData = append(tableData, header)
|
||||
tableData = append(tableData, rows...)
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
}
|
||||
|
||||
// printListOutput checks the --output flag: when a machine-readable format is
|
||||
// requested it serialises data as JSON/YAML; otherwise it calls renderTable
|
||||
// to produce the human-readable pterm table.
|
||||
// requested it serialises data as JSON/YAML; otherwise it calls the render
|
||||
// callback to produce the human-readable pterm table.
|
||||
func printListOutput(
|
||||
cmd *cobra.Command,
|
||||
data any,
|
||||
|
|
@ -269,56 +376,25 @@ func printListOutput(
|
|||
|
||||
// printError writes err to stderr, formatting it as JSON/YAML when the
|
||||
// --output flag requests machine-readable output. Used exclusively by
|
||||
// Execute() so that every error surfaces in the format the caller asked for.
|
||||
// [Execute] so that every error surfaces in the format the caller asked for.
|
||||
func printError(err error, outputFormat string) {
|
||||
type errOutput struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
e := errOutput{Error: err.Error()}
|
||||
|
||||
var formatted []byte
|
||||
|
||||
switch outputFormat {
|
||||
case outputFormatJSON:
|
||||
formatted, _ = json.MarshalIndent(e, "", "\t") //nolint:errchkjson // errOutput contains only a string field
|
||||
case outputFormatJSONLine:
|
||||
formatted, _ = json.Marshal(e) //nolint:errchkjson // errOutput contains only a string field
|
||||
case outputFormatYAML:
|
||||
formatted, _ = yaml.Marshal(e)
|
||||
default:
|
||||
if outputFormat == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s\n", formatted)
|
||||
// formatOutput cannot fail here: errOutput is a single string field.
|
||||
out, _ := formatOutput(errOutput{Error: err.Error()}, "", outputFormat)
|
||||
fmt.Fprintf(os.Stderr, "%s\n", out)
|
||||
}
|
||||
|
||||
func hasMachineOutputFlag() bool {
|
||||
for _, arg := range os.Args {
|
||||
if arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type tokenAuth struct {
|
||||
token string
|
||||
}
|
||||
|
||||
// Return value is mapped to request headers.
|
||||
func (t tokenAuth) GetRequestMetadata(
|
||||
ctx context.Context,
|
||||
in ...string,
|
||||
) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + t.token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tokenAuth) RequireTransportSecurity() bool {
|
||||
return true
|
||||
return slices.ContainsFunc(os.Args, func(arg string) bool {
|
||||
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
|
||||
})
|
||||
}
|
||||
|
|
|
|||
72
cmd/headscale/cli/utils_socket_test.go
Normal file
72
cmd/headscale/cli/utils_socket_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDialHeadscaleSocketRetriesUntilPresent proves the CLI socket dialer
|
||||
// tolerates a not-yet-created socket (the server-still-starting race) by
|
||||
// retrying until it appears, rather than failing immediately like a bare dial.
|
||||
func TestDialHeadscaleSocketRetriesUntilPresent(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "headscale.sock")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
done := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := dialHeadscaleSocket(ctx, sock)
|
||||
done <- result{conn, err}
|
||||
}()
|
||||
|
||||
// Listen only after the dialer has begun, so its backoff must retry the
|
||||
// absent socket and connect once it exists.
|
||||
var lc net.ListenConfig
|
||||
|
||||
ln, err := lc.Listen(ctx, "unix", sock)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
if conn, _ := ln.Accept(); conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
res := <-done
|
||||
require.NoError(t, res.err)
|
||||
require.NotNil(t, res.conn)
|
||||
|
||||
res.conn.Close()
|
||||
}
|
||||
|
||||
// TestDialHeadscaleSocketRespectsDeadline proves the retry is bounded by the
|
||||
// context: when the socket never appears, the dialer returns an error around the
|
||||
// deadline instead of hanging.
|
||||
func TestDialHeadscaleSocketRespectsDeadline(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "absent.sock")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
conn, err := dialHeadscaleSocket(ctx, sock)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, conn)
|
||||
assert.Less(t, time.Since(start), 5*time.Second, "should stop near the deadline, not hang")
|
||||
}
|
||||
35
cmd/headscale/cli/utils_test.go
Normal file
35
cmd/headscale/cli/utils_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClientBaseURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare host defaults to https",
|
||||
address: "headscale.example.com:50443",
|
||||
want: "https://headscale.example.com:50443",
|
||||
},
|
||||
{
|
||||
name: "explicit https scheme is kept",
|
||||
address: "https://headscale.example.com",
|
||||
want: "https://headscale.example.com",
|
||||
},
|
||||
{
|
||||
name: "explicit http scheme is kept",
|
||||
address: "http://localhost:8080",
|
||||
want: "http://localhost:8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := clientBaseURL(tt.address); got != tt.want {
|
||||
t.Errorf("clientBaseURL(%q) = %q, want %q", tt.address, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -11,21 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
var colors bool
|
||||
|
||||
switch l := termcolor.SupportLevel(os.Stderr); l {
|
||||
case termcolor.Level16M:
|
||||
colors = true
|
||||
case termcolor.Level256:
|
||||
colors = true
|
||||
case termcolor.LevelBasic:
|
||||
colors = true
|
||||
case termcolor.LevelNone:
|
||||
colors = false
|
||||
default:
|
||||
// no color, return text as is.
|
||||
colors = false
|
||||
}
|
||||
colors := termcolor.SupportLevel(os.Stderr) != termcolor.LevelNone
|
||||
|
||||
// Adhere to no-color.org manifesto of allowing users to
|
||||
// turn off color in cli/services
|
||||
|
|
|
|||
|
|
@ -71,26 +71,8 @@ func killTestContainers(ctx context.Context) error {
|
|||
removed := 0
|
||||
|
||||
for _, cont := range containers {
|
||||
shouldRemove := false
|
||||
|
||||
for _, name := range cont.Names {
|
||||
if strings.Contains(name, "headscale-test-suite") ||
|
||||
strings.Contains(name, "hs-") ||
|
||||
strings.Contains(name, "ts-") ||
|
||||
strings.Contains(name, "derp-") {
|
||||
shouldRemove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if isTestContainerName(cont.Names) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
|
@ -129,13 +111,7 @@ func killTestContainersByRunID(ctx context.Context, runID string) error {
|
|||
removed := 0
|
||||
|
||||
for _, cont := range containers {
|
||||
// Kill the container if it's running
|
||||
if cont.State == "running" {
|
||||
_ = cli.ContainerKill(ctx, cont.ID, "KILL")
|
||||
}
|
||||
|
||||
// Remove the container with retry logic
|
||||
if removeContainerWithRetry(ctx, cli, cont.ID) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
|
@ -173,20 +149,8 @@ func cleanupStaleTestContainers(ctx context.Context) error {
|
|||
|
||||
for _, cont := range containers {
|
||||
// Only remove containers that look like test containers
|
||||
shouldRemove := false
|
||||
|
||||
for _, name := range cont.Names {
|
||||
if strings.Contains(name, "headscale-test-suite") ||
|
||||
strings.Contains(name, "hs-") ||
|
||||
strings.Contains(name, "ts-") ||
|
||||
strings.Contains(name, "derp-") {
|
||||
shouldRemove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
if removeContainerWithRetry(ctx, cli, cont.ID) {
|
||||
if isTestContainerName(cont.Names) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
|
@ -223,6 +187,46 @@ func removeContainerWithRetry(ctx context.Context, cli *client.Client, container
|
|||
return err == nil
|
||||
}
|
||||
|
||||
// testContainerNamePrefixes are the name prefixes used by containers that the
|
||||
// integration test harness creates (headscale, tailscale, DERP, and k3s).
|
||||
var testContainerNamePrefixes = []string{"hs-", "ts-", "derp-", "k3s-"}
|
||||
|
||||
// matchesTestContainerPrefix reports whether name belongs to an integration
|
||||
// test container, ignoring any leading "/" that Docker prefixes names with.
|
||||
func matchesTestContainerPrefix(name string) bool {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
for _, prefix := range testContainerNamePrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isTestContainerName reports whether any of the container names belong to an
|
||||
// integration test container.
|
||||
func isTestContainerName(names []string) bool {
|
||||
for _, name := range names {
|
||||
if strings.Contains(name, "headscale-test-suite") ||
|
||||
matchesTestContainerPrefix(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// killAndRemove kills a running container then removes it with retry logic,
|
||||
// reporting whether the removal succeeded.
|
||||
func killAndRemove(ctx context.Context, cli *client.Client, cont container.Summary) bool {
|
||||
if cont.State == "running" {
|
||||
_ = cli.ContainerKill(ctx, cont.ID, "KILL")
|
||||
}
|
||||
|
||||
return removeContainerWithRetry(ctx, cli, cont.ID)
|
||||
}
|
||||
|
||||
// pruneDockerNetworks removes unused Docker networks.
|
||||
func pruneDockerNetworks(ctx context.Context) error {
|
||||
cli, err := createDockerClient(ctx)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
|
|
@ -522,9 +523,9 @@ func checkImageAvailableLocally(ctx context.Context, cli *client.Client, imageNa
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// ensureImageAvailable checks if the image is available locally first, then pulls if needed.
|
||||
// ensureImageAvailable pulls imageName if missing, using Docker Hub
|
||||
// credentials and retrying transient errors.
|
||||
func ensureImageAvailable(ctx context.Context, cli *client.Client, imageName string, verbose bool) error {
|
||||
// First check if image is available locally
|
||||
available, err := checkImageAvailableLocally(ctx, cli, imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking local image availability: %w", err)
|
||||
|
|
@ -538,34 +539,64 @@ func ensureImageAvailable(ctx context.Context, cli *client.Client, imageName str
|
|||
return nil
|
||||
}
|
||||
|
||||
// Image not available locally, try to pull it
|
||||
if verbose {
|
||||
log.Printf("Image %s not found locally, pulling...", imageName)
|
||||
}
|
||||
|
||||
reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{})
|
||||
registryAuth, err := dockertestutil.RegistryAuth()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pulling image %s: %w", imageName, err)
|
||||
return fmt.Errorf("resolving registry auth: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if verbose {
|
||||
_, err = io.Copy(os.Stdout, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading pull output: %w", err)
|
||||
}
|
||||
} else {
|
||||
_, err = io.Copy(io.Discard, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading pull output: %w", err)
|
||||
}
|
||||
_, err = backoff.Retry(
|
||||
ctx,
|
||||
func() (struct{}, error) {
|
||||
reader, pullErr := cli.ImagePull(ctx, imageName, image.PullOptions{RegistryAuth: registryAuth})
|
||||
if pullErr != nil {
|
||||
if isPermanentDockerPullError(pullErr) {
|
||||
return struct{}{}, backoff.Permanent(pullErr)
|
||||
}
|
||||
|
||||
return struct{}{}, fmt.Errorf("pulling image %s: %w", imageName, pullErr)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
sink := io.Discard
|
||||
if verbose {
|
||||
sink = os.Stdout
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(sink, reader)
|
||||
if copyErr != nil {
|
||||
return struct{}{}, fmt.Errorf("reading pull output: %w", copyErr)
|
||||
}
|
||||
|
||||
return struct{}{}, nil
|
||||
},
|
||||
backoff.WithBackOff(backoff.NewExponentialBackOff()),
|
||||
backoff.WithMaxElapsedTime(60*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
log.Printf("Image %s pulled successfully", imageName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPermanentDockerPullError(err error) bool {
|
||||
msg := strings.ToLower(err.Error())
|
||||
|
||||
return strings.Contains(msg, "manifest unknown") ||
|
||||
strings.Contains(msg, "manifest not found") ||
|
||||
strings.Contains(msg, "repository does not exist") ||
|
||||
strings.Contains(msg, "name unknown") ||
|
||||
strings.Contains(msg, "no such image")
|
||||
}
|
||||
|
||||
// listControlFiles displays the headscale test artifacts created in the control logs directory.
|
||||
func listControlFiles(logsDir string) {
|
||||
entries, err := os.ReadDir(logsDir)
|
||||
|
|
@ -704,7 +735,7 @@ func getCurrentTestContainers(containers []container.Summary, testContainerID st
|
|||
for _, cont := range containers {
|
||||
for _, name := range cont.Names {
|
||||
containerName := strings.TrimPrefix(name, "/")
|
||||
if strings.HasPrefix(containerName, "hs-") || strings.HasPrefix(containerName, "ts-") {
|
||||
if matchesTestContainerPrefix(containerName) {
|
||||
// Check if container has matching run ID label
|
||||
if cont.Labels != nil && cont.Labels["hi.run-id"] == runID {
|
||||
testRunContainers = append(testRunContainers, testContainer{
|
||||
|
|
@ -738,17 +769,6 @@ func extractContainerArtifacts(ctx context.Context, cli *client.Client, containe
|
|||
return fmt.Errorf("extracting logs: %w", err)
|
||||
}
|
||||
|
||||
// Extract tar files for headscale containers only
|
||||
if strings.HasPrefix(containerName, "hs-") {
|
||||
err := extractContainerFiles(ctx, cli, containerID, containerName, logsDir, verbose)
|
||||
if 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
|
||||
}
|
||||
|
||||
|
|
@ -796,12 +816,3 @@ func extractContainerLogs(ctx context.Context, cli *client.Client, containerID,
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
354
cmd/hi/doctor.go
354
cmd/hi/doctor.go
|
|
@ -5,8 +5,25 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||
"github.com/juanfont/headscale/integration/k3sic"
|
||||
)
|
||||
|
||||
const (
|
||||
statusPass = "PASS"
|
||||
statusFail = "FAIL"
|
||||
statusWarn = "WARN"
|
||||
|
||||
nameDockerDaemon = "Docker Daemon"
|
||||
nameDockerContext = "Docker Context"
|
||||
nameDockerSocket = "Docker Socket"
|
||||
nameGolangImage = "Golang Image"
|
||||
nameK3sImage = "K3s Image"
|
||||
nameGoInstall = "Go Installation"
|
||||
)
|
||||
|
||||
var ErrSystemChecksFailed = errors.New("system checks failed")
|
||||
|
|
@ -19,6 +36,21 @@ type DoctorResult struct {
|
|||
Suggestions []string
|
||||
}
|
||||
|
||||
// pass builds a passing DoctorResult.
|
||||
func pass(name, message string) DoctorResult {
|
||||
return DoctorResult{Name: name, Status: statusPass, Message: message}
|
||||
}
|
||||
|
||||
// warn builds a warning DoctorResult with optional suggestions.
|
||||
func warn(name, message string, suggestions ...string) DoctorResult {
|
||||
return DoctorResult{Name: name, Status: statusWarn, Message: message, Suggestions: suggestions}
|
||||
}
|
||||
|
||||
// fail builds a failing DoctorResult with optional suggestions.
|
||||
func fail(name, message string, suggestions ...string) DoctorResult {
|
||||
return DoctorResult{Name: name, Status: statusFail, Message: message, Suggestions: suggestions}
|
||||
}
|
||||
|
||||
// runDoctorCheck performs comprehensive pre-flight checks for integration testing.
|
||||
func runDoctorCheck(ctx context.Context) error {
|
||||
results := []DoctorResult{}
|
||||
|
|
@ -31,10 +63,12 @@ func runDoctorCheck(ctx context.Context) error {
|
|||
results = append(results, dockerResult)
|
||||
|
||||
// If Docker is available, run additional checks
|
||||
if dockerResult.Status == "PASS" {
|
||||
if dockerResult.Status == statusPass {
|
||||
results = append(results, checkDockerContext(ctx))
|
||||
results = append(results, checkDockerSocket(ctx))
|
||||
results = append(results, checkDockerHubCredentials())
|
||||
results = append(results, checkGolangImage(ctx))
|
||||
results = append(results, checkK3sImage(ctx))
|
||||
}
|
||||
|
||||
// Check 3: Go installation
|
||||
|
|
@ -51,7 +85,7 @@ func runDoctorCheck(ctx context.Context) error {
|
|||
|
||||
// Return error if any critical checks failed
|
||||
for _, result := range results {
|
||||
if result.Status == "FAIL" {
|
||||
if result.Status == statusFail {
|
||||
return fmt.Errorf("%w - see details above", ErrSystemChecksFailed)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,140 +99,115 @@ func runDoctorCheck(ctx context.Context) error {
|
|||
func checkDockerBinary() DoctorResult {
|
||||
_, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Binary",
|
||||
Status: "FAIL",
|
||||
Message: "Docker binary not found in PATH",
|
||||
Suggestions: []string{
|
||||
"Install Docker: https://docs.docker.com/get-docker/",
|
||||
"For macOS: consider using colima or Docker Desktop",
|
||||
"Ensure docker is in your PATH",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
"Docker Binary",
|
||||
"Docker binary not found in PATH",
|
||||
"Install Docker: https://docs.docker.com/get-docker/",
|
||||
"For macOS: consider using colima or Docker Desktop",
|
||||
"Ensure docker is in your PATH",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Binary",
|
||||
Status: "PASS",
|
||||
Message: "Docker binary found",
|
||||
}
|
||||
return pass("Docker Binary", "Docker binary found")
|
||||
}
|
||||
|
||||
// checkDockerDaemon verifies Docker daemon is running and accessible.
|
||||
func checkDockerDaemon(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Daemon",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot create Docker client: %v", err),
|
||||
Suggestions: []string{
|
||||
"Start Docker daemon/service",
|
||||
"Check Docker Desktop is running (if using Docker Desktop)",
|
||||
"For colima: run 'colima start'",
|
||||
"Verify DOCKER_HOST environment variable if set",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameDockerDaemon,
|
||||
fmt.Sprintf("Cannot create Docker client: %v", err),
|
||||
"Start Docker daemon/service",
|
||||
"Check Docker Desktop is running (if using Docker Desktop)",
|
||||
"For colima: run 'colima start'",
|
||||
"Verify DOCKER_HOST environment variable if set",
|
||||
)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Daemon",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot ping Docker daemon: %v", err),
|
||||
Suggestions: []string{
|
||||
"Ensure Docker daemon is running",
|
||||
"Check Docker socket permissions",
|
||||
"Try: docker info",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameDockerDaemon,
|
||||
fmt.Sprintf("Cannot ping Docker daemon: %v", err),
|
||||
"Ensure Docker daemon is running",
|
||||
"Check Docker socket permissions",
|
||||
"Try: docker info",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Daemon",
|
||||
Status: "PASS",
|
||||
Message: "Docker daemon is running and accessible",
|
||||
}
|
||||
return pass(nameDockerDaemon, "Docker daemon is running and accessible")
|
||||
}
|
||||
|
||||
// checkDockerContext verifies Docker context configuration.
|
||||
func checkDockerContext(ctx context.Context) DoctorResult {
|
||||
contextInfo, err := getCurrentDockerContext(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Context",
|
||||
Status: "WARN",
|
||||
Message: "Could not detect Docker context, using default settings",
|
||||
Suggestions: []string{
|
||||
"Check: docker context ls",
|
||||
"Consider setting up a specific context if needed",
|
||||
},
|
||||
}
|
||||
return warn(
|
||||
nameDockerContext,
|
||||
"Could not detect Docker context, using default settings",
|
||||
"Check: docker context ls",
|
||||
"Consider setting up a specific context if needed",
|
||||
)
|
||||
}
|
||||
|
||||
if contextInfo == nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Context",
|
||||
Status: "PASS",
|
||||
Message: "Using default Docker context",
|
||||
}
|
||||
return pass(nameDockerContext, "Using default Docker context")
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Context",
|
||||
Status: "PASS",
|
||||
Message: "Using Docker context: " + contextInfo.Name,
|
||||
}
|
||||
return pass(nameDockerContext, "Using Docker context: "+contextInfo.Name)
|
||||
}
|
||||
|
||||
// checkDockerSocket verifies Docker socket accessibility.
|
||||
func checkDockerSocket(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Socket",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot access Docker socket: %v", err),
|
||||
Suggestions: []string{
|
||||
"Check Docker socket permissions",
|
||||
"Add user to docker group: sudo usermod -aG docker $USER",
|
||||
"For colima: ensure socket is accessible",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameDockerSocket,
|
||||
fmt.Sprintf("Cannot access Docker socket: %v", err),
|
||||
"Check Docker socket permissions",
|
||||
"Add user to docker group: sudo usermod -aG docker $USER",
|
||||
"For colima: ensure socket is accessible",
|
||||
)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Socket",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot get Docker info: %v", err),
|
||||
Suggestions: []string{
|
||||
"Check Docker daemon status",
|
||||
"Verify socket permissions",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameDockerSocket,
|
||||
fmt.Sprintf("Cannot get Docker info: %v", err),
|
||||
"Check Docker daemon status",
|
||||
"Verify socket permissions",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Socket",
|
||||
Status: "PASS",
|
||||
Message: fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion),
|
||||
return pass(nameDockerSocket, fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion))
|
||||
}
|
||||
|
||||
// checkDockerHubCredentials warns when pulls would be anonymous and
|
||||
// therefore rate-limited.
|
||||
func checkDockerHubCredentials() DoctorResult {
|
||||
_, _, source := dockertestutil.Credentials()
|
||||
if source == dockertestutil.CredentialSourceAnonymous {
|
||||
return warn(
|
||||
"Docker Hub Credentials",
|
||||
"No Docker Hub credentials found — pulls will be rate-limited (100/6h per IP)",
|
||||
"Run: docker login",
|
||||
"Or export DOCKERHUB_USERNAME and DOCKERHUB_TOKEN",
|
||||
"In CI: ensure the docker/login-action step is configured with secrets",
|
||||
)
|
||||
}
|
||||
|
||||
return pass("Docker Hub Credentials", fmt.Sprintf("Credentials available (source: %s)", source))
|
||||
}
|
||||
|
||||
// checkGolangImage verifies the golang Docker image is available locally or can be pulled.
|
||||
func checkGolangImage(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "FAIL",
|
||||
Message: "Cannot create Docker client for image check",
|
||||
}
|
||||
return fail(nameGolangImage, "Cannot create Docker client for image check")
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
|
|
@ -208,81 +217,94 @@ func checkGolangImage(ctx context.Context) DoctorResult {
|
|||
// First check if image is available locally
|
||||
available, err := checkImageAvailableLocally(ctx, cli, imageName)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot check golang image %s: %v", imageName, err),
|
||||
Suggestions: []string{
|
||||
"Check Docker daemon status",
|
||||
"Try: docker images | grep golang",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameGolangImage,
|
||||
fmt.Sprintf("Cannot check golang image %s: %v", imageName, err),
|
||||
"Check Docker daemon status",
|
||||
"Try: docker images | grep golang",
|
||||
)
|
||||
}
|
||||
|
||||
if available {
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "PASS",
|
||||
Message: fmt.Sprintf("Golang image %s is available locally", imageName),
|
||||
}
|
||||
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is available locally", imageName))
|
||||
}
|
||||
|
||||
// Image not available locally, try to pull it
|
||||
err = ensureImageAvailable(ctx, cli, imageName, false)
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Golang image %s not available locally and cannot pull: %v", imageName, err),
|
||||
Suggestions: []string{
|
||||
"Check internet connectivity",
|
||||
"Verify Docker Hub access",
|
||||
"Try: docker pull " + imageName,
|
||||
"Or run tests offline if image was pulled previously",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameGolangImage,
|
||||
fmt.Sprintf("Golang image %s not available locally and cannot pull: %v", imageName, err),
|
||||
"Check internet connectivity",
|
||||
"Verify Docker Hub access",
|
||||
"Try: docker pull "+imageName,
|
||||
"Or run tests offline if image was pulled previously",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "PASS",
|
||||
Message: fmt.Sprintf("Golang image %s is now available", imageName),
|
||||
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is now available", imageName))
|
||||
}
|
||||
|
||||
// checkK3sImage verifies the ghcr k3s image used by TestK8sOperator is available
|
||||
// locally or can be pulled. The image is pinned (see [k3sic.K3sImage]).
|
||||
func checkK3sImage(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
return fail(nameK3sImage, "Cannot create Docker client for image check")
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
imageName := k3sic.K3sImage
|
||||
|
||||
available, err := checkImageAvailableLocally(ctx, cli, imageName)
|
||||
if err != nil {
|
||||
return fail(
|
||||
nameK3sImage,
|
||||
fmt.Sprintf("Cannot check k3s image %s: %v", imageName, err),
|
||||
"Check Docker daemon status",
|
||||
"Try: docker images | grep k3s",
|
||||
)
|
||||
}
|
||||
|
||||
if available {
|
||||
return pass(nameK3sImage, fmt.Sprintf("K3s image %s is available locally", imageName))
|
||||
}
|
||||
|
||||
err = ensureImageAvailable(ctx, cli, imageName, false)
|
||||
if err != nil {
|
||||
return warn(
|
||||
nameK3sImage,
|
||||
fmt.Sprintf("K3s image %s not available locally and could not pull: %v", imageName, err),
|
||||
"Only TestK8sOperator needs this image; other tests are unaffected",
|
||||
"Try: docker pull "+imageName,
|
||||
)
|
||||
}
|
||||
|
||||
return pass(nameK3sImage, fmt.Sprintf("K3s image %s is now available", imageName))
|
||||
}
|
||||
|
||||
// checkGoInstallation verifies Go is installed and working.
|
||||
func checkGoInstallation(ctx context.Context) DoctorResult {
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Go Installation",
|
||||
Status: "FAIL",
|
||||
Message: "Go binary not found in PATH",
|
||||
Suggestions: []string{
|
||||
"Install Go: https://golang.org/dl/",
|
||||
"Ensure go is in your PATH",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameGoInstall,
|
||||
"Go binary not found in PATH",
|
||||
"Install Go: https://golang.org/dl/",
|
||||
"Ensure go is in your PATH",
|
||||
)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "version")
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Go Installation",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot get Go version: %v", err),
|
||||
}
|
||||
return fail(nameGoInstall, fmt.Sprintf("Cannot get Go version: %v", err))
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Go Installation",
|
||||
Status: "PASS",
|
||||
Message: version,
|
||||
}
|
||||
return pass(nameGoInstall, version)
|
||||
}
|
||||
|
||||
// checkGitRepository verifies we're in a git repository.
|
||||
|
|
@ -291,26 +313,19 @@ func checkGitRepository(ctx context.Context) DoctorResult {
|
|||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return DoctorResult{
|
||||
Name: "Git Repository",
|
||||
Status: "FAIL",
|
||||
Message: "Not in a Git repository",
|
||||
Suggestions: []string{
|
||||
"Run from within the headscale git repository",
|
||||
"Clone the repository: git clone https://github.com/juanfont/headscale.git",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
"Git Repository",
|
||||
"Not in a Git repository",
|
||||
"Run from within the headscale git repository",
|
||||
"Clone the repository: git clone https://github.com/juanfont/headscale.git",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Git Repository",
|
||||
Status: "PASS",
|
||||
Message: "Running in Git repository",
|
||||
}
|
||||
return pass("Git Repository", "Running in Git repository")
|
||||
}
|
||||
|
||||
// checkRequiredFiles verifies required files exist.
|
||||
func checkRequiredFiles(ctx context.Context) DoctorResult {
|
||||
func checkRequiredFiles(_ context.Context) DoctorResult {
|
||||
requiredFiles := []string{
|
||||
"go.mod",
|
||||
"integration/",
|
||||
|
|
@ -320,32 +335,23 @@ func checkRequiredFiles(ctx context.Context) DoctorResult {
|
|||
var missingFiles []string
|
||||
|
||||
for _, file := range requiredFiles {
|
||||
cmd := exec.CommandContext(ctx, "test", "-e", file)
|
||||
|
||||
err := cmd.Run()
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
missingFiles = append(missingFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingFiles) > 0 {
|
||||
return DoctorResult{
|
||||
Name: "Required Files",
|
||||
Status: "FAIL",
|
||||
Message: "Missing required files: " + strings.Join(missingFiles, ", "),
|
||||
Suggestions: []string{
|
||||
"Ensure you're in the headscale project root directory",
|
||||
"Check that integration/ directory exists",
|
||||
"Verify this is a complete headscale repository",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
"Required Files",
|
||||
"Missing required files: "+strings.Join(missingFiles, ", "),
|
||||
"Ensure you're in the headscale project root directory",
|
||||
"Check that integration/ directory exists",
|
||||
"Verify this is a complete headscale repository",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Required Files",
|
||||
Status: "PASS",
|
||||
Message: "All required files found",
|
||||
}
|
||||
return pass("Required Files", "All required files found")
|
||||
}
|
||||
|
||||
// displayDoctorResults shows the results in a formatted way.
|
||||
|
|
@ -357,11 +363,11 @@ func displayDoctorResults(results []DoctorResult) {
|
|||
var icon string
|
||||
|
||||
switch result.Status {
|
||||
case "PASS":
|
||||
case statusPass:
|
||||
icon = "✅"
|
||||
case "WARN":
|
||||
case statusWarn:
|
||||
icon = "⚠️"
|
||||
case "FAIL":
|
||||
case statusFail:
|
||||
icon = "❌"
|
||||
default:
|
||||
icon = "❓"
|
||||
|
|
|
|||
89
cmd/hi/listversions.go
Normal file
89
cmd/hi/listversions.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/creachadair/command"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnknownSet = errors.New("unknown --set value (want must|all)")
|
||||
errUnknownFormat = errors.New("unknown --format value (want space|newline|json)")
|
||||
)
|
||||
|
||||
// ListVersionsConfig holds flags for the list-versions subcommand.
|
||||
type ListVersionsConfig struct {
|
||||
Set string `flag:"set,default=must,Version set: must|all"`
|
||||
Exclude string `flag:"exclude,Comma-separated versions to exclude (e.g. head,unstable)"`
|
||||
Format string `flag:"format,default=space,Output format: space|newline|json"`
|
||||
}
|
||||
|
||||
var listVersionsConfig ListVersionsConfig
|
||||
|
||||
// listVersions prints the Tailscale versions used by integration tests
|
||||
// in a format CI can shell out to. Mirrors integration/scenario.go
|
||||
// AllVersions and MustTestVersions: "head" and "unstable" are bare
|
||||
// tags, releases get a "v" prefix so each entry can be appended to
|
||||
// "ghcr.io/tailscale/tailscale:" directly.
|
||||
func listVersions(env *command.Env) error {
|
||||
release := capver.TailscaleLatestMajorMinor(capver.SupportedMajorMinorVersions, true)
|
||||
all := append([]string{"head", "unstable"}, release...)
|
||||
must := append(append([]string{}, all[0:4]...), all[len(all)-2:]...)
|
||||
|
||||
var versions []string
|
||||
|
||||
switch listVersionsConfig.Set {
|
||||
case "must":
|
||||
versions = must
|
||||
case "all":
|
||||
versions = all
|
||||
default:
|
||||
return fmt.Errorf("%w: %q", errUnknownSet, listVersionsConfig.Set)
|
||||
}
|
||||
|
||||
excluded := make(map[string]bool)
|
||||
|
||||
if listVersionsConfig.Exclude != "" {
|
||||
for v := range strings.SplitSeq(listVersionsConfig.Exclude, ",") {
|
||||
excluded[strings.TrimSpace(v)] = true
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(versions))
|
||||
|
||||
for _, v := range versions {
|
||||
if excluded[v] {
|
||||
continue
|
||||
}
|
||||
|
||||
if v != "head" && v != "unstable" {
|
||||
v = "v" + v
|
||||
}
|
||||
|
||||
out = append(out, v)
|
||||
}
|
||||
|
||||
switch listVersionsConfig.Format {
|
||||
case "space":
|
||||
fmt.Println(strings.Join(out, " "))
|
||||
case "newline":
|
||||
for _, v := range out {
|
||||
fmt.Println(v)
|
||||
}
|
||||
case "json":
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(b))
|
||||
default:
|
||||
return fmt.Errorf("%w: %q", errUnknownFormat, listVersionsConfig.Format)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -29,6 +29,13 @@ func main() {
|
|||
return runDoctorCheck(env.Context())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list-versions",
|
||||
Help: "Print Tailscale versions used by integration tests",
|
||||
Usage: "list-versions [flags]",
|
||||
SetFlags: command.Flags(flax.MustBind, &listVersionsConfig),
|
||||
Run: listVersions,
|
||||
},
|
||||
{
|
||||
Name: "clean",
|
||||
Help: "Clean Docker resources",
|
||||
|
|
@ -79,20 +86,17 @@ func main() {
|
|||
}
|
||||
|
||||
func cleanAll(ctx context.Context) error {
|
||||
err := killTestContainers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, step := range []func(context.Context) error{
|
||||
killTestContainers,
|
||||
pruneDockerNetworks,
|
||||
cleanOldImages,
|
||||
cleanCacheVolume,
|
||||
} {
|
||||
err := step(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = pruneDockerNetworks(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cleanOldImages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cleanCacheVolume(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/creachadair/command"
|
||||
|
|
@ -66,64 +67,21 @@ func runIntegrationTest(env *command.Env) error {
|
|||
|
||||
// detectGoVersion reads the Go version from go.mod file.
|
||||
func detectGoVersion() string {
|
||||
goModPath := filepath.Join("..", "..", "go.mod")
|
||||
|
||||
if _, err := os.Stat("go.mod"); err == nil { //nolint:noinlineerr
|
||||
goModPath = "go.mod"
|
||||
} else if _, err := os.Stat("../../go.mod"); err == nil { //nolint:noinlineerr
|
||||
goModPath = "../../go.mod"
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(goModPath)
|
||||
content, err := os.ReadFile("go.mod")
|
||||
if err != nil {
|
||||
return "1.26.1"
|
||||
content, err = os.ReadFile(filepath.Join("..", "..", "go.mod"))
|
||||
if err != nil {
|
||||
return "1.26.1"
|
||||
}
|
||||
}
|
||||
|
||||
lines := splitLines(string(content))
|
||||
for _, line := range lines {
|
||||
if len(line) > 3 && line[:3] == "go " {
|
||||
version := line[3:]
|
||||
if idx := indexOf(version, " "); idx != -1 {
|
||||
version = version[:idx]
|
||||
for line := range strings.Lines(string(content)) {
|
||||
if rest, ok := strings.CutPrefix(line, "go "); ok {
|
||||
if f := strings.Fields(rest); len(f) > 0 {
|
||||
return f[0]
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
}
|
||||
|
||||
return "1.26.1"
|
||||
}
|
||||
|
||||
// splitLines splits a string into lines without using strings.Split.
|
||||
func splitLines(s string) []string {
|
||||
var (
|
||||
lines []string
|
||||
current string
|
||||
)
|
||||
|
||||
for _, char := range s {
|
||||
if char == '\n' {
|
||||
lines = append(lines, current)
|
||||
current = ""
|
||||
} else {
|
||||
current += string(char)
|
||||
}
|
||||
}
|
||||
|
||||
if current != "" {
|
||||
lines = append(lines, current)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// indexOf finds the first occurrence of substr in s.
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -160,7 +161,7 @@ func (sc *StatsCollector) monitorDockerEvents(ctx context.Context, runID string,
|
|||
continue
|
||||
}
|
||||
|
||||
// Convert to types.Container format for consistency
|
||||
// Convert to [types.Container] format for consistency
|
||||
cont := types.Container{ //nolint:staticcheck // SA1019: use container.Summary
|
||||
ID: containerInfo.ID,
|
||||
Names: []string{containerInfo.Name},
|
||||
|
|
@ -256,7 +257,7 @@ func (sc *StatsCollector) collectStatsForContainer(ctx context.Context, containe
|
|||
|
||||
err := decoder.Decode(&stats)
|
||||
if err != nil {
|
||||
// EOF is expected when container stops or stream ends
|
||||
// [io.EOF] is expected when container stops or stream ends
|
||||
if err.Error() != "EOF" && verbose {
|
||||
log.Printf("Failed to decode stats for container %s: %v", containerID[:12], err)
|
||||
}
|
||||
|
|
@ -312,7 +313,7 @@ func calculateCPUPercent(prevStats, stats *container.Stats) float64 { //nolint:s
|
|||
// Calculate CPU percentage: (container CPU delta / system CPU delta) * number of CPUs * 100
|
||||
numCPUs := float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
|
||||
if numCPUs == 0 {
|
||||
// Fallback: if PercpuUsage is not available, assume 1 CPU
|
||||
// Fallback: if [PercpuUsage] is not available, assume 1 CPU
|
||||
numCPUs = 1.0
|
||||
}
|
||||
|
||||
|
|
@ -374,24 +375,24 @@ func (sc *StatsCollector) GetSummary() []ContainerStatsSummary {
|
|||
SampleCount: len(stats),
|
||||
}
|
||||
|
||||
// Calculate CPU stats
|
||||
cpuValues := make([]float64, len(stats))
|
||||
memoryValues := make([]float64, len(stats))
|
||||
extract := func(get func(StatsSample) float64) []float64 {
|
||||
values := make([]float64, len(stats))
|
||||
for i, sample := range stats {
|
||||
values[i] = get(sample)
|
||||
}
|
||||
|
||||
for i, sample := range stats {
|
||||
cpuValues[i] = sample.CPUUsage
|
||||
memoryValues[i] = sample.MemoryMB
|
||||
return values
|
||||
}
|
||||
|
||||
summary.CPU = calculateStatsSummary(cpuValues)
|
||||
summary.Memory = calculateStatsSummary(memoryValues)
|
||||
summary.CPU = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.CPUUsage }))
|
||||
summary.Memory = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.MemoryMB }))
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
// Sort by container name for consistent output
|
||||
sort.Slice(summaries, func(i, j int) bool {
|
||||
return summaries[i].ContainerName < summaries[j].ContainerName
|
||||
slices.SortFunc(summaries, func(a, b ContainerStatsSummary) int {
|
||||
return cmp.Compare(a.ContainerName, b.ContainerName)
|
||||
})
|
||||
|
||||
return summaries
|
||||
|
|
@ -408,14 +409,8 @@ func calculateStatsSummary(values []float64) StatsSummary {
|
|||
sum := 0.0
|
||||
|
||||
for _, value := range values {
|
||||
if value < minVal {
|
||||
minVal = value
|
||||
}
|
||||
|
||||
if value > maxVal {
|
||||
maxVal = value
|
||||
}
|
||||
|
||||
minVal = min(minVal, value)
|
||||
maxVal = max(maxVal, value)
|
||||
sum += value
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
// vendorhash check exit non-zero if flakehashes.json is stale
|
||||
// vendorhash update recompute and rewrite flakehashes.json
|
||||
//
|
||||
// The JSON schema and goModFingerprint algorithm mirror upstream
|
||||
// The JSON schema and [goModFingerprint] algorithm mirror upstream
|
||||
// tailscale's tool/updateflakes so a future shared library extraction
|
||||
// is straightforward.
|
||||
package main
|
||||
|
|
@ -82,8 +82,8 @@ func usage() {
|
|||
fmt.Fprintln(os.Stderr, "usage: vendorhash <check|update>")
|
||||
}
|
||||
|
||||
// errStale signals to main that the check found a mismatch; it has
|
||||
// already printed a remediation message, so main should exit 1
|
||||
// errStale signals to [main] that the check found a mismatch; it has
|
||||
// already printed a remediation message, so [main] should exit 1
|
||||
// silently.
|
||||
var errStale = errors.New("vendor hash stale")
|
||||
|
||||
|
|
|
|||
|
|
@ -23,21 +23,12 @@ listen_addr: 127.0.0.1:8080
|
|||
# Use an empty value to disable the metrics listener.
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
|
||||
# Address to listen for gRPC.
|
||||
# gRPC is used for controlling a headscale server
|
||||
# remotely with the CLI
|
||||
# Note: Remote access _only_ works if you have
|
||||
# valid certificates.
|
||||
#
|
||||
# For production:
|
||||
# grpc_listen_addr: 0.0.0.0:50443
|
||||
grpc_listen_addr: 127.0.0.1:50443
|
||||
|
||||
# Allow the gRPC admin interface to run in INSECURE
|
||||
# mode. This is not recommended as the traffic will
|
||||
# be unencrypted. Only enable if you know what you
|
||||
# are doing.
|
||||
grpc_allow_insecure: false
|
||||
# CIDR(s) of reverse proxies (e.g. 127.0.0.1/32) whose
|
||||
# True-Client-IP, X-Real-IP and X-Forwarded-For headers should
|
||||
# be honoured. Empty (default) ignores those headers; setting
|
||||
# this without a proxy in front lets clients spoof their logged
|
||||
# source IP.
|
||||
trusted_proxies: []
|
||||
|
||||
# The Noise section includes specific configuration for the
|
||||
# TS2021 Noise protocol
|
||||
|
|
@ -80,8 +71,7 @@ prefixes:
|
|||
# connection cannot be established.
|
||||
# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
|
||||
#
|
||||
# headscale needs a list of DERP servers that can be presented
|
||||
# to the clients.
|
||||
# Headscale needs a list of DERP servers that can be presented to the clients.
|
||||
derp:
|
||||
server:
|
||||
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
|
||||
|
|
@ -126,9 +116,9 @@ derp:
|
|||
|
||||
# Locally available DERP map files encoded in YAML
|
||||
#
|
||||
# This option is mostly interesting for people hosting
|
||||
# their own DERP servers:
|
||||
# This option is mostly interesting for people hosting their own DERP servers:
|
||||
# https://tailscale.com/docs/reference/derp-servers/custom-derp-servers
|
||||
# https://headscale.net/stable/ref/derp/
|
||||
#
|
||||
# paths:
|
||||
# - /etc/headscale/derp-example.yaml
|
||||
|
|
@ -240,12 +230,12 @@ database:
|
|||
# ssl: false
|
||||
|
||||
### TLS configuration
|
||||
#
|
||||
## Let's encrypt / ACME
|
||||
#
|
||||
# headscale supports automatically requesting and setting up
|
||||
# See: https://headscale.net/stable/ref/tls/
|
||||
|
||||
## Let's Encrypt / ACME
|
||||
# Headscale supports automatically requesting and setting up
|
||||
# TLS for a domain with Let's Encrypt.
|
||||
#
|
||||
|
||||
# URL to ACME directory
|
||||
acme_url: https://acme-v02.api.letsencrypt.org/directory
|
||||
|
||||
|
|
@ -255,15 +245,13 @@ acme_email: ""
|
|||
# Domain name to request a TLS certificate for:
|
||||
tls_letsencrypt_hostname: ""
|
||||
|
||||
# Path to store certificates and metadata needed by
|
||||
# letsencrypt
|
||||
# For production:
|
||||
# Path to store certificates and metadata needed by letsencrypt
|
||||
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
|
||||
|
||||
# Type of ACME challenge to use, currently supported types:
|
||||
# HTTP-01 or TLS-ALPN-01
|
||||
# See: docs/ref/tls.md for more information
|
||||
tls_letsencrypt_challenge_type: HTTP-01
|
||||
|
||||
# When HTTP-01 challenge is chosen, letsencrypt must set up a
|
||||
# verification endpoint, and it will be listening on:
|
||||
# :http = port 80
|
||||
|
|
@ -289,6 +277,7 @@ policy:
|
|||
# The mode can be "file" or "database" that defines
|
||||
# where the policies are stored and read from.
|
||||
mode: file
|
||||
|
||||
# If the mode is set to "file", the path to a HuJSON file containing policies.
|
||||
path: ""
|
||||
|
||||
|
|
@ -297,7 +286,6 @@ policy:
|
|||
# headscale supports Tailscale's DNS configuration and MagicDNS.
|
||||
# Please have a look to their docs to better understand the concepts:
|
||||
#
|
||||
# - https://tailscale.com/docs/features/access-control/acls
|
||||
# - https://tailscale.com/docs/features/magicdns
|
||||
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns
|
||||
#
|
||||
|
|
@ -314,7 +302,7 @@ policy:
|
|||
# If you want stop Headscale from managing the DNS configuration
|
||||
# all the fields under `dns` should be set to empty values.
|
||||
dns:
|
||||
# Whether to use [MagicDNS](https://tailscale.com/docs/features/magicdns).
|
||||
# Whether to use MagicDNS
|
||||
magic_dns: true
|
||||
|
||||
# Defines the base domain to create the hostnames for MagicDNS.
|
||||
|
|
@ -355,7 +343,7 @@ dns:
|
|||
|
||||
# Extra DNS records
|
||||
# so far only A and AAAA records are supported (on the tailscale side)
|
||||
# See: docs/ref/dns.md
|
||||
# See: https://headscale.net/stable/ref/dns/
|
||||
extra_records: []
|
||||
# - name: "grafana.myvpn.example.com"
|
||||
# type: "A"
|
||||
|
|
@ -374,6 +362,7 @@ unix_socket: /var/run/headscale/headscale.sock
|
|||
unix_socket_permission: "0770"
|
||||
|
||||
# OpenID Connect
|
||||
# https://headscale.net/stable/ref/oidc/
|
||||
# oidc:
|
||||
# # Block startup until the identity provider is available and healthy.
|
||||
# only_start_if_oidc_is_available: true
|
||||
|
|
@ -470,6 +459,7 @@ taildrop:
|
|||
# choice.
|
||||
auto_update:
|
||||
enabled: false
|
||||
|
||||
# Advanced performance tuning parameters.
|
||||
# The defaults are carefully chosen and should rarely need adjustment.
|
||||
# Only modify these if you have identified a specific performance issue.
|
||||
|
|
|
|||
|
|
@ -152,15 +152,15 @@ See also <https://tailscale.com/docs/concepts/device-visibility>.
|
|||
Headscale checks if the policy is valid during startup and refuses to start if it detects an error. The error message
|
||||
indicates which part of the policy is invalid. Follow these steps to fix your policy:
|
||||
|
||||
- Dump the policy to a file: `headscale policy get --bypass-grpc-and-access-database-directly > policy.json`
|
||||
- Dump the policy to a file: `headscale policy get --bypass-server-and-access-database-directly > policy.json`
|
||||
- Edit and fixup `policy.json`. Use the command `headscale policy check --file policy.json` to validate the policy.
|
||||
- Load the modified policy: `headscale policy set --bypass-grpc-and-access-database-directly --file policy.json`
|
||||
- Load the modified policy: `headscale policy set --bypass-server-and-access-database-directly --file policy.json`
|
||||
- Start Headscale as usual.
|
||||
|
||||
!!! warning "Full server configuration required"
|
||||
|
||||
The above commands to get/set the policy require a complete server configuration file including database settings. A
|
||||
minimal config to [control Headscale via remote CLI](../ref/api.md#grpc) is not sufficient. You may use
|
||||
minimal config to [control Headscale via remote CLI](../ref/api.md#remote-control) is not sufficient. You may use
|
||||
`headscale -c /path/to/config.yaml` to specify the path to an alternative configuration file.
|
||||
|
||||
## How can I migrate back to the recommended IP prefixes?
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ provides on overview of Headscale's feature and compatibility with the Tailscale
|
|||
- [x] [Global and restricted nameservers (split DNS)](https://tailscale.com/docs/reference/dns-in-tailscale#nameservers)
|
||||
- [x] [search domains](https://tailscale.com/docs/reference/dns-in-tailscale#search-domains)
|
||||
- [x] [Extra DNS records (Headscale only)](../ref/dns.md#setting-extra-dns-records)
|
||||
- [x] [Taildrop](https://tailscale.com/docs/features/taildrop)
|
||||
- [x] File sharing
|
||||
- [x] [Taildrive](https://tailscale.com/docs/features/taildrive)
|
||||
- [x] [Taildrop](https://tailscale.com/docs/features/taildrop)
|
||||
- [x] [Tags](../ref/tags.md)
|
||||
- [x] [Routes](../ref/routes.md)
|
||||
- [x] [Subnet routers](../ref/routes.md#subnet-router)
|
||||
|
|
@ -31,6 +33,9 @@ provides on overview of Headscale's feature and compatibility with the Tailscale
|
|||
routers](../ref/routes.md#automatically-approve-routes-of-a-subnet-router) and [exit
|
||||
nodes](../ref/routes.md#automatically-approve-an-exit-node-with-auto-approvers)
|
||||
- [x] [Tailscale SSH](https://tailscale.com/docs/features/tailscale-ssh)
|
||||
- [x] [Node attributes](../ref/policy.md#node-attributes)
|
||||
- [x] [Tests](https://tailscale.com/docs/reference/syntax/policy-file#tests) and
|
||||
[sshTests](https://tailscale.com/docs/reference/syntax/policy-file#ssh-tests)
|
||||
- [x] [Node registration using Single-Sign-On (OpenID Connect)](../ref/oidc.md) ([GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC))
|
||||
- [x] Basic registration
|
||||
- [x] Update user profile from identity provider
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# API
|
||||
|
||||
Headscale provides a [HTTP REST API](#rest-api) and a [gRPC interface](#grpc) which may be used to integrate a [web
|
||||
interface](integration/web-ui.md), [remote control Headscale](#setup-remote-control) or provide a base for custom
|
||||
Headscale provides a [HTTP REST API](#rest-api) which may be used to integrate a [web
|
||||
interface](integration/web-ui.md), [remote control Headscale](#remote-control) or provide a base for custom
|
||||
integration and tooling.
|
||||
|
||||
Both interfaces require a valid API key before use. To create an API key, log into your Headscale server and generate
|
||||
The API requires a valid API key before use. To create an API key, log into your Headscale server and generate
|
||||
one with the default expiration of 90 days:
|
||||
|
||||
```shell
|
||||
|
|
@ -29,12 +29,12 @@ headscale apikeys expire --prefix <PREFIX>
|
|||
## REST API
|
||||
|
||||
- API endpoint: `/api/v1`, e.g. `https://headscale.example.com/api/v1`
|
||||
- Documentation: `/swagger`, e.g. `https://headscale.example.com/swagger`
|
||||
- Documentation: `/api/v1/docs`, e.g. `https://headscale.example.com/api/v1/docs`
|
||||
- Headscale Version: `/version`, e.g. `https://headscale.example.com/version`
|
||||
- Authenticate using HTTP Bearer authentication by sending the [API key](#api) with the HTTP `Authorization: Bearer <API_KEY>` header.
|
||||
|
||||
Start by [creating an API key](#api) and test it with the examples below. Read the API documentation provided by your
|
||||
Headscale server at `/swagger` for details.
|
||||
Headscale server at `/api/v1/docs` for details.
|
||||
|
||||
=== "Get details for all users"
|
||||
|
||||
|
|
@ -54,20 +54,18 @@ Headscale server at `/swagger` for details.
|
|||
|
||||
```console
|
||||
curl -H "Authorization: Bearer <API_KEY>" \
|
||||
--json '{"user": "<USER>", "authId": "AUTH_ID>"}' \
|
||||
--json '{"user": "<USER>", "authId": "<AUTH_ID>"}' \
|
||||
https://headscale.example.com/api/v1/auth/register
|
||||
```
|
||||
|
||||
## gRPC
|
||||
## Remote control
|
||||
|
||||
The gRPC interface can be used to control a Headscale instance from a remote machine with the `headscale` binary.
|
||||
The `headscale` binary can control a Headscale instance from a remote machine over the HTTP API.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
- A workstation to run `headscale` (any supported platform, e.g. Linux).
|
||||
- A Headscale server with gRPC enabled.
|
||||
- Connections to the gRPC port (default: `50443`) are allowed.
|
||||
- Remote access requires an encrypted connection via TLS.
|
||||
- The Headscale server reachable over HTTP(S).
|
||||
- An [API key](#api) to authenticate with the Headscale server.
|
||||
|
||||
### Setup remote control
|
||||
|
|
@ -88,19 +86,20 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
|||
|
||||
```yaml title="config.yaml"
|
||||
cli:
|
||||
address: <HEADSCALE_ADDRESS>:<PORT>
|
||||
address: <HEADSCALE_URL>
|
||||
api_key: <API_KEY>
|
||||
```
|
||||
|
||||
=== "Environment variables"
|
||||
|
||||
```shell
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_ADDRESS>:<PORT>"
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_URL>"
|
||||
export HEADSCALE_CLI_API_KEY="<API_KEY>"
|
||||
```
|
||||
|
||||
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_ADDRESS>:<PORT>`, instead of
|
||||
connecting to the local instance.
|
||||
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_URL>` (e.g.
|
||||
`https://headscale.example.com`), instead of connecting to the local instance. A bare host without a scheme is
|
||||
assumed to be `https`.
|
||||
|
||||
1. Test the connection by listing all nodes:
|
||||
|
||||
|
|
@ -113,15 +112,12 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
|||
|
||||
### Behind a proxy
|
||||
|
||||
It's possible to run the gRPC remote endpoint behind a reverse proxy, like Nginx, and have it run on the _same_ port as Headscale.
|
||||
|
||||
While this is _not a supported_ feature, an example on how this can be set up on
|
||||
[NixOS is shown here](https://github.com/kradalby/dotfiles/blob/4489cdbb19cddfbfae82cd70448a38fde5a76711/machines/headscale.oracldn/headscale.nix#L61-L91).
|
||||
The remote CLI uses the same HTTP API as everything else, so it works through the reverse proxy already in front of
|
||||
Headscale with no extra setup.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Make sure you have the _same_ Headscale version on your server and workstation.
|
||||
- Ensure that connections to the gRPC port are allowed.
|
||||
- Verify that your TLS certificate is valid and trusted.
|
||||
- If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
|
||||
- Add your self-signed certificate to the trust store of your OS _or_
|
||||
|
|
|
|||
|
|
@ -1,90 +1,183 @@
|
|||
# Running headscale behind a reverse proxy
|
||||
# Running Headscale behind a reverse proxy
|
||||
|
||||
!!! warning "Community documentation"
|
||||
|
||||
This page is not actively maintained by the headscale authors and is
|
||||
written by community members. It is _not_ verified by headscale developers.
|
||||
This page is not actively maintained by the Headscale authors and is
|
||||
written by community members. It is _not_ verified by Headscale developers.
|
||||
|
||||
**It might be outdated and it might miss necessary steps**.
|
||||
|
||||
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
||||
Running Headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want
|
||||
to reuse the same external IP and port - usually tcp/443 for HTTPS.
|
||||
|
||||
### WebSockets
|
||||
Please see [limitations](#limitations) for known issues and limitations.
|
||||
|
||||
The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.
|
||||
## Configuration
|
||||
|
||||
WebSockets support is also required when using the Headscale [embedded DERP server](../derp.md). In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
|
||||
The configuration depends on the set of Headscale features you intend to use. Please have a look at the
|
||||
[requirements](../../setup/requirements.md) and especially the [ports in use](../../setup/requirements.md#ports-in-use)
|
||||
section to learn what a Tailscale clients expects.
|
||||
|
||||
### Cloudflare
|
||||
The configuration examples in this documentation are basic and cover only HTTP and HTTPS traffic. Other features such as
|
||||
STUN for Headscale's [embedded DERP server](../derp.md) are expected to be exposed directly or to be only available on
|
||||
localhost.
|
||||
|
||||
Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See [this issue](https://github.com/juanfont/headscale/issues/1468)
|
||||
### WebSocket
|
||||
|
||||
Tailscale clients are using a custom protocol (Tailscale Control Protocol) to communicate with a control server such as
|
||||
Headscale. The reverse proxy **must** be configured to support WebSockets in order to communicate with Tailscale clients
|
||||
and it needs to handle two peculiarities of the Tailscale Control Protocol:
|
||||
|
||||
- The POST method is used to upgrade the WebSocket connection.
|
||||
- The value for the `Upgrade` header is `tailscale-control-protocol`.
|
||||
|
||||
### TLS
|
||||
|
||||
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.
|
||||
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration
|
||||
values to your Headscale [configuration file](../configuration.md):
|
||||
|
||||
```yaml title="config.yaml"
|
||||
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
```yaml title="config.yaml" hl_lines="1"
|
||||
server_url: https://<SERVER_NAME>
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
|
||||
## nginx
|
||||
Headscale logs `WRN listening without TLS but ServerURL does not start with http://` during startup. This is expected
|
||||
and indicates that the reverse proxy is in charge of terminating TLS.
|
||||
|
||||
The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.
|
||||
### Trusted proxies
|
||||
|
||||
```nginx title="nginx.conf"
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
Headscale ignores `True-Client-IP`, `X-Real-IP` and `X-Forwarded-For` headers unless the request's TCP peer matches the
|
||||
`trusted_proxies` configuration option. Set this to the CIDR(s) your reverse proxy connects from so the real client IP
|
||||
appears in access logs.
|
||||
|
||||
```yaml title="config.yaml"
|
||||
trusted_proxies:
|
||||
- 127.0.0.1/32
|
||||
- ::1/128
|
||||
```
|
||||
|
||||
The reverse proxy is responsible to replace any client-supplied `True-Client-IP`, `X-Real-IP`, `X-Forwarded-For` headers
|
||||
on inbound requests with sanitized values. Headscale picks the first valid IP address supplied by headers in this order:
|
||||
|
||||
- `True-Client-IP`
|
||||
- `X-Real-IP`
|
||||
- `X-Forwarded-For`
|
||||
|
||||
## Limitations
|
||||
|
||||
- A reverse proxy adds another layer of complexity that needs to be able to handle the [Tailscale Control
|
||||
Protocol](#websocket) properly. Be sure to test your setup without a reverse proxy before raising an issue.
|
||||
- STUN (used along with the [embedded DERP server](../derp.md)) requires udp/3478 to be served publicly.
|
||||
|
||||
## Reverse proxy specific configuration
|
||||
|
||||
!!! warning "Third-party software and services"
|
||||
|
||||
This section of the documentation is specific for third-party software and services. We recommend users read the
|
||||
third-party documentation for a secure configuration.
|
||||
|
||||
This following Headscale configuration may be used as base for the various reverse proxy examples below. The following
|
||||
is [assumed](../../setup/requirements.md):
|
||||
|
||||
- Service for Tailscale clients is served via HTTPS on port 443.
|
||||
- The reverse proxy redirects HTTP to HTTPS and is terminating TLS.
|
||||
- Both Headscale and the reverse proxy are running on the same host.
|
||||
- [Metrics](../debug.md#metrics-and-debug-endpoint) are not proxied, those are available via localhost.
|
||||
|
||||
```yaml title="config.yaml" hl_lines="1"
|
||||
server_url: https://<SERVER_NAME>
|
||||
listen_addr: 127.0.0.1:8080
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
trusted_proxies:
|
||||
- 127.0.0.1/32
|
||||
- ::1/128
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
|
||||
### Apache
|
||||
|
||||
The following basic Apache configuration works with the Headscale configuration [as shown
|
||||
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
|
||||
|
||||
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
|
||||
- `<PATH_TO_TLS_CERT>`: Absolute path to your TLS certificate
|
||||
- `<PATH_TO_TLS_KEY>`: Absolute path to your TLS private key
|
||||
|
||||
```apache title="apache.conf" hl_lines="2 7 11 14-15"
|
||||
<VirtualHost *:80>
|
||||
ServerName <SERVER_NAME>
|
||||
|
||||
# Tailscale captive portal detection
|
||||
RedirectMatch 204 ^/generate_204$
|
||||
|
||||
RedirectMatch permanent "^/(.*)$" "https://<SERVER_NAME>/$1"
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName <SERVER_NAME>
|
||||
|
||||
SSLEngine On
|
||||
SSLCertificateFile <PATH_TO_TLS_CERT>
|
||||
SSLCertificateKeyFile <PATH_TO_TLS_KEY>
|
||||
|
||||
RequestHeader set True-Client-IP "%{REMOTE_ADDR}s"
|
||||
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://127.0.0.1:8080/ upgrade=any
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSocket traffic whose `Upgrade` header
|
||||
value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache
|
||||
docs](https://httpd.apache.org/docs/current/mod/mod_proxy.html#upgrade) for more information on this.
|
||||
|
||||
### Caddy
|
||||
|
||||
The following basic Caddyfile works with the Headscale configuration [as shown
|
||||
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
|
||||
|
||||
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
|
||||
|
||||
```none title="Caddyfile" hl_lines="1 12"
|
||||
http://<SERVER_NAME> {
|
||||
# Tailscale captive portal detection
|
||||
handle /generate_204 {
|
||||
respond 204
|
||||
}
|
||||
|
||||
handle * {
|
||||
redir https://{host}{uri}
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
server_name <YOUR_SERVER_NAME>;
|
||||
|
||||
ssl_certificate <PATH_TO_CERT>;
|
||||
ssl_certificate_key <PATH_CERT_KEY>;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<IP:PORT>;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $server_name;
|
||||
proxy_redirect http:// https://;
|
||||
proxy_buffering off;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
|
||||
}
|
||||
<SERVER_NAME> {
|
||||
reverse_proxy 127.0.0.1:8080 {
|
||||
header_up True-Client-IP {remote_host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## istio/envoy
|
||||
Caddy will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your
|
||||
domain/subdomain, force HTTPS, and proxy WebSocket connections.
|
||||
|
||||
If you using [Istio](https://istio.io/) ingressgateway or [Envoy](https://www.envoyproxy.io/) as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:
|
||||
### Cloudflare
|
||||
|
||||
```log
|
||||
Sending local reply with details upgrade_failed
|
||||
```
|
||||
Running Headscale behind a Cloudflare Proxy or Cloudflare Tunnel is not supported and will not work as Cloudflare does
|
||||
not support [WebSocket POSTs as required by the Tailscale protocol](#websocket). See [issue
|
||||
1468](https://github.com/juanfont/headscale/issues/1468) for more information.
|
||||
|
||||
### Envoy
|
||||
|
||||
You need to add a new upgrade_type named `tailscale-control-protocol`. [see details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig)
|
||||
You need to add a new upgrade_type named `tailscale-control-protocol`. [See
|
||||
details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig).
|
||||
|
||||
### Istio
|
||||
|
||||
Same as envoy, we can use `EnvoyFilter` to add upgrade_type.
|
||||
Same as [envoy](#envoy), we can use `EnvoyFilter` to add a new upgrade_type named `tailscale-control-protocol`.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
|
|
@ -109,33 +202,68 @@ spec:
|
|||
- upgrade_type: tailscale-control-protocol
|
||||
```
|
||||
|
||||
## Caddy
|
||||
### Nginx
|
||||
|
||||
The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the `config.yaml` specifications above to disable headscale's built in TLS. Replace values as necessary - `<YOUR_SERVER_NAME>` should be the FQDN at which headscale will be served, and `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `localhost:8080`.
|
||||
The following basic Nginx configuration works with the Headscale configuration [as shown
|
||||
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
|
||||
|
||||
```none title="Caddyfile"
|
||||
<YOUR_SERVER_NAME> {
|
||||
reverse_proxy <IP:PORT>
|
||||
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
|
||||
- `<PATH_TO_TLS_CERT>`: Absolute path to your TLS certificate
|
||||
- `<PATH_TO_TLS_KEY>`: Absolute path to your TLS private key
|
||||
|
||||
```nginx title="nginx.conf" hl_lines="19 37 39-40"
|
||||
# headscale
|
||||
upstream headscale {
|
||||
zone upstreams 64K;
|
||||
server 127.0.0.1:8080 max_fails=1 fail_timeout=5s;
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
# websocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# http
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name <SERVER_NAME>;
|
||||
|
||||
# Tailscale captive portal detection
|
||||
location = /generate_204 {
|
||||
return 204;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# https
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
server_name <SERVER_NAME>;
|
||||
|
||||
ssl_certificate <PATH_TO_TLS_CERT>;
|
||||
ssl_certificate_key <PATH_TO_TLS_KEY>;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header True-Client-IP $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
proxy_pass http://headscale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.
|
||||
|
||||
For a slightly more complex configuration which utilizes Docker containers to manage Caddy, headscale, and Headscale-UI, [Guru Computing's guide](https://blog.gurucomputing.com.au/smart-vpns-with-headscale/) is an excellent reference.
|
||||
|
||||
## Apache
|
||||
|
||||
The following minimal Apache config will proxy traffic to the headscale instance on `<IP:PORT>`. Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSockets traffic whose `Upgrade` header value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache docs](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) for more information on this.
|
||||
|
||||
```apache title="apache.conf"
|
||||
<VirtualHost *:443>
|
||||
ServerName <YOUR_SERVER_NAME>
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://<IP:PORT>/ upgrade=any
|
||||
|
||||
SSLEngine On
|
||||
SSLCertificateFile <PATH_TO_CERT>
|
||||
SSLCertificateKeyFile <PATH_CERT_KEY>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -23,5 +23,7 @@ Headscale doesn't provide a built-in web interface but users may pick one from t
|
|||
- [HeadControl](https://github.com/ahmadzip/HeadControl) - Minimal Headscale admin dashboard, built with Go and HTMX
|
||||
- [Headscale Manager](https://github.com/hkdone/headscalemanager) - Headscale UI for Android
|
||||
- [Headscale UI](https://github.com/MunMunMiao/headscale-ui) - Headscale UI online and Self-hosting
|
||||
- [Headscale Panel](https://github.com/headscale-panel/panel) - A modern Headscale management panel with a clean,
|
||||
network-operations-focused UI
|
||||
|
||||
You can ask for support on our [Discord server](https://discord.gg/c84AZQhmpx) in the "web-interfaces" channel.
|
||||
|
|
|
|||
|
|
@ -158,17 +158,17 @@ devices. Can only be used in policy destinations.
|
|||
{
|
||||
"src": ["boss@"],
|
||||
"dst": ["boss@"],
|
||||
"ip": "*"
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"src": ["dev1@"],
|
||||
"dst": ["dev1@"],
|
||||
"ip": "*"
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"src": ["intern1@"],
|
||||
"dst": ["intern1@"],
|
||||
"ip": "*"
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -196,5 +196,54 @@ Used in Tailscale SSH rules to allow access to any user except root. Can only be
|
|||
This autogroup resolves to all IP addresses (`0.0.0.0/0` and `::/0`) which also includes all IP addresses outside the
|
||||
standard Tailscale IP ranges. This autogroup can only be used as source.
|
||||
|
||||
## Node Attributes
|
||||
|
||||
[Node attributes](https://tailscale.com/docs/reference/syntax/policy-file#node-attributes) allow for device-specific
|
||||
configuration and attributes. At least the following node attributes are currently supported by Headscale[^2]:
|
||||
|
||||
- `drive:access`, `drive:share`: [Taildrive support](https://tailscale.com/docs/features/taildrive).
|
||||
- `nextdns:<profile>`, `nextdns:no-device-info`: [NextDNS integration](https://tailscale.com/docs/integrations/nextdns).
|
||||
Be sure to set NextDNS as global resolver in the [configuration](configuration.md).
|
||||
- `magicdns-aaaa`: Respond to AAAA queries on the local [MagicDNS](https://tailscale.com/docs/features/magicdns)
|
||||
resolver at 100.100.100.100.
|
||||
- `disable-ipv4`: Selectively disable IPv4 for specfic nodes. This is may be useful to workaround [CGNat
|
||||
conflicts](https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts).
|
||||
- `randomize-client-port`: Allocate a [random port for WireGuard
|
||||
traffic](https://tailscale.com/docs/reference/syntax/policy-file#randomizeclientport) instead of the static default
|
||||
port 41641.
|
||||
- `disable-captive-portal-detection`: [Disable automatic captive portal
|
||||
detection](https://tailscale.com/docs/integrations/captive-portals#disable-captive-portal-detection).
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"nodeAttrs": [
|
||||
{
|
||||
// Enable MagicDNS AAAA records for all nodes
|
||||
"target": ["*"]
|
||||
"attr": ["magicdns-aaaa"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Network-wide policy options
|
||||
|
||||
The following options are applied for the entire tailnet. Consider [node attributes](#node-attributes) for a more
|
||||
fine-grained configuration instead.
|
||||
|
||||
- `randomizeClientPort`: Allocate a [random port for WireGuard
|
||||
traffic](https://tailscale.com/docs/reference/syntax/policy-file#randomizeclientport) instead of the static default
|
||||
port 41641.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
// Use a random WireGuard port for the entire tailnet
|
||||
"randomizeClientPort": true
|
||||
}
|
||||
```
|
||||
|
||||
[^1]: Headscale also allows to store the policy in the database. This is typically only required in case a [web
|
||||
interface](integration/web-ui.md) is used.
|
||||
|
||||
[^2]: Other key-only node attributes can be used as well. Find them in the client source code with `grep -E '^\s+NodeAttr\w+' tailcfg/tailcfg.go` or by using [GitHub code search (requires
|
||||
login)](https://github.com/search?q=repo%3Atailscale%2Ftailscale%20language%3Ago%20path%3Atailcfg%2Ftailcfg.go%20symbol%3A%2FNodeAttr%5Cw%2B%2F&type=code).
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ distributions are Ubuntu 22.04 or newer, Debian 12 or newer.
|
|||
sudo nano /etc/headscale/config.yaml
|
||||
```
|
||||
|
||||
1. Enable and start the headscale service:
|
||||
1. Restart headscale to pick up configuration changes:
|
||||
|
||||
```shell
|
||||
sudo systemctl enable --now headscale
|
||||
sudo systemctl restart headscale
|
||||
```
|
||||
|
||||
1. Verify that headscale is running as intended:
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ The ports in use vary with the intended scenario and enabled features. Some of t
|
|||
- udp/3478
|
||||
- Expose publicly: yes
|
||||
- STUN, required if the [embedded DERP server](../ref/derp.md) is enabled
|
||||
- tcp/50443
|
||||
- Expose publicly: yes
|
||||
- Only required if the gRPC interface is used to [remote-control Headscale](../ref/api.md#grpc).
|
||||
- tcp/9090
|
||||
- Expose publicly: no
|
||||
- [Metrics and debug endpoint](../ref/debug.md#metrics-and-debug-endpoint)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
!!! tip "Required update path"
|
||||
|
||||
Its required to update from one stable version to the next (e.g. 0.26.0 → 0.27.1 → 0.28.0) without skipping minor
|
||||
It's required to update from one stable version to the next (e.g. 0.26.0 → 0.27.1 → 0.28.0) without skipping minor
|
||||
versions in between. You should always pick the latest available patch release.
|
||||
|
||||
Update an existing Headscale installation to a new version:
|
||||
|
|
@ -23,7 +23,7 @@ upgrading. A full backup of Headscale depends on your individual setup, but belo
|
|||
|
||||
=== "Standard installation"
|
||||
|
||||
A installation that follows our [official releases](install/official.md) setup guide uses the following paths:
|
||||
An installation that follows our [official releases](install/official.md) setup guide uses the following paths:
|
||||
|
||||
- [Configuration file](../ref/configuration.md): `/etc/headscale/config.yaml`
|
||||
- Data directory: `/var/lib/headscale`
|
||||
|
|
@ -37,7 +37,7 @@ upgrading. A full backup of Headscale depends on your individual setup, but belo
|
|||
|
||||
=== "Container"
|
||||
|
||||
A installation that follows our [container](install/container.md) setup guide uses a single source volume directory
|
||||
An installation that follows our [container](install/container.md) setup guide uses a single source volume directory
|
||||
that contains the configuration file, data directory and the SQLite database.
|
||||
|
||||
```console
|
||||
|
|
|
|||
87
flake.lock
generated
87
flake.lock
generated
|
|
@ -1,5 +1,27 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-checks": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781951072,
|
||||
"narHash": "sha256-hA9u6hB4QzpReP8hudxqiaa4vLhoRvtXi6YmUgJRGEs=",
|
||||
"owner": "kradalby",
|
||||
"repo": "flake-checks",
|
||||
"rev": "3d2882efec5cf10f8b5a8a035d93ba6ba9f21717",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "kradalby",
|
||||
"repo": "flake-checks",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
|
|
@ -18,25 +40,44 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1777270315,
|
||||
"narHash": "sha256-yKB4G6cKsQsWN7M6rZGk6gkJPDNPIzT05y4qzRyCDlI=",
|
||||
"lastModified": 1781153106,
|
||||
"narHash": "sha256-yzsroLCcuRG4KdGMxWt0eXKOrRSgQT8/xjYngeq9ujU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6368eda62c9775c38ef7f714b2555a741c20c72d",
|
||||
"rev": "9ee75f111a06d7ab2b2f729698a8eff53d54e070",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"ref": "staging-next-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-checks": "flake-checks",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
|
|
@ -54,6 +95,42 @@
|
|||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"flake-checks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780220602,
|
||||
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
|
|||
132
flake.nix
132
flake.nix
|
|
@ -2,14 +2,24 @@
|
|||
description = "headscale - Open Source Tailscale Control server";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
# Pinned to staging-next-26.05 for Go 1.26.4 (security fix GO-2026-5037/5039):
|
||||
# nixpkgs-unstable still ships 1.26.3 — the bump is merged to nixpkgs staging
|
||||
# but the large-rebuild staging->unstable pipeline lags. The 26.05 line is
|
||||
# otherwise current (dev tools match unstable). Switch back to nixpkgs-unstable
|
||||
# once it ships go_1_26 >= 1.26.4.
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/staging-next-26.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
# Reusable Go flake checks (build/test/lint/format); CI runs them via
|
||||
# `nix build .#checks.<system>.<name>` instead of bespoke per-tool steps.
|
||||
flake-checks.url = "github:kradalby/flake-checks";
|
||||
flake-checks.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self
|
||||
, nixpkgs
|
||||
, flake-utils
|
||||
, flake-checks
|
||||
, ...
|
||||
}:
|
||||
let
|
||||
|
|
@ -26,6 +36,7 @@
|
|||
overlays.default = _: prev:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${prev.stdenv.hostPlatform.system};
|
||||
# Go 1.26 builder; resolves to Go 1.26.4 from the pinned nixpkgs.
|
||||
buildGo = pkgs.buildGo126Module;
|
||||
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
|
||||
in
|
||||
|
|
@ -60,53 +71,20 @@
|
|||
subPackages = [ "cmd/hi" ];
|
||||
};
|
||||
|
||||
protoc-gen-grpc-gateway = buildGo rec {
|
||||
pname = "grpc-gateway";
|
||||
version = "2.28.0";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "grpc-ecosystem";
|
||||
repo = "grpc-gateway";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-93omvHb+b+S0w4D+FGEEwYYDjgumJFDAruc1P4elfvA=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-jVP5zfFPfHeAEApKNJzZwuZLA+DjKgkL7m2DFG72UNs=";
|
||||
|
||||
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||
|
||||
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
|
||||
};
|
||||
|
||||
protobuf-language-server = buildGo rec {
|
||||
pname = "protobuf-language-server";
|
||||
version = "ab4c128";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "lasorda";
|
||||
repo = "protobuf-language-server";
|
||||
rev = "ab4c128f00774d51bd6d1f4cfa735f4b7c8619e3";
|
||||
sha256 = "sha256-yF6kG+qTRxVO/qp2V9HgTyFBeOm5RQzeqdZFrdidwxM=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-4nTpKBe7ekJsfQf+P6edT/9Vp2SBYbKz1ITawD3bhkI=";
|
||||
|
||||
subPackages = [ "." ];
|
||||
};
|
||||
|
||||
# Build golangci-lint with Go 1.26 (upstream uses hardcoded Go version)
|
||||
# Build golangci-lint with stock Go 1.26 (upstream uses hardcoded Go
|
||||
# version); it does not build against the pinned 1.26.4.
|
||||
golangci-lint = buildGo rec {
|
||||
pname = "golangci-lint";
|
||||
version = "2.11.4";
|
||||
version = "2.12.2";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "golangci";
|
||||
repo = "golangci-lint";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-B19aLvfNRY9TOYw/71f2vpNUuSIz8OI4dL0ijGezsas=";
|
||||
hash = "sha256-qR7fp1x2S+EwEAcplRHTvA3jWwLr/XSiYKSZtAwkrNU=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-xuoj4+U4tB5gpABKq4Dbp2cxnljxdYoBbO8A7DqPM5E=";
|
||||
vendorHash = "sha256-AG5wtLwWLz55bdp1oi3cW+9O3yj1W1P7MV9zxym7Pb4=";
|
||||
|
||||
subPackages = [ "cmd/golangci-lint" ];
|
||||
|
||||
|
|
@ -179,6 +157,11 @@
|
|||
yq-go
|
||||
ripgrep
|
||||
postgresql
|
||||
|
||||
# External clients exercised by the Tailscale-compatible v2 API
|
||||
# roundtrip tests (TestAPIv2). Binaries: tofu, tscli.
|
||||
opentofu
|
||||
tscli
|
||||
python314Packages.mdformat
|
||||
python314Packages.mdformat-footnote
|
||||
python314Packages.mdformat-frontmatter
|
||||
|
|
@ -188,17 +171,8 @@
|
|||
# 'dot' is needed for pprof graphs
|
||||
# go tool pprof -http=: <source>
|
||||
graphviz
|
||||
|
||||
# Protobuf dependencies
|
||||
protobuf
|
||||
protoc-gen-go
|
||||
protoc-gen-go-grpc
|
||||
protoc-gen-grpc-gateway
|
||||
buf
|
||||
clang-tools # clang-format
|
||||
protobuf-language-server
|
||||
]
|
||||
++ lib.optional pkgs.stdenv.isLinux [ traceroute ];
|
||||
++ lib.optionals pkgs.stdenv.isLinux [ traceroute ];
|
||||
|
||||
# Add entry to build a docker image with headscale
|
||||
# caveat: only works on Linux
|
||||
|
|
@ -212,6 +186,59 @@
|
|||
contents = [ pkgs.headscale ];
|
||||
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
|
||||
};
|
||||
|
||||
# Go flake checks from the flake-checks library. CI gates on
|
||||
# `nix build .#checks.<system>.<name>`; the logic lives here, not in
|
||||
# bespoke workflow steps. Linux-only: parts of the tree are
|
||||
# Linux-specific and the pure unit subset is validated by CI.
|
||||
fc = flake-checks.lib;
|
||||
common = {
|
||||
inherit pkgs;
|
||||
root = ./.;
|
||||
pname = "headscale";
|
||||
version = headscaleVersion;
|
||||
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
|
||||
goPkg = pkgs.go_1_26;
|
||||
# //go:embed targets and test-read files outside the default whitelist.
|
||||
embedDirs = [ ./hscontrol/assets ./hscontrol/db/schema.sql ./config-example.yaml ];
|
||||
extraSrc = [
|
||||
./hscontrol/testdata
|
||||
./hscontrol/types/testdata
|
||||
./hscontrol/db/testdata
|
||||
./hscontrol/policy/v2/testdata
|
||||
];
|
||||
};
|
||||
goChecks = {
|
||||
build = fc.goBuild (common // { subPackages = [ "cmd/headscale" ]; });
|
||||
|
||||
# The pure unit subset. ./integration (Docker) and
|
||||
# ./hscontrol/servertest (slow: 10s+ convergence plus race/stress/HA
|
||||
# property tests — run by the servertest workflow instead) are dropped
|
||||
# from the test set but kept in source so cmd/hi and friends still
|
||||
# compile; TestPostgres* needs a server (the SQLite equivalents still
|
||||
# run). CGO off matches the build.
|
||||
gotest = fc.goTest (common // {
|
||||
testExclude = [ "/integration" "/hscontrol/servertest" ];
|
||||
goSkip = [ "TestPostgres" ];
|
||||
testEnv = "export CGO_ENABLED=0";
|
||||
});
|
||||
|
||||
# Full-tree golangci-lint (golines, gofumpt, etc.); uses the overlay's
|
||||
# golangci-lint built against the pinned Go.
|
||||
golangci-lint = fc.goLint common;
|
||||
|
||||
# nixpkgs-fmt + prettier, excluding generated output. goFmt = "off":
|
||||
# Go formatting (golines, gofumpt) is enforced by the golangci-lint
|
||||
# check, not treefmt. prettierExts matches the old prettier-lint glob
|
||||
# (no json: testdata fixtures are hand-formatted).
|
||||
formatting = fc.goFormat (common // {
|
||||
goFmt = "off";
|
||||
prettier = true;
|
||||
prettierExts = [ "ts" "js" "md" "yaml" "yml" "sass" "css" "scss" "html" ];
|
||||
# Mirror .prettierignore (docs/ are mkdocs-flavoured; gen/ generated).
|
||||
fmtExclude = [ ./gen ./docs ];
|
||||
});
|
||||
};
|
||||
in
|
||||
{
|
||||
# `nix develop`
|
||||
|
|
@ -229,7 +256,7 @@
|
|||
(pkgs.writeShellScriptBin
|
||||
"go-mod-update-all"
|
||||
''
|
||||
cat go.mod | ${pkgs.silver-searcher}/bin/ag "\t" | ${pkgs.silver-searcher}/bin/ag -v indirect | ${pkgs.gawk}/bin/awk '{print $1}' | ${pkgs.findutils}/bin/xargs go get -u
|
||||
cat go.mod | ${pkgs.ripgrep}/bin/rg "\t" | ${pkgs.ripgrep}/bin/rg -v indirect | ${pkgs.gawk}/bin/awk '{print $1}' | ${pkgs.findutils}/bin/xargs go get -u
|
||||
go mod tidy
|
||||
'')
|
||||
];
|
||||
|
|
@ -257,6 +284,9 @@
|
|||
|
||||
checks = {
|
||||
headscale = pkgs.testers.nixosTest (import ./nix/tests/headscale.nix);
|
||||
};
|
||||
}
|
||||
# The Go build/test checks are gated to Linux: parts of the tree are
|
||||
# Linux-specific and the pure unit subset is validated by CI.
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux goChecks;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"vendor": {
|
||||
"goModSum": "sha256-IE0n9cSqO4XNX4RN+CGBk9VC46iACiZKDFf/215iivk=",
|
||||
"sri": "sha256-ijEIP9NSomhlWOgsVN7tPvSuvkTiLtnvXvhZmatIDLM="
|
||||
"goModSum": "sha256-SJml8RXGmb2p0g1nOsHn86FA1hwgd5ZffLSkUj5zek8=",
|
||||
"sri": "sha256-pjGNuVtgFFzWNq/2cK7a4iyF13AfcHz098nk92a9Ido="
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4728
gen/client/v1/client.gen.go
Normal file
4728
gen/client/v1/client.gen.go
Normal file
File diff suppressed because it is too large
Load diff
4013
gen/client/v2/client.gen.go
Normal file
4013
gen/client/v2/client.gen.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,537 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/apikey.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ApiKey struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ApiKey) Reset() {
|
||||
*x = ApiKey{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ApiKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ApiKey) ProtoMessage() {}
|
||||
|
||||
func (x *ApiKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ApiKey.ProtoReflect.Descriptor instead.
|
||||
func (*ApiKey) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetLastSeen() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.LastSeen
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) Reset() {
|
||||
*x = CreateApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ApiKey string `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) Reset() {
|
||||
*x = CreateApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) GetApiKey() string {
|
||||
if x != nil {
|
||||
return x.ApiKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExpireApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) Reset() {
|
||||
*x = ExpireApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpireApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExpireApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpireApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExpireApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExpireApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyResponse) Reset() {
|
||||
*x = ExpireApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpireApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExpireApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpireApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExpireApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type ListApiKeysRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListApiKeysRequest) Reset() {
|
||||
*x = ListApiKeysRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListApiKeysRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListApiKeysRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListApiKeysRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListApiKeysRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListApiKeysRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
type ListApiKeysResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ApiKeys []*ApiKey `protobuf:"bytes,1,rep,name=api_keys,json=apiKeys,proto3" json:"api_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) Reset() {
|
||||
*x = ListApiKeysResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListApiKeysResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListApiKeysResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListApiKeysResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListApiKeysResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) GetApiKeys() []*ApiKey {
|
||||
if x != nil {
|
||||
return x.ApiKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) Reset() {
|
||||
*x = DeleteApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyResponse) Reset() {
|
||||
*x = DeleteApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
var File_headscale_v1_apikey_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_apikey_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/apikey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe0\x01\n" +
|
||||
"\x06ApiKey\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" +
|
||||
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x127\n" +
|
||||
"\tlast_seen\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\"Q\n" +
|
||||
"\x13CreateApiKeyRequest\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\"/\n" +
|
||||
"\x14CreateApiKeyResponse\x12\x17\n" +
|
||||
"\aapi_key\x18\x01 \x01(\tR\x06apiKey\"=\n" +
|
||||
"\x13ExpireApiKeyRequest\x12\x16\n" +
|
||||
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
|
||||
"\x14ExpireApiKeyResponse\"\x14\n" +
|
||||
"\x12ListApiKeysRequest\"F\n" +
|
||||
"\x13ListApiKeysResponse\x12/\n" +
|
||||
"\bapi_keys\x18\x01 \x03(\v2\x14.headscale.v1.ApiKeyR\aapiKeys\"=\n" +
|
||||
"\x13DeleteApiKeyRequest\x12\x16\n" +
|
||||
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
|
||||
"\x14DeleteApiKeyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_apikey_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_apikey_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_apikey_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_apikey_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_apikey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_apikey_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_apikey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_apikey_proto_goTypes = []any{
|
||||
(*ApiKey)(nil), // 0: headscale.v1.ApiKey
|
||||
(*CreateApiKeyRequest)(nil), // 1: headscale.v1.CreateApiKeyRequest
|
||||
(*CreateApiKeyResponse)(nil), // 2: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyRequest)(nil), // 3: headscale.v1.ExpireApiKeyRequest
|
||||
(*ExpireApiKeyResponse)(nil), // 4: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysRequest)(nil), // 5: headscale.v1.ListApiKeysRequest
|
||||
(*ListApiKeysResponse)(nil), // 6: headscale.v1.ListApiKeysResponse
|
||||
(*DeleteApiKeyRequest)(nil), // 7: headscale.v1.DeleteApiKeyRequest
|
||||
(*DeleteApiKeyResponse)(nil), // 8: headscale.v1.DeleteApiKeyResponse
|
||||
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_apikey_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.ApiKey.expiration:type_name -> google.protobuf.Timestamp
|
||||
9, // 1: headscale.v1.ApiKey.created_at:type_name -> google.protobuf.Timestamp
|
||||
9, // 2: headscale.v1.ApiKey.last_seen:type_name -> google.protobuf.Timestamp
|
||||
9, // 3: headscale.v1.CreateApiKeyRequest.expiration:type_name -> google.protobuf.Timestamp
|
||||
0, // 4: headscale.v1.ListApiKeysResponse.api_keys:type_name -> headscale.v1.ApiKey
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_apikey_proto_init() }
|
||||
func file_headscale_v1_apikey_proto_init() {
|
||||
if File_headscale_v1_apikey_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_apikey_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_apikey_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_apikey_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_apikey_proto = out.File
|
||||
file_headscale_v1_apikey_proto_goTypes = nil
|
||||
file_headscale_v1_apikey_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/auth.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type AuthRegisterRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
AuthId string `protobuf:"bytes,2,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) Reset() {
|
||||
*x = AuthRegisterRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRegisterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRegisterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRegisterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRegisterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthRegisterResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Node *Node `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) Reset() {
|
||||
*x = AuthRegisterResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRegisterResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRegisterResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRegisterResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRegisterResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) GetNode() *Node {
|
||||
if x != nil {
|
||||
return x.Node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthApproveRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) Reset() {
|
||||
*x = AuthApproveRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthApproveRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthApproveRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthApproveRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthApproveRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthApproveResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthApproveResponse) Reset() {
|
||||
*x = AuthApproveResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthApproveResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthApproveResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthApproveResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthApproveResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthApproveResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
type AuthRejectRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) Reset() {
|
||||
*x = AuthRejectRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRejectRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRejectRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRejectRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRejectRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthRejectResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRejectResponse) Reset() {
|
||||
*x = AuthRejectResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRejectResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRejectResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRejectResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRejectResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRejectResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
var File_headscale_v1_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_auth_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17headscale/v1/auth.proto\x12\fheadscale.v1\x1a\x17headscale/v1/node.proto\"B\n" +
|
||||
"\x13AuthRegisterRequest\x12\x12\n" +
|
||||
"\x04user\x18\x01 \x01(\tR\x04user\x12\x17\n" +
|
||||
"\aauth_id\x18\x02 \x01(\tR\x06authId\">\n" +
|
||||
"\x14AuthRegisterResponse\x12&\n" +
|
||||
"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"-\n" +
|
||||
"\x12AuthApproveRequest\x12\x17\n" +
|
||||
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x15\n" +
|
||||
"\x13AuthApproveResponse\",\n" +
|
||||
"\x11AuthRejectRequest\x12\x17\n" +
|
||||
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x14\n" +
|
||||
"\x12AuthRejectResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_auth_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_auth_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_auth_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_auth_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_headscale_v1_auth_proto_goTypes = []any{
|
||||
(*AuthRegisterRequest)(nil), // 0: headscale.v1.AuthRegisterRequest
|
||||
(*AuthRegisterResponse)(nil), // 1: headscale.v1.AuthRegisterResponse
|
||||
(*AuthApproveRequest)(nil), // 2: headscale.v1.AuthApproveRequest
|
||||
(*AuthApproveResponse)(nil), // 3: headscale.v1.AuthApproveResponse
|
||||
(*AuthRejectRequest)(nil), // 4: headscale.v1.AuthRejectRequest
|
||||
(*AuthRejectResponse)(nil), // 5: headscale.v1.AuthRejectResponse
|
||||
(*Node)(nil), // 6: headscale.v1.Node
|
||||
}
|
||||
var file_headscale_v1_auth_proto_depIdxs = []int32{
|
||||
6, // 0: headscale.v1.AuthRegisterResponse.node:type_name -> headscale.v1.Node
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_auth_proto_init() }
|
||||
func file_headscale_v1_auth_proto_init() {
|
||||
if File_headscale_v1_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_node_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_auth_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_auth_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_auth_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_auth_proto = out.File
|
||||
file_headscale_v1_auth_proto_goTypes = nil
|
||||
file_headscale_v1_auth_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,890 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/device.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Latency struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
LatencyMs float32 `protobuf:"fixed32,1,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"`
|
||||
Preferred bool `protobuf:"varint,2,opt,name=preferred,proto3" json:"preferred,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Latency) Reset() {
|
||||
*x = Latency{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Latency) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Latency) ProtoMessage() {}
|
||||
|
||||
func (x *Latency) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Latency.ProtoReflect.Descriptor instead.
|
||||
func (*Latency) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Latency) GetLatencyMs() float32 {
|
||||
if x != nil {
|
||||
return x.LatencyMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Latency) GetPreferred() bool {
|
||||
if x != nil {
|
||||
return x.Preferred
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ClientSupports struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
HairPinning bool `protobuf:"varint,1,opt,name=hair_pinning,json=hairPinning,proto3" json:"hair_pinning,omitempty"`
|
||||
Ipv6 bool `protobuf:"varint,2,opt,name=ipv6,proto3" json:"ipv6,omitempty"`
|
||||
Pcp bool `protobuf:"varint,3,opt,name=pcp,proto3" json:"pcp,omitempty"`
|
||||
Pmp bool `protobuf:"varint,4,opt,name=pmp,proto3" json:"pmp,omitempty"`
|
||||
Udp bool `protobuf:"varint,5,opt,name=udp,proto3" json:"udp,omitempty"`
|
||||
Upnp bool `protobuf:"varint,6,opt,name=upnp,proto3" json:"upnp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientSupports) Reset() {
|
||||
*x = ClientSupports{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientSupports) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientSupports) ProtoMessage() {}
|
||||
|
||||
func (x *ClientSupports) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientSupports.ProtoReflect.Descriptor instead.
|
||||
func (*ClientSupports) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetHairPinning() bool {
|
||||
if x != nil {
|
||||
return x.HairPinning
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetIpv6() bool {
|
||||
if x != nil {
|
||||
return x.Ipv6
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetPcp() bool {
|
||||
if x != nil {
|
||||
return x.Pcp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetPmp() bool {
|
||||
if x != nil {
|
||||
return x.Pmp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetUdp() bool {
|
||||
if x != nil {
|
||||
return x.Udp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetUpnp() bool {
|
||||
if x != nil {
|
||||
return x.Upnp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ClientConnectivity struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Endpoints []string `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||
Derp string `protobuf:"bytes,2,opt,name=derp,proto3" json:"derp,omitempty"`
|
||||
MappingVariesByDestIp bool `protobuf:"varint,3,opt,name=mapping_varies_by_dest_ip,json=mappingVariesByDestIp,proto3" json:"mapping_varies_by_dest_ip,omitempty"`
|
||||
Latency map[string]*Latency `protobuf:"bytes,4,rep,name=latency,proto3" json:"latency,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
ClientSupports *ClientSupports `protobuf:"bytes,5,opt,name=client_supports,json=clientSupports,proto3" json:"client_supports,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) Reset() {
|
||||
*x = ClientConnectivity{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientConnectivity) ProtoMessage() {}
|
||||
|
||||
func (x *ClientConnectivity) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientConnectivity.ProtoReflect.Descriptor instead.
|
||||
func (*ClientConnectivity) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetEndpoints() []string {
|
||||
if x != nil {
|
||||
return x.Endpoints
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetDerp() string {
|
||||
if x != nil {
|
||||
return x.Derp
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetMappingVariesByDestIp() bool {
|
||||
if x != nil {
|
||||
return x.MappingVariesByDestIp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetLatency() map[string]*Latency {
|
||||
if x != nil {
|
||||
return x.Latency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetClientSupports() *ClientSupports {
|
||||
if x != nil {
|
||||
return x.ClientSupports
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetDeviceRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) Reset() {
|
||||
*x = GetDeviceRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetDeviceResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Addresses []string `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"`
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
ClientVersion string `protobuf:"bytes,6,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"`
|
||||
UpdateAvailable bool `protobuf:"varint,7,opt,name=update_available,json=updateAvailable,proto3" json:"update_available,omitempty"`
|
||||
Os string `protobuf:"bytes,8,opt,name=os,proto3" json:"os,omitempty"`
|
||||
Created *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created,proto3" json:"created,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
KeyExpiryDisabled bool `protobuf:"varint,11,opt,name=key_expiry_disabled,json=keyExpiryDisabled,proto3" json:"key_expiry_disabled,omitempty"`
|
||||
Expires *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expires,proto3" json:"expires,omitempty"`
|
||||
Authorized bool `protobuf:"varint,13,opt,name=authorized,proto3" json:"authorized,omitempty"`
|
||||
IsExternal bool `protobuf:"varint,14,opt,name=is_external,json=isExternal,proto3" json:"is_external,omitempty"`
|
||||
MachineKey string `protobuf:"bytes,15,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"`
|
||||
NodeKey string `protobuf:"bytes,16,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"`
|
||||
BlocksIncomingConnections bool `protobuf:"varint,17,opt,name=blocks_incoming_connections,json=blocksIncomingConnections,proto3" json:"blocks_incoming_connections,omitempty"`
|
||||
EnabledRoutes []string `protobuf:"bytes,18,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,19,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
ClientConnectivity *ClientConnectivity `protobuf:"bytes,20,opt,name=client_connectivity,json=clientConnectivity,proto3" json:"client_connectivity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) Reset() {
|
||||
*x = GetDeviceResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAddresses() []string {
|
||||
if x != nil {
|
||||
return x.Addresses
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetClientVersion() string {
|
||||
if x != nil {
|
||||
return x.ClientVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetUpdateAvailable() bool {
|
||||
if x != nil {
|
||||
return x.UpdateAvailable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetOs() string {
|
||||
if x != nil {
|
||||
return x.Os
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetCreated() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Created
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetLastSeen() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.LastSeen
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetKeyExpiryDisabled() bool {
|
||||
if x != nil {
|
||||
return x.KeyExpiryDisabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetExpires() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expires
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAuthorized() bool {
|
||||
if x != nil {
|
||||
return x.Authorized
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetIsExternal() bool {
|
||||
if x != nil {
|
||||
return x.IsExternal
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetMachineKey() string {
|
||||
if x != nil {
|
||||
return x.MachineKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetNodeKey() string {
|
||||
if x != nil {
|
||||
return x.NodeKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetBlocksIncomingConnections() bool {
|
||||
if x != nil {
|
||||
return x.BlocksIncomingConnections
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetClientConnectivity() *ClientConnectivity {
|
||||
if x != nil {
|
||||
return x.ClientConnectivity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteDeviceRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) Reset() {
|
||||
*x = DeleteDeviceRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteDeviceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteDeviceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteDeviceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteDeviceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DeleteDeviceResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceResponse) Reset() {
|
||||
*x = DeleteDeviceResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteDeviceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteDeviceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteDeviceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteDeviceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type GetDeviceRoutesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) Reset() {
|
||||
*x = GetDeviceRoutesRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRoutesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRoutesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRoutesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetDeviceRoutesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) Reset() {
|
||||
*x = GetDeviceRoutesResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRoutesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRoutesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRoutesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EnableDeviceRoutesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Routes []string `protobuf:"bytes,2,rep,name=routes,proto3" json:"routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) Reset() {
|
||||
*x = EnableDeviceRoutesRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EnableDeviceRoutesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EnableDeviceRoutesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*EnableDeviceRoutesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) GetRoutes() []string {
|
||||
if x != nil {
|
||||
return x.Routes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EnableDeviceRoutesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) Reset() {
|
||||
*x = EnableDeviceRoutesResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EnableDeviceRoutesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EnableDeviceRoutesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*EnableDeviceRoutesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_device_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_device_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/device.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"F\n" +
|
||||
"\aLatency\x12\x1d\n" +
|
||||
"\n" +
|
||||
"latency_ms\x18\x01 \x01(\x02R\tlatencyMs\x12\x1c\n" +
|
||||
"\tpreferred\x18\x02 \x01(\bR\tpreferred\"\x91\x01\n" +
|
||||
"\x0eClientSupports\x12!\n" +
|
||||
"\fhair_pinning\x18\x01 \x01(\bR\vhairPinning\x12\x12\n" +
|
||||
"\x04ipv6\x18\x02 \x01(\bR\x04ipv6\x12\x10\n" +
|
||||
"\x03pcp\x18\x03 \x01(\bR\x03pcp\x12\x10\n" +
|
||||
"\x03pmp\x18\x04 \x01(\bR\x03pmp\x12\x10\n" +
|
||||
"\x03udp\x18\x05 \x01(\bR\x03udp\x12\x12\n" +
|
||||
"\x04upnp\x18\x06 \x01(\bR\x04upnp\"\xe3\x02\n" +
|
||||
"\x12ClientConnectivity\x12\x1c\n" +
|
||||
"\tendpoints\x18\x01 \x03(\tR\tendpoints\x12\x12\n" +
|
||||
"\x04derp\x18\x02 \x01(\tR\x04derp\x128\n" +
|
||||
"\x19mapping_varies_by_dest_ip\x18\x03 \x01(\bR\x15mappingVariesByDestIp\x12G\n" +
|
||||
"\alatency\x18\x04 \x03(\v2-.headscale.v1.ClientConnectivity.LatencyEntryR\alatency\x12E\n" +
|
||||
"\x0fclient_supports\x18\x05 \x01(\v2\x1c.headscale.v1.ClientSupportsR\x0eclientSupports\x1aQ\n" +
|
||||
"\fLatencyEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12+\n" +
|
||||
"\x05value\x18\x02 \x01(\v2\x15.headscale.v1.LatencyR\x05value:\x028\x01\"\"\n" +
|
||||
"\x10GetDeviceRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"\xa0\x06\n" +
|
||||
"\x11GetDeviceResponse\x12\x1c\n" +
|
||||
"\taddresses\x18\x01 \x03(\tR\taddresses\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04user\x18\x03 \x01(\tR\x04user\x12\x12\n" +
|
||||
"\x04name\x18\x04 \x01(\tR\x04name\x12\x1a\n" +
|
||||
"\bhostname\x18\x05 \x01(\tR\bhostname\x12%\n" +
|
||||
"\x0eclient_version\x18\x06 \x01(\tR\rclientVersion\x12)\n" +
|
||||
"\x10update_available\x18\a \x01(\bR\x0fupdateAvailable\x12\x0e\n" +
|
||||
"\x02os\x18\b \x01(\tR\x02os\x124\n" +
|
||||
"\acreated\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x127\n" +
|
||||
"\tlast_seen\x18\n" +
|
||||
" \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\x12.\n" +
|
||||
"\x13key_expiry_disabled\x18\v \x01(\bR\x11keyExpiryDisabled\x124\n" +
|
||||
"\aexpires\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\aexpires\x12\x1e\n" +
|
||||
"\n" +
|
||||
"authorized\x18\r \x01(\bR\n" +
|
||||
"authorized\x12\x1f\n" +
|
||||
"\vis_external\x18\x0e \x01(\bR\n" +
|
||||
"isExternal\x12\x1f\n" +
|
||||
"\vmachine_key\x18\x0f \x01(\tR\n" +
|
||||
"machineKey\x12\x19\n" +
|
||||
"\bnode_key\x18\x10 \x01(\tR\anodeKey\x12>\n" +
|
||||
"\x1bblocks_incoming_connections\x18\x11 \x01(\bR\x19blocksIncomingConnections\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x12 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x13 \x03(\tR\x10advertisedRoutes\x12Q\n" +
|
||||
"\x13client_connectivity\x18\x14 \x01(\v2 .headscale.v1.ClientConnectivityR\x12clientConnectivity\"%\n" +
|
||||
"\x13DeleteDeviceRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" +
|
||||
"\x14DeleteDeviceResponse\"(\n" +
|
||||
"\x16GetDeviceRoutesRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"m\n" +
|
||||
"\x17GetDeviceRoutesResponse\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutes\"C\n" +
|
||||
"\x19EnableDeviceRoutesRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" +
|
||||
"\x06routes\x18\x02 \x03(\tR\x06routes\"p\n" +
|
||||
"\x1aEnableDeviceRoutesResponse\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutesB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_device_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_device_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_device_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_device_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_device_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_device_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_device_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_headscale_v1_device_proto_goTypes = []any{
|
||||
(*Latency)(nil), // 0: headscale.v1.Latency
|
||||
(*ClientSupports)(nil), // 1: headscale.v1.ClientSupports
|
||||
(*ClientConnectivity)(nil), // 2: headscale.v1.ClientConnectivity
|
||||
(*GetDeviceRequest)(nil), // 3: headscale.v1.GetDeviceRequest
|
||||
(*GetDeviceResponse)(nil), // 4: headscale.v1.GetDeviceResponse
|
||||
(*DeleteDeviceRequest)(nil), // 5: headscale.v1.DeleteDeviceRequest
|
||||
(*DeleteDeviceResponse)(nil), // 6: headscale.v1.DeleteDeviceResponse
|
||||
(*GetDeviceRoutesRequest)(nil), // 7: headscale.v1.GetDeviceRoutesRequest
|
||||
(*GetDeviceRoutesResponse)(nil), // 8: headscale.v1.GetDeviceRoutesResponse
|
||||
(*EnableDeviceRoutesRequest)(nil), // 9: headscale.v1.EnableDeviceRoutesRequest
|
||||
(*EnableDeviceRoutesResponse)(nil), // 10: headscale.v1.EnableDeviceRoutesResponse
|
||||
nil, // 11: headscale.v1.ClientConnectivity.LatencyEntry
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_device_proto_depIdxs = []int32{
|
||||
11, // 0: headscale.v1.ClientConnectivity.latency:type_name -> headscale.v1.ClientConnectivity.LatencyEntry
|
||||
1, // 1: headscale.v1.ClientConnectivity.client_supports:type_name -> headscale.v1.ClientSupports
|
||||
12, // 2: headscale.v1.GetDeviceResponse.created:type_name -> google.protobuf.Timestamp
|
||||
12, // 3: headscale.v1.GetDeviceResponse.last_seen:type_name -> google.protobuf.Timestamp
|
||||
12, // 4: headscale.v1.GetDeviceResponse.expires:type_name -> google.protobuf.Timestamp
|
||||
2, // 5: headscale.v1.GetDeviceResponse.client_connectivity:type_name -> headscale.v1.ClientConnectivity
|
||||
0, // 6: headscale.v1.ClientConnectivity.LatencyEntry.value:type_name -> headscale.v1.Latency
|
||||
7, // [7:7] is the sub-list for method output_type
|
||||
7, // [7:7] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_device_proto_init() }
|
||||
func file_headscale_v1_device_proto_init() {
|
||||
if File_headscale_v1_device_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_device_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_device_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_device_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_device_proto = out.File
|
||||
file_headscale_v1_device_proto_goTypes = nil
|
||||
file_headscale_v1_device_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/headscale.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type HealthRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthRequest) Reset() {
|
||||
*x = HealthRequest{}
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthRequest) ProtoMessage() {}
|
||||
|
||||
func (x *HealthRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead.
|
||||
func (*HealthRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type HealthResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DatabaseConnectivity bool `protobuf:"varint,1,opt,name=database_connectivity,json=databaseConnectivity,proto3" json:"database_connectivity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthResponse) Reset() {
|
||||
*x = HealthResponse{}
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HealthResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HealthResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *HealthResponse) GetDatabaseConnectivity() bool {
|
||||
if x != nil {
|
||||
return x.DatabaseConnectivity
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_headscale_v1_headscale_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_headscale_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1cheadscale/v1/headscale.proto\x12\fheadscale.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17headscale/v1/user.proto\x1a\x1dheadscale/v1/preauthkey.proto\x1a\x17headscale/v1/node.proto\x1a\x19headscale/v1/apikey.proto\x1a\x17headscale/v1/auth.proto\x1a\x19headscale/v1/policy.proto\"\x0f\n" +
|
||||
"\rHealthRequest\"E\n" +
|
||||
"\x0eHealthResponse\x123\n" +
|
||||
"\x15database_connectivity\x18\x01 \x01(\bR\x14databaseConnectivity2\xe0\x1a\n" +
|
||||
"\x10HeadscaleService\x12h\n" +
|
||||
"\n" +
|
||||
"CreateUser\x12\x1f.headscale.v1.CreateUserRequest\x1a .headscale.v1.CreateUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11:\x01*\"\f/api/v1/user\x12\x80\x01\n" +
|
||||
"\n" +
|
||||
"RenameUser\x12\x1f.headscale.v1.RenameUserRequest\x1a .headscale.v1.RenameUserResponse\"/\x82\xd3\xe4\x93\x02)\"'/api/v1/user/{old_id}/rename/{new_name}\x12j\n" +
|
||||
"\n" +
|
||||
"DeleteUser\x12\x1f.headscale.v1.DeleteUserRequest\x1a .headscale.v1.DeleteUserResponse\"\x19\x82\xd3\xe4\x93\x02\x13*\x11/api/v1/user/{id}\x12b\n" +
|
||||
"\tListUsers\x12\x1e.headscale.v1.ListUsersRequest\x1a\x1f.headscale.v1.ListUsersResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/user\x12\x80\x01\n" +
|
||||
"\x10CreatePreAuthKey\x12%.headscale.v1.CreatePreAuthKeyRequest\x1a&.headscale.v1.CreatePreAuthKeyResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/preauthkey\x12\x87\x01\n" +
|
||||
"\x10ExpirePreAuthKey\x12%.headscale.v1.ExpirePreAuthKeyRequest\x1a&.headscale.v1.ExpirePreAuthKeyResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/v1/preauthkey/expire\x12}\n" +
|
||||
"\x10DeletePreAuthKey\x12%.headscale.v1.DeletePreAuthKeyRequest\x1a&.headscale.v1.DeletePreAuthKeyResponse\"\x1a\x82\xd3\xe4\x93\x02\x14*\x12/api/v1/preauthkey\x12z\n" +
|
||||
"\x0fListPreAuthKeys\x12$.headscale.v1.ListPreAuthKeysRequest\x1a%.headscale.v1.ListPreAuthKeysResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/preauthkey\x12}\n" +
|
||||
"\x0fDebugCreateNode\x12$.headscale.v1.DebugCreateNodeRequest\x1a%.headscale.v1.DebugCreateNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/debug/node\x12f\n" +
|
||||
"\aGetNode\x12\x1c.headscale.v1.GetNodeRequest\x1a\x1d.headscale.v1.GetNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/node/{node_id}\x12n\n" +
|
||||
"\aSetTags\x12\x1c.headscale.v1.SetTagsRequest\x1a\x1d.headscale.v1.SetTagsResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/node/{node_id}/tags\x12\x96\x01\n" +
|
||||
"\x11SetApprovedRoutes\x12&.headscale.v1.SetApprovedRoutesRequest\x1a'.headscale.v1.SetApprovedRoutesResponse\"0\x82\xd3\xe4\x93\x02*:\x01*\"%/api/v1/node/{node_id}/approve_routes\x12t\n" +
|
||||
"\fRegisterNode\x12!.headscale.v1.RegisterNodeRequest\x1a\".headscale.v1.RegisterNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x15/api/v1/node/register\x12o\n" +
|
||||
"\n" +
|
||||
"DeleteNode\x12\x1f.headscale.v1.DeleteNodeRequest\x1a .headscale.v1.DeleteNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/node/{node_id}\x12v\n" +
|
||||
"\n" +
|
||||
"ExpireNode\x12\x1f.headscale.v1.ExpireNodeRequest\x1a .headscale.v1.ExpireNodeResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1d/api/v1/node/{node_id}/expire\x12\x81\x01\n" +
|
||||
"\n" +
|
||||
"RenameNode\x12\x1f.headscale.v1.RenameNodeRequest\x1a .headscale.v1.RenameNodeResponse\"0\x82\xd3\xe4\x93\x02*\"(/api/v1/node/{node_id}/rename/{new_name}\x12b\n" +
|
||||
"\tListNodes\x12\x1e.headscale.v1.ListNodesRequest\x1a\x1f.headscale.v1.ListNodesResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/node\x12\x80\x01\n" +
|
||||
"\x0fBackfillNodeIPs\x12$.headscale.v1.BackfillNodeIPsRequest\x1a%.headscale.v1.BackfillNodeIPsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x18/api/v1/node/backfillips\x12w\n" +
|
||||
"\fAuthRegister\x12!.headscale.v1.AuthRegisterRequest\x1a\".headscale.v1.AuthRegisterResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/register\x12s\n" +
|
||||
"\vAuthApprove\x12 .headscale.v1.AuthApproveRequest\x1a!.headscale.v1.AuthApproveResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/auth/approve\x12o\n" +
|
||||
"\n" +
|
||||
"AuthReject\x12\x1f.headscale.v1.AuthRejectRequest\x1a .headscale.v1.AuthRejectResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/api/v1/auth/reject\x12p\n" +
|
||||
"\fCreateApiKey\x12!.headscale.v1.CreateApiKeyRequest\x1a\".headscale.v1.CreateApiKeyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\"\x0e/api/v1/apikey\x12w\n" +
|
||||
"\fExpireApiKey\x12!.headscale.v1.ExpireApiKeyRequest\x1a\".headscale.v1.ExpireApiKeyResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/apikey/expire\x12j\n" +
|
||||
"\vListApiKeys\x12 .headscale.v1.ListApiKeysRequest\x1a!.headscale.v1.ListApiKeysResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/apikey\x12v\n" +
|
||||
"\fDeleteApiKey\x12!.headscale.v1.DeleteApiKeyRequest\x1a\".headscale.v1.DeleteApiKeyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19*\x17/api/v1/apikey/{prefix}\x12d\n" +
|
||||
"\tGetPolicy\x12\x1e.headscale.v1.GetPolicyRequest\x1a\x1f.headscale.v1.GetPolicyResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/policy\x12g\n" +
|
||||
"\tSetPolicy\x12\x1e.headscale.v1.SetPolicyRequest\x1a\x1f.headscale.v1.SetPolicyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\x1a\x0e/api/v1/policy\x12s\n" +
|
||||
"\vCheckPolicy\x12 .headscale.v1.CheckPolicyRequest\x1a!.headscale.v1.CheckPolicyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/policy/check\x12[\n" +
|
||||
"\x06Health\x12\x1b.headscale.v1.HealthRequest\x1a\x1c.headscale.v1.HealthResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/healthB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_headscale_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_headscale_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_headscale_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_headscale_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_headscale_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_headscale_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_headscale_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_headscale_v1_headscale_proto_goTypes = []any{
|
||||
(*HealthRequest)(nil), // 0: headscale.v1.HealthRequest
|
||||
(*HealthResponse)(nil), // 1: headscale.v1.HealthResponse
|
||||
(*CreateUserRequest)(nil), // 2: headscale.v1.CreateUserRequest
|
||||
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
|
||||
(*DeleteUserRequest)(nil), // 4: headscale.v1.DeleteUserRequest
|
||||
(*ListUsersRequest)(nil), // 5: headscale.v1.ListUsersRequest
|
||||
(*CreatePreAuthKeyRequest)(nil), // 6: headscale.v1.CreatePreAuthKeyRequest
|
||||
(*ExpirePreAuthKeyRequest)(nil), // 7: headscale.v1.ExpirePreAuthKeyRequest
|
||||
(*DeletePreAuthKeyRequest)(nil), // 8: headscale.v1.DeletePreAuthKeyRequest
|
||||
(*ListPreAuthKeysRequest)(nil), // 9: headscale.v1.ListPreAuthKeysRequest
|
||||
(*DebugCreateNodeRequest)(nil), // 10: headscale.v1.DebugCreateNodeRequest
|
||||
(*GetNodeRequest)(nil), // 11: headscale.v1.GetNodeRequest
|
||||
(*SetTagsRequest)(nil), // 12: headscale.v1.SetTagsRequest
|
||||
(*SetApprovedRoutesRequest)(nil), // 13: headscale.v1.SetApprovedRoutesRequest
|
||||
(*RegisterNodeRequest)(nil), // 14: headscale.v1.RegisterNodeRequest
|
||||
(*DeleteNodeRequest)(nil), // 15: headscale.v1.DeleteNodeRequest
|
||||
(*ExpireNodeRequest)(nil), // 16: headscale.v1.ExpireNodeRequest
|
||||
(*RenameNodeRequest)(nil), // 17: headscale.v1.RenameNodeRequest
|
||||
(*ListNodesRequest)(nil), // 18: headscale.v1.ListNodesRequest
|
||||
(*BackfillNodeIPsRequest)(nil), // 19: headscale.v1.BackfillNodeIPsRequest
|
||||
(*AuthRegisterRequest)(nil), // 20: headscale.v1.AuthRegisterRequest
|
||||
(*AuthApproveRequest)(nil), // 21: headscale.v1.AuthApproveRequest
|
||||
(*AuthRejectRequest)(nil), // 22: headscale.v1.AuthRejectRequest
|
||||
(*CreateApiKeyRequest)(nil), // 23: headscale.v1.CreateApiKeyRequest
|
||||
(*ExpireApiKeyRequest)(nil), // 24: headscale.v1.ExpireApiKeyRequest
|
||||
(*ListApiKeysRequest)(nil), // 25: headscale.v1.ListApiKeysRequest
|
||||
(*DeleteApiKeyRequest)(nil), // 26: headscale.v1.DeleteApiKeyRequest
|
||||
(*GetPolicyRequest)(nil), // 27: headscale.v1.GetPolicyRequest
|
||||
(*SetPolicyRequest)(nil), // 28: headscale.v1.SetPolicyRequest
|
||||
(*CheckPolicyRequest)(nil), // 29: headscale.v1.CheckPolicyRequest
|
||||
(*CreateUserResponse)(nil), // 30: headscale.v1.CreateUserResponse
|
||||
(*RenameUserResponse)(nil), // 31: headscale.v1.RenameUserResponse
|
||||
(*DeleteUserResponse)(nil), // 32: headscale.v1.DeleteUserResponse
|
||||
(*ListUsersResponse)(nil), // 33: headscale.v1.ListUsersResponse
|
||||
(*CreatePreAuthKeyResponse)(nil), // 34: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 35: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*DeletePreAuthKeyResponse)(nil), // 36: headscale.v1.DeletePreAuthKeyResponse
|
||||
(*ListPreAuthKeysResponse)(nil), // 37: headscale.v1.ListPreAuthKeysResponse
|
||||
(*DebugCreateNodeResponse)(nil), // 38: headscale.v1.DebugCreateNodeResponse
|
||||
(*GetNodeResponse)(nil), // 39: headscale.v1.GetNodeResponse
|
||||
(*SetTagsResponse)(nil), // 40: headscale.v1.SetTagsResponse
|
||||
(*SetApprovedRoutesResponse)(nil), // 41: headscale.v1.SetApprovedRoutesResponse
|
||||
(*RegisterNodeResponse)(nil), // 42: headscale.v1.RegisterNodeResponse
|
||||
(*DeleteNodeResponse)(nil), // 43: headscale.v1.DeleteNodeResponse
|
||||
(*ExpireNodeResponse)(nil), // 44: headscale.v1.ExpireNodeResponse
|
||||
(*RenameNodeResponse)(nil), // 45: headscale.v1.RenameNodeResponse
|
||||
(*ListNodesResponse)(nil), // 46: headscale.v1.ListNodesResponse
|
||||
(*BackfillNodeIPsResponse)(nil), // 47: headscale.v1.BackfillNodeIPsResponse
|
||||
(*AuthRegisterResponse)(nil), // 48: headscale.v1.AuthRegisterResponse
|
||||
(*AuthApproveResponse)(nil), // 49: headscale.v1.AuthApproveResponse
|
||||
(*AuthRejectResponse)(nil), // 50: headscale.v1.AuthRejectResponse
|
||||
(*CreateApiKeyResponse)(nil), // 51: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyResponse)(nil), // 52: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysResponse)(nil), // 53: headscale.v1.ListApiKeysResponse
|
||||
(*DeleteApiKeyResponse)(nil), // 54: headscale.v1.DeleteApiKeyResponse
|
||||
(*GetPolicyResponse)(nil), // 55: headscale.v1.GetPolicyResponse
|
||||
(*SetPolicyResponse)(nil), // 56: headscale.v1.SetPolicyResponse
|
||||
(*CheckPolicyResponse)(nil), // 57: headscale.v1.CheckPolicyResponse
|
||||
}
|
||||
var file_headscale_v1_headscale_proto_depIdxs = []int32{
|
||||
2, // 0: headscale.v1.HeadscaleService.CreateUser:input_type -> headscale.v1.CreateUserRequest
|
||||
3, // 1: headscale.v1.HeadscaleService.RenameUser:input_type -> headscale.v1.RenameUserRequest
|
||||
4, // 2: headscale.v1.HeadscaleService.DeleteUser:input_type -> headscale.v1.DeleteUserRequest
|
||||
5, // 3: headscale.v1.HeadscaleService.ListUsers:input_type -> headscale.v1.ListUsersRequest
|
||||
6, // 4: headscale.v1.HeadscaleService.CreatePreAuthKey:input_type -> headscale.v1.CreatePreAuthKeyRequest
|
||||
7, // 5: headscale.v1.HeadscaleService.ExpirePreAuthKey:input_type -> headscale.v1.ExpirePreAuthKeyRequest
|
||||
8, // 6: headscale.v1.HeadscaleService.DeletePreAuthKey:input_type -> headscale.v1.DeletePreAuthKeyRequest
|
||||
9, // 7: headscale.v1.HeadscaleService.ListPreAuthKeys:input_type -> headscale.v1.ListPreAuthKeysRequest
|
||||
10, // 8: headscale.v1.HeadscaleService.DebugCreateNode:input_type -> headscale.v1.DebugCreateNodeRequest
|
||||
11, // 9: headscale.v1.HeadscaleService.GetNode:input_type -> headscale.v1.GetNodeRequest
|
||||
12, // 10: headscale.v1.HeadscaleService.SetTags:input_type -> headscale.v1.SetTagsRequest
|
||||
13, // 11: headscale.v1.HeadscaleService.SetApprovedRoutes:input_type -> headscale.v1.SetApprovedRoutesRequest
|
||||
14, // 12: headscale.v1.HeadscaleService.RegisterNode:input_type -> headscale.v1.RegisterNodeRequest
|
||||
15, // 13: headscale.v1.HeadscaleService.DeleteNode:input_type -> headscale.v1.DeleteNodeRequest
|
||||
16, // 14: headscale.v1.HeadscaleService.ExpireNode:input_type -> headscale.v1.ExpireNodeRequest
|
||||
17, // 15: headscale.v1.HeadscaleService.RenameNode:input_type -> headscale.v1.RenameNodeRequest
|
||||
18, // 16: headscale.v1.HeadscaleService.ListNodes:input_type -> headscale.v1.ListNodesRequest
|
||||
19, // 17: headscale.v1.HeadscaleService.BackfillNodeIPs:input_type -> headscale.v1.BackfillNodeIPsRequest
|
||||
20, // 18: headscale.v1.HeadscaleService.AuthRegister:input_type -> headscale.v1.AuthRegisterRequest
|
||||
21, // 19: headscale.v1.HeadscaleService.AuthApprove:input_type -> headscale.v1.AuthApproveRequest
|
||||
22, // 20: headscale.v1.HeadscaleService.AuthReject:input_type -> headscale.v1.AuthRejectRequest
|
||||
23, // 21: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
|
||||
24, // 22: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
|
||||
25, // 23: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
|
||||
26, // 24: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest
|
||||
27, // 25: headscale.v1.HeadscaleService.GetPolicy:input_type -> headscale.v1.GetPolicyRequest
|
||||
28, // 26: headscale.v1.HeadscaleService.SetPolicy:input_type -> headscale.v1.SetPolicyRequest
|
||||
29, // 27: headscale.v1.HeadscaleService.CheckPolicy:input_type -> headscale.v1.CheckPolicyRequest
|
||||
0, // 28: headscale.v1.HeadscaleService.Health:input_type -> headscale.v1.HealthRequest
|
||||
30, // 29: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse
|
||||
31, // 30: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse
|
||||
32, // 31: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse
|
||||
33, // 32: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse
|
||||
34, // 33: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
|
||||
35, // 34: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
|
||||
36, // 35: headscale.v1.HeadscaleService.DeletePreAuthKey:output_type -> headscale.v1.DeletePreAuthKeyResponse
|
||||
37, // 36: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
|
||||
38, // 37: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse
|
||||
39, // 38: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse
|
||||
40, // 39: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
|
||||
41, // 40: headscale.v1.HeadscaleService.SetApprovedRoutes:output_type -> headscale.v1.SetApprovedRoutesResponse
|
||||
42, // 41: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse
|
||||
43, // 42: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse
|
||||
44, // 43: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse
|
||||
45, // 44: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse
|
||||
46, // 45: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse
|
||||
47, // 46: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse
|
||||
48, // 47: headscale.v1.HeadscaleService.AuthRegister:output_type -> headscale.v1.AuthRegisterResponse
|
||||
49, // 48: headscale.v1.HeadscaleService.AuthApprove:output_type -> headscale.v1.AuthApproveResponse
|
||||
50, // 49: headscale.v1.HeadscaleService.AuthReject:output_type -> headscale.v1.AuthRejectResponse
|
||||
51, // 50: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
|
||||
52, // 51: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
|
||||
53, // 52: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
|
||||
54, // 53: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse
|
||||
55, // 54: headscale.v1.HeadscaleService.GetPolicy:output_type -> headscale.v1.GetPolicyResponse
|
||||
56, // 55: headscale.v1.HeadscaleService.SetPolicy:output_type -> headscale.v1.SetPolicyResponse
|
||||
57, // 56: headscale.v1.HeadscaleService.CheckPolicy:output_type -> headscale.v1.CheckPolicyResponse
|
||||
1, // 57: headscale.v1.HeadscaleService.Health:output_type -> headscale.v1.HealthResponse
|
||||
29, // [29:58] is the sub-list for method output_type
|
||||
0, // [0:29] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_headscale_proto_init() }
|
||||
func file_headscale_v1_headscale_proto_init() {
|
||||
if File_headscale_v1_headscale_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_user_proto_init()
|
||||
file_headscale_v1_preauthkey_proto_init()
|
||||
file_headscale_v1_node_proto_init()
|
||||
file_headscale_v1_apikey_proto_init()
|
||||
file_headscale_v1_auth_proto_init()
|
||||
file_headscale_v1_policy_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_headscale_v1_headscale_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_headscale_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_headscale_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_headscale_proto = out.File
|
||||
file_headscale_v1_headscale_proto_goTypes = nil
|
||||
file_headscale_v1_headscale_proto_depIdxs = nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,363 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/policy.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SetPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) Reset() {
|
||||
*x = SetPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SetPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) Reset() {
|
||||
*x = SetPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetPolicyRequest) Reset() {
|
||||
*x = GetPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type GetPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) Reset() {
|
||||
*x = GetPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CheckPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) Reset() {
|
||||
*x = CheckPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CheckPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CheckPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CheckPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CheckPolicyResponse) Reset() {
|
||||
*x = CheckPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CheckPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CheckPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CheckPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
var File_headscale_v1_policy_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_policy_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/policy.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"*\n" +
|
||||
"\x10SetPolicyRequest\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\"f\n" +
|
||||
"\x11SetPolicyResponse\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" +
|
||||
"\x10GetPolicyRequest\"f\n" +
|
||||
"\x11GetPolicyResponse\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\",\n" +
|
||||
"\x12CheckPolicyRequest\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\"\x15\n" +
|
||||
"\x13CheckPolicyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_policy_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_policy_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_policy_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_policy_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_policy_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_headscale_v1_policy_proto_goTypes = []any{
|
||||
(*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest
|
||||
(*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse
|
||||
(*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest
|
||||
(*GetPolicyResponse)(nil), // 3: headscale.v1.GetPolicyResponse
|
||||
(*CheckPolicyRequest)(nil), // 4: headscale.v1.CheckPolicyRequest
|
||||
(*CheckPolicyResponse)(nil), // 5: headscale.v1.CheckPolicyResponse
|
||||
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_policy_proto_depIdxs = []int32{
|
||||
6, // 0: headscale.v1.SetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
6, // 1: headscale.v1.GetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_policy_proto_init() }
|
||||
func file_headscale_v1_policy_proto_init() {
|
||||
if File_headscale_v1_policy_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_policy_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_policy_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_policy_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_policy_proto = out.File
|
||||
file_headscale_v1_policy_proto_goTypes = nil
|
||||
file_headscale_v1_policy_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,596 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/preauthkey.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type PreAuthKey struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Reusable bool `protobuf:"varint,4,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
||||
Ephemeral bool `protobuf:"varint,5,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
||||
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) Reset() {
|
||||
*x = PreAuthKey{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PreAuthKey) ProtoMessage() {}
|
||||
|
||||
func (x *PreAuthKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PreAuthKey.ProtoReflect.Descriptor instead.
|
||||
func (*PreAuthKey) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetReusable() bool {
|
||||
if x != nil {
|
||||
return x.Reusable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetEphemeral() bool {
|
||||
if x != nil {
|
||||
return x.Ephemeral
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetUsed() bool {
|
||||
if x != nil {
|
||||
return x.Used
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetAclTags() []string {
|
||||
if x != nil {
|
||||
return x.AclTags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreatePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User uint64 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
||||
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) Reset() {
|
||||
*x = CreatePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreatePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreatePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetUser() uint64 {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetReusable() bool {
|
||||
if x != nil {
|
||||
return x.Reusable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetEphemeral() bool {
|
||||
if x != nil {
|
||||
return x.Ephemeral
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
|
||||
if x != nil {
|
||||
return x.AclTags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreatePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PreAuthKey *PreAuthKey `protobuf:"bytes,1,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) Reset() {
|
||||
*x = CreatePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreatePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreatePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreatePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
|
||||
if x != nil {
|
||||
return x.PreAuthKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExpirePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) Reset() {
|
||||
*x = ExpirePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpirePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpirePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExpirePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) Reset() {
|
||||
*x = ExpirePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpirePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpirePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type DeletePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) Reset() {
|
||||
*x = DeletePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeletePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeletePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeletePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeletePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) Reset() {
|
||||
*x = DeletePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeletePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeletePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeletePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type ListPreAuthKeysRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) Reset() {
|
||||
*x = ListPreAuthKeysRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListPreAuthKeysRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListPreAuthKeysRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
type ListPreAuthKeysResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PreAuthKeys []*PreAuthKey `protobuf:"bytes,1,rep,name=pre_auth_keys,json=preAuthKeys,proto3" json:"pre_auth_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) Reset() {
|
||||
*x = ListPreAuthKeysResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListPreAuthKeysResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListPreAuthKeysResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListPreAuthKeysResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
|
||||
if x != nil {
|
||||
return x.PreAuthKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_preauthkey_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_preauthkey_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1dheadscale/v1/preauthkey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17headscale/v1/user.proto\"\xb6\x02\n" +
|
||||
"\n" +
|
||||
"PreAuthKey\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\x12\x10\n" +
|
||||
"\x03key\x18\x03 \x01(\tR\x03key\x12\x1a\n" +
|
||||
"\breusable\x18\x04 \x01(\bR\breusable\x12\x1c\n" +
|
||||
"\tephemeral\x18\x05 \x01(\bR\tephemeral\x12\x12\n" +
|
||||
"\x04used\x18\x06 \x01(\bR\x04used\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x19\n" +
|
||||
"\bacl_tags\x18\t \x03(\tR\aaclTags\"\xbe\x01\n" +
|
||||
"\x17CreatePreAuthKeyRequest\x12\x12\n" +
|
||||
"\x04user\x18\x01 \x01(\x04R\x04user\x12\x1a\n" +
|
||||
"\breusable\x18\x02 \x01(\bR\breusable\x12\x1c\n" +
|
||||
"\tephemeral\x18\x03 \x01(\bR\tephemeral\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x12\x19\n" +
|
||||
"\bacl_tags\x18\x05 \x03(\tR\aaclTags\"V\n" +
|
||||
"\x18CreatePreAuthKeyResponse\x12:\n" +
|
||||
"\fpre_auth_key\x18\x01 \x01(\v2\x18.headscale.v1.PreAuthKeyR\n" +
|
||||
"preAuthKey\")\n" +
|
||||
"\x17ExpirePreAuthKeyRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
|
||||
"\x18ExpirePreAuthKeyResponse\")\n" +
|
||||
"\x17DeletePreAuthKeyRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
|
||||
"\x18DeletePreAuthKeyResponse\"\x18\n" +
|
||||
"\x16ListPreAuthKeysRequest\"W\n" +
|
||||
"\x17ListPreAuthKeysResponse\x12<\n" +
|
||||
"\rpre_auth_keys\x18\x01 \x03(\v2\x18.headscale.v1.PreAuthKeyR\vpreAuthKeysB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_preauthkey_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_preauthkey_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_preauthkey_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_preauthkey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_preauthkey_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_preauthkey_proto_goTypes = []any{
|
||||
(*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey
|
||||
(*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest
|
||||
(*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyRequest)(nil), // 3: headscale.v1.ExpirePreAuthKeyRequest
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 4: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*DeletePreAuthKeyRequest)(nil), // 5: headscale.v1.DeletePreAuthKeyRequest
|
||||
(*DeletePreAuthKeyResponse)(nil), // 6: headscale.v1.DeletePreAuthKeyResponse
|
||||
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
|
||||
(*ListPreAuthKeysResponse)(nil), // 8: headscale.v1.ListPreAuthKeysResponse
|
||||
(*User)(nil), // 9: headscale.v1.User
|
||||
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_preauthkey_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.PreAuthKey.user:type_name -> headscale.v1.User
|
||||
10, // 1: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
|
||||
10, // 2: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
|
||||
10, // 3: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
|
||||
0, // 4: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
|
||||
0, // 5: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_preauthkey_proto_init() }
|
||||
func file_headscale_v1_preauthkey_proto_init() {
|
||||
if File_headscale_v1_preauthkey_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_user_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_preauthkey_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_preauthkey_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_preauthkey_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_preauthkey_proto = out.File
|
||||
file_headscale_v1_preauthkey_proto_goTypes = nil
|
||||
file_headscale_v1_preauthkey_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,615 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/user.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type User struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
ProviderId string `protobuf:"bytes,6,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"`
|
||||
Provider string `protobuf:"bytes,7,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||
ProfilePicUrl string `protobuf:"bytes,8,opt,name=profile_pic_url,json=profilePicUrl,proto3" json:"profile_pic_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *User) Reset() {
|
||||
*x = User{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *User) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*User) ProtoMessage() {}
|
||||
|
||||
func (x *User) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use User.ProtoReflect.Descriptor instead.
|
||||
func (*User) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *User) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *User) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProviderId() string {
|
||||
if x != nil {
|
||||
return x.ProviderId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProvider() string {
|
||||
if x != nil {
|
||||
return x.Provider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProfilePicUrl() string {
|
||||
if x != nil {
|
||||
return x.ProfilePicUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
PictureUrl string `protobuf:"bytes,4,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) Reset() {
|
||||
*x = CreateUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetPictureUrl() string {
|
||||
if x != nil {
|
||||
return x.PictureUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) Reset() {
|
||||
*x = CreateUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RenameUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
OldId uint64 `protobuf:"varint,1,opt,name=old_id,json=oldId,proto3" json:"old_id,omitempty"`
|
||||
NewName string `protobuf:"bytes,2,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) Reset() {
|
||||
*x = RenameUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RenameUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RenameUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RenameUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RenameUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) GetOldId() uint64 {
|
||||
if x != nil {
|
||||
return x.OldId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) GetNewName() string {
|
||||
if x != nil {
|
||||
return x.NewName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RenameUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) Reset() {
|
||||
*x = RenameUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RenameUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RenameUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RenameUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RenameUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) Reset() {
|
||||
*x = DeleteUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteUserResponse) Reset() {
|
||||
*x = DeleteUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type ListUsersRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) Reset() {
|
||||
*x = ListUsersRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUsersRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListUsersRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListUsersRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListUsersResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) Reset() {
|
||||
*x = ListUsersResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUsersResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListUsersResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListUsersResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) GetUsers() []*User {
|
||||
if x != nil {
|
||||
return x.Users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_user_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_user_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17headscale/v1/user.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x83\x02\n" +
|
||||
"\x04User\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12!\n" +
|
||||
"\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x14\n" +
|
||||
"\x05email\x18\x05 \x01(\tR\x05email\x12\x1f\n" +
|
||||
"\vprovider_id\x18\x06 \x01(\tR\n" +
|
||||
"providerId\x12\x1a\n" +
|
||||
"\bprovider\x18\a \x01(\tR\bprovider\x12&\n" +
|
||||
"\x0fprofile_pic_url\x18\b \x01(\tR\rprofilePicUrl\"\x81\x01\n" +
|
||||
"\x11CreateUserRequest\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12!\n" +
|
||||
"\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" +
|
||||
"\x05email\x18\x03 \x01(\tR\x05email\x12\x1f\n" +
|
||||
"\vpicture_url\x18\x04 \x01(\tR\n" +
|
||||
"pictureUrl\"<\n" +
|
||||
"\x12CreateUserResponse\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"E\n" +
|
||||
"\x11RenameUserRequest\x12\x15\n" +
|
||||
"\x06old_id\x18\x01 \x01(\x04R\x05oldId\x12\x19\n" +
|
||||
"\bnew_name\x18\x02 \x01(\tR\anewName\"<\n" +
|
||||
"\x12RenameUserResponse\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"#\n" +
|
||||
"\x11DeleteUserRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x14\n" +
|
||||
"\x12DeleteUserResponse\"L\n" +
|
||||
"\x10ListUsersRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05email\x18\x03 \x01(\tR\x05email\"=\n" +
|
||||
"\x11ListUsersResponse\x12(\n" +
|
||||
"\x05users\x18\x01 \x03(\v2\x12.headscale.v1.UserR\x05usersB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_user_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_user_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_user_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_user_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_user_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_user_proto_goTypes = []any{
|
||||
(*User)(nil), // 0: headscale.v1.User
|
||||
(*CreateUserRequest)(nil), // 1: headscale.v1.CreateUserRequest
|
||||
(*CreateUserResponse)(nil), // 2: headscale.v1.CreateUserResponse
|
||||
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
|
||||
(*RenameUserResponse)(nil), // 4: headscale.v1.RenameUserResponse
|
||||
(*DeleteUserRequest)(nil), // 5: headscale.v1.DeleteUserRequest
|
||||
(*DeleteUserResponse)(nil), // 6: headscale.v1.DeleteUserResponse
|
||||
(*ListUsersRequest)(nil), // 7: headscale.v1.ListUsersRequest
|
||||
(*ListUsersResponse)(nil), // 8: headscale.v1.ListUsersResponse
|
||||
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_user_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.User.created_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 1: headscale.v1.CreateUserResponse.user:type_name -> headscale.v1.User
|
||||
0, // 2: headscale.v1.RenameUserResponse.user:type_name -> headscale.v1.User
|
||||
0, // 3: headscale.v1.ListUsersResponse.users:type_name -> headscale.v1.User
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_user_proto_init() }
|
||||
func file_headscale_v1_user_proto_init() {
|
||||
if File_headscale_v1_user_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_user_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_user_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_user_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_user_proto = out.File
|
||||
file_headscale_v1_user_proto_goTypes = nil
|
||||
file_headscale_v1_user_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/apikey.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/auth.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/device.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/node.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/policy.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/preauthkey.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/user.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
go.mod
131
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/juanfont/headscale
|
||||
|
||||
go 1.26.2
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/arl/statsviz v0.8.0
|
||||
|
|
@ -8,54 +8,52 @@ require (
|
|||
github.com/chasefleming/elem-go v0.31.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-oidc/v3 v3.18.0
|
||||
github.com/creachadair/command v0.2.2
|
||||
github.com/creachadair/flax v0.0.5
|
||||
github.com/creachadair/command v0.2.6
|
||||
github.com/creachadair/flax v0.0.6
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jagottsicher/termcolor v1.0.2
|
||||
github.com/oapi-codegen/runtime v1.4.1
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/common v0.67.5
|
||||
github.com/prometheus/common v0.68.1
|
||||
github.com/pterm/pterm v0.12.83
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0
|
||||
github.com/realclientip/realclientip-go v1.0.0
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sasha-s/go-deadlock v0.3.9
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
github.com/tailscale/squibble v0.0.0-20260303070345-3ac5157f405e
|
||||
github.com/tailscale/tailsql v0.0.0-20260322172246-3ab0c1744d9c
|
||||
github.com/tailscale/squibble v0.0.0-20260411062017-141f5d618bc4
|
||||
github.com/tailscale/tailsql v0.0.0-20260522170732-77aec5aabc76
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
golang.org/x/sync v0.21.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
pgregory.net/rapid v1.2.0
|
||||
tailscale.com v1.97.0-pre.0.20260429005429-40088602c960
|
||||
pgregory.net/rapid v1.3.0
|
||||
tailscale.com v1.101.0-pre
|
||||
tailscale.com/client/tailscale/v2 v2.9.0
|
||||
zombiezen.com/go/postgrestest v1.0.1
|
||||
)
|
||||
|
||||
|
|
@ -77,10 +75,10 @@ require (
|
|||
// together, e.g:
|
||||
// go get modernc.org/libc@v1.55.3 modernc.org/sqlite@v1.33.1
|
||||
require (
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.48.2
|
||||
modernc.org/sqlite v1.52.0
|
||||
)
|
||||
|
||||
// NOTE: gvisor must be updated in lockstep with
|
||||
|
|
@ -93,7 +91,7 @@ require gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
|||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/keyboard v0.2.10 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
|
|
@ -104,6 +102,7 @@ require (
|
|||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
|
||||
|
|
@ -126,49 +125,50 @@ require (
|
|||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/continuity v0.5.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/creachadair/mds v0.26.2 // indirect
|
||||
github.com/creachadair/mds v0.29.0 // indirect
|
||||
github.com/creachadair/msync v0.8.2 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.4.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/cli v29.5.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect
|
||||
github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/gookit/color v1.6.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // 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.8.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.10.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
|
|
@ -176,18 +176,18 @@ require (
|
|||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||
github.com/kamstrup/intmap v0.5.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/lib/pq v1.11.1 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/mdlayher/netlink v1.8.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.54.1 // indirect
|
||||
github.com/moby/moby/client v0.4.0 // indirect
|
||||
github.com/moby/moby/api v1.54.2 // indirect
|
||||
github.com/moby/moby/client v0.4.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
|
|
@ -197,15 +197,14 @@ require (
|
|||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.3.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/peterbourgon/ff/v3 v3.4.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/pires/go-proxyproto v0.9.2 // 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.7.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/safchain/ethtool v0.7.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
|
|
@ -218,9 +217,9 @@ require (
|
|||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/setec v0.0.0-20260115174028-19d190c5556d // indirect
|
||||
github.com/tailscale/setec v0.0.0-20260310221408-dcd97e42f251 // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260304043104-4184faf59e56 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
|
||||
github.com/toqueteos/webbrowser v1.2.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
|
|
@ -228,24 +227,28 @@ require (
|
|||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
golang.org/x/image v0.27.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
golang.org/x/tools v0.45.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-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
k8s.io/client-go v0.34.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect
|
||||
|
|
|
|||
342
go.sum
342
go.sum
|
|
@ -4,8 +4,8 @@ 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=
|
||||
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
|
||||
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||
atomicgo.dev/keyboard v0.2.10 h1:v7mvUKUZLHIggxULEIuWbT+WkkyQSgdbA201EziAhHU=
|
||||
atomicgo.dev/keyboard v0.2.10/go.mod h1:ap/z5ilnhLqYq852m6kPeTq5Z6aESGWu5mzRpJlC6aI=
|
||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
|
|
@ -22,28 +22,23 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
|||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 h1:1ltqoej5GtaWF8jaiA49HwsZD459jqm9YFz9ZtMFpQA=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/arl/statsviz v0.8.0 h1:O6GjjVxEDxcByAucOSl29HaGYLXsuwA3ujJw8H9E7/U=
|
||||
github.com/arl/statsviz v0.8.0/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
||||
|
|
@ -90,6 +85,7 @@ github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZ
|
|||
github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo=
|
||||
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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
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.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
|
|
@ -113,11 +109,10 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
|
|||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
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/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
|
||||
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
|
|
@ -129,18 +124,20 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8
|
|||
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creachadair/command v0.2.2 h1:4RGsUhqFf1imFC+vMWOOCiQdncThCdcdMJp0JNCjxxc=
|
||||
github.com/creachadair/command v0.2.2/go.mod h1:Z6Zp6CSJcnaWWR4wHgdqzODnFdxFJAaa/DrcVkeUu3E=
|
||||
github.com/creachadair/flax v0.0.5 h1:zt+CRuXQASxwQ68e9GHAOnEgAU29nF0zYMHOCrL5wzE=
|
||||
github.com/creachadair/flax v0.0.5/go.mod h1:F1PML0JZLXSNDMNiRGK2yjm5f+L9QCHchyHBldFymj8=
|
||||
github.com/creachadair/mds v0.26.2 h1:rCtvEV/bCRY0hGfwvvMg0p3yzKgBE8l/9OV4fjF9QQ8=
|
||||
github.com/creachadair/mds v0.26.2/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI=
|
||||
github.com/creachadair/command v0.2.6 h1:+d4xUSZza5nqRVVsVRf5jDbix/0uKxVqFrc30F4uyRI=
|
||||
github.com/creachadair/command v0.2.6/go.mod h1:HWvS3zEpVakKlNXxT/j1U8tNoDaY7By7ORYQ+m1ha2U=
|
||||
github.com/creachadair/flax v0.0.6 h1:IFUdRdfKynNTyR5SRlrHMUGnxxMZZen8RVpwcDq/ftk=
|
||||
github.com/creachadair/flax v0.0.6/go.mod h1:F1PML0JZLXSNDMNiRGK2yjm5f+L9QCHchyHBldFymj8=
|
||||
github.com/creachadair/mds v0.29.0 h1:LyR8pWAj2ofsrdJ9s2YoIqN8b6a1djHmPQULD2fTSCA=
|
||||
github.com/creachadair/mds v0.29.0/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI=
|
||||
github.com/creachadair/msync v0.8.2 h1:ujvc/SVJPn+bFwmjUHucXNTTn3opVe2YbQ46mBCnP08=
|
||||
github.com/creachadair/msync v0.8.2/go.mod h1:LzxqD9kfIl/O3DczkwOgJplLPqwrTbIhINlf9bHIsEY=
|
||||
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.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0 h1:fb0WZCatnaiHLphMQDDWDjygNxfMkX/ENma3QsRl7vY=
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0/go.mod h1:k9hwjlgWFt1t2jsmQGlsgXAG2FBTZa4kkjV581qAtfo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
|
|
@ -155,12 +152,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM=
|
||||
github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
|
||||
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
|
|
@ -174,10 +171,10 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
|||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
|
|
@ -186,18 +183,18 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
|
|||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE=
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
|
@ -209,6 +206,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
|||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
||||
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=
|
||||
|
|
@ -224,8 +223,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
|
|
@ -242,26 +239,22 @@ github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdF
|
|||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno=
|
||||
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/pprof v0.0.0-20260604005048-7023385849c0 h1:h1QTMDl6q9wDvDCJVpKQSjgleGFYnd2fOxmg2K+6BGE=
|
||||
github.com/google/pprof v0.0.0-20260604005048-7023385849c0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
|
|
@ -280,8 +273,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM=
|
||||
|
|
@ -299,44 +292,38 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk=
|
||||
github.com/kamstrup/intmap v0.5.2/go.mod h1:gWUVWHKzWj8xpJVFf5GC0O26bWmv3GqdnIX/LMT6Aq4=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
|
||||
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
|
||||
|
|
@ -351,10 +338,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
|||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
|
||||
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
|
||||
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
|
|
@ -371,6 +358,10 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
|||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
|
||||
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
|
||||
github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo=
|
||||
github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
|
|
@ -382,15 +373,13 @@ github.com/opencontainers/runc v1.3.2/go.mod h1:F7UQQEsxcjUNnFpT1qPLHZBKYP7yWwk6
|
|||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
|
||||
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
|
||||
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1 h1:EMacvLRUd2O1K0eWod27ZP5CY1iTNkhBDLSN+Q4JEvA=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1/go.mod h1:qXbiq/2X4ZUMMshsqlWyTHOcw7ns+GZmlqZZN05ZHcQ=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||
|
|
@ -404,34 +393,26 @@ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Q
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
||||
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
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.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||
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/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
|
||||
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA=
|
||||
github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/realclientip/realclientip-go v1.0.0 h1:+yPxeC0mEaJzq1BfCt2h4BxlyrvIIBzR6suDc3BEF1U=
|
||||
github.com/realclientip/realclientip-go v1.0.0/go.mod h1:CXnUdVwFRcXFJIRb/dTYqbT7ud48+Pi2pFm80bxDmcI=
|
||||
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=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is=
|
||||
github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ=
|
||||
|
|
@ -441,9 +422,8 @@ github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
|||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w=
|
||||
github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
|
|
@ -459,13 +439,10 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
|
|
@ -487,18 +464,18 @@ github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4
|
|||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/setec v0.0.0-20260115174028-19d190c5556d h1:N+TtzIaGYREbLbKZB0WU0vVnMSfaqUkSf3qMEi03hwE=
|
||||
github.com/tailscale/setec v0.0.0-20260115174028-19d190c5556d/go.mod h1:6NU8H/GLPVX2TnXAY1duyy9ylLaHwFpr0X93UPiYmNI=
|
||||
github.com/tailscale/squibble v0.0.0-20260303070345-3ac5157f405e h1:4yfp5/YDr+TzbUME/PalYJVXAsp7zA2Gv2xQMZ9Qors=
|
||||
github.com/tailscale/squibble v0.0.0-20260303070345-3ac5157f405e/go.mod h1:xJkMmR3t+thnUQhA3Q4m2VSlS5pcOq+CIjmU/xfKKx4=
|
||||
github.com/tailscale/tailsql v0.0.0-20260322172246-3ab0c1744d9c h1:7lJQ/zycbk1E9e0nUiMuwIDYprFTLpWXUwiPdi+tRlI=
|
||||
github.com/tailscale/tailsql v0.0.0-20260322172246-3ab0c1744d9c/go.mod h1:bpNmZdvZKmBstrZunT+NXL6hmrFw5AsuT7MGiYS8sRc=
|
||||
github.com/tailscale/setec v0.0.0-20260310221408-dcd97e42f251 h1:kNnJlwxSzue+VRJuDdZ/yebcMM2q9KqFmK9xQqH1YRc=
|
||||
github.com/tailscale/setec v0.0.0-20260310221408-dcd97e42f251/go.mod h1:6NU8H/GLPVX2TnXAY1duyy9ylLaHwFpr0X93UPiYmNI=
|
||||
github.com/tailscale/squibble v0.0.0-20260411062017-141f5d618bc4 h1:1ghkd9YIC4J7umZuu9jZz8afWJSj1hCRSzfvdI2Q3Vo=
|
||||
github.com/tailscale/squibble v0.0.0-20260411062017-141f5d618bc4/go.mod h1:EVp9PDh7v69Do6aNzFfLvL0fdCph3XskGMi+WcS/uOM=
|
||||
github.com/tailscale/tailsql v0.0.0-20260522170732-77aec5aabc76 h1:o7mEEIci+U0H3Ddo0JRMOxm2VGcCKaVPro/F+f3qFbg=
|
||||
github.com/tailscale/tailsql v0.0.0-20260522170732-77aec5aabc76/go.mod h1:N2Dm+UzRWz01zPMDl5XHkSVq91kNPWU13uUUibcOm+c=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 h1:0tpDdAj9sSfSZg4gMwNTdqMP592sBrq2Sm0w6ipnh7k=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368/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-20260304043104-4184faf59e56 h1:/R1vu+eNhg1eKstmVPEKvsJgkh4TUyb+J+Eadwv+d/I=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260304043104-4184faf59e56/go.mod h1:zvaAPQrjUBWufXgqpSQ1/BYu9ZFOKnsNWLFQe+E78cM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||
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=
|
||||
|
|
@ -524,34 +501,33 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
|
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
|
|
@ -561,43 +537,39 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260603202125-055de637280b h1:E7MAoHE/7prIY6tu29UATfH3hVHv6IWqOchjE48pTAU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260603202125-055de637280b/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -605,59 +577,51 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
|
|
@ -674,10 +638,10 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
|||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
|
|
@ -686,29 +650,31 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
|
||||
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
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.97.0-pre.0.20260429005429-40088602c960 h1:I56vAGia4DV24Dbv8N07F/Awtnguvmm7PAgWCxCIdqw=
|
||||
tailscale.com v1.97.0-pre.0.20260429005429-40088602c960/go.mod h1:8nwFkmNdNRtTIM2dkmr/DhbzSKeLmzusWOTacX1zVKk=
|
||||
tailscale.com v1.101.0-pre h1:q1eBWxryj7Lz5fMvi7npSbN/fJ3q6/crvbbfMkx89F8=
|
||||
tailscale.com v1.101.0-pre/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
||||
tailscale.com/client/tailscale/v2 v2.9.0 h1:zBZIIeIYXL42qvvile7d29O2DKSr3AfNc2gzd1JCf2o=
|
||||
tailscale.com/client/tailscale/v2 v2.9.0/go.mod h1:FGjvGT3ThHelqo0gfdK3IN3k1dwNbRzYbQh2XO3C47U=
|
||||
zombiezen.com/go/postgrestest v1.0.1 h1:aXoADQAJmZDU3+xilYVut0pHhgc0sF8ZspPW9gFNwP4=
|
||||
zombiezen.com/go/postgrestest v1.0.1/go.mod h1:marlZezr+k2oSJrvXHnZUs1olHqpE9czlz8ZYkVxliQ=
|
||||
|
|
|
|||
160
hscontrol/api/v1/api.go
Normal file
160
hscontrol/api/v1/api.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// Package apiv1 is the code-first Huma implementation of the Headscale v1 API.
|
||||
// Handlers are a thin adapter over hscontrol/state; Huma emits the OpenAPI 3.1
|
||||
// spec from the Go definitions (see Spec), and that spec drives the client.
|
||||
//
|
||||
// It depends only on the domain layer (hscontrol/state, hscontrol/types) via
|
||||
// Backend, never on the hscontrol server package, so a future hscontrol/api/v2
|
||||
// can sit beside it without either importing the other.
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humachi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||
)
|
||||
|
||||
// Backend is the dependency surface the v1 API needs from the control plane:
|
||||
// the state layer, the change-notification sink that distributes updates to
|
||||
// connected nodes, and the config (only Policy.Mode and Policy.Path are read).
|
||||
type Backend struct {
|
||||
State *state.State
|
||||
Change func(...change.Change)
|
||||
Cfg *types.Config
|
||||
}
|
||||
|
||||
// NewAPI builds the v1 Huma API on the given chi router and registers every
|
||||
// operation. Auth is enforced by a Huma middleware driven by each operation's
|
||||
// declared bearer security (see authMiddleware); locally-trusted requests
|
||||
// bypass it via WithLocalTrust.
|
||||
func NewAPI(router chi.Router, backend Backend) huma.API {
|
||||
config := huma.DefaultConfig("Headscale API", "v1")
|
||||
config.Info.Description = "Headscale control server API."
|
||||
|
||||
// Version the OpenAPI/docs routes under /api/v1 so a future v2 owns its own.
|
||||
// These register as plain mux routes, not operations, so they never appear
|
||||
// in the emitted spec or client.
|
||||
config.OpenAPIPath = "/api/v1/openapi"
|
||||
config.DocsPath = "/api/v1/docs"
|
||||
|
||||
// The v1 API does not emit "$schema".
|
||||
config.SchemasPath = ""
|
||||
|
||||
// Drop the default schema-link create hook: it injects a "$schema" property
|
||||
// and Link header into every response, which the v1 contract omits.
|
||||
config.CreateHooks = nil
|
||||
|
||||
config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
|
||||
"bearer": {
|
||||
Type: "http",
|
||||
Scheme: "bearer",
|
||||
},
|
||||
}
|
||||
|
||||
api := humachi.New(router, config)
|
||||
|
||||
// Must run before register: Huma snapshots the middleware chain at operation
|
||||
// registration, so a middleware added afterwards would silently never run.
|
||||
api.UseMiddleware(authMiddleware(api, backend))
|
||||
|
||||
register(api, backend)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// bearerAuth is the security requirement applied to every operation: all
|
||||
// /api/v1 routes require an API key.
|
||||
var bearerAuth = []map[string][]string{{"bearer": {}}}
|
||||
|
||||
// registrations is populated by each resource file's init(), so adding a
|
||||
// resource group means adding a file rather than editing a shared point. Huma
|
||||
// sorts the emitted spec, so init order does not affect output.
|
||||
var registrations []func(huma.API, Backend)
|
||||
|
||||
// register wires up every operation contributed by the resource files.
|
||||
func register(api huma.API, b Backend) {
|
||||
for _, fn := range registrations {
|
||||
fn(api, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Spec emits the OpenAPI 3.1 document. The zero Backend is safe because
|
||||
// handlers are registered but never invoked during emission.
|
||||
func Spec() ([]byte, error) {
|
||||
api := NewAPI(chi.NewMux(), Backend{})
|
||||
return api.OpenAPI().YAML()
|
||||
}
|
||||
|
||||
// Spec30 emits the document downgraded to OpenAPI 3.0.3, needed because the
|
||||
// client generator (oapi-codegen v2) cannot yet read the 3.1 spec.
|
||||
func Spec30() ([]byte, error) {
|
||||
api := NewAPI(chi.NewMux(), Backend{})
|
||||
return api.OpenAPI().DowngradeYAML()
|
||||
}
|
||||
|
||||
// Handler builds the v1 API on a fresh mux and returns both. Callers mount the
|
||||
// mux and may use mux.Match to detect which paths this API serves.
|
||||
func Handler(backend Backend) (*chi.Mux, huma.API) {
|
||||
mux := chi.NewMux()
|
||||
api := NewAPI(mux, backend)
|
||||
|
||||
return mux, api
|
||||
}
|
||||
|
||||
// localTrustKey marks a request as arriving over a locally-trusted transport;
|
||||
// the auth middleware skips authentication for such requests.
|
||||
type localTrustKey struct{}
|
||||
|
||||
// WithLocalTrust wraps a handler so its requests bypass API-key authentication.
|
||||
// The unix socket uses this — access to the socket is the trust boundary — as
|
||||
// do in-process tests that exercise the mux directly.
|
||||
func WithLocalTrust(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
next.ServeHTTP(w, req.WithContext(
|
||||
context.WithValue(req.Context(), localTrustKey{}, struct{}{}),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// authMiddleware is a pure gate enforcing the bearer API key for any operation
|
||||
// that declares security; the v1 handlers do not read caller identity.
|
||||
// Locally-trusted requests and operations without declared security pass
|
||||
// through. b.State is nil only during spec emission, where no request is
|
||||
// served, so it is never dereferenced there.
|
||||
func authMiddleware(api huma.API, b Backend) func(huma.Context, func(huma.Context)) {
|
||||
return func(ctx huma.Context, next func(huma.Context)) {
|
||||
if ctx.Context().Value(localTrustKey{}) != nil {
|
||||
next(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(ctx.Operation().Security) == 0 {
|
||||
next(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token, ok := strings.CutPrefix(ctx.Header("Authorization"), "Bearer ")
|
||||
if !ok {
|
||||
_ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Unauthorized")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := b.State.ValidateAPIKey(token)
|
||||
if err != nil || !valid {
|
||||
_ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Unauthorized")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next(ctx)
|
||||
}
|
||||
}
|
||||
245
hscontrol/api/v1/apikeys.go
Normal file
245
hscontrol/api/v1/apikeys.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerApiKeys)
|
||||
}
|
||||
|
||||
// ApiKey is the v1 ApiKey message. Timestamps are pointers so a nil source is
|
||||
// emitted as JSON null, matching protojson's unset Timestamp (e.g. lastSeen on
|
||||
// a fresh key).
|
||||
type ApiKey struct {
|
||||
ID string `format:"uint64" json:"id"`
|
||||
Prefix string `json:"prefix"`
|
||||
Expiration *time.Time `json:"expiration" nullable:"true"`
|
||||
CreatedAt *time.Time `json:"createdAt" nullable:"true"`
|
||||
LastSeen *time.Time `json:"lastSeen" nullable:"true"`
|
||||
}
|
||||
|
||||
// CreateApiKeyRequestBody is the v1.CreateApiKeyRequest body.
|
||||
type CreateApiKeyRequestBody struct {
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
}
|
||||
|
||||
// ExpireApiKeyRequestBody is the v1.ExpireApiKeyRequest body.
|
||||
type ExpireApiKeyRequestBody struct {
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
ID string `format:"uint64" json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
createApiKeyInput struct {
|
||||
Body CreateApiKeyRequestBody
|
||||
}
|
||||
createApiKeyOutput struct {
|
||||
Body struct {
|
||||
APIKey string `json:"apiKey"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
expireApiKeyInput struct {
|
||||
Body ExpireApiKeyRequestBody
|
||||
}
|
||||
expireApiKeyOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
listApiKeysOutput struct {
|
||||
Body struct {
|
||||
APIKeys []ApiKey `json:"apiKeys" nullable:"false"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
deleteApiKeyInput struct {
|
||||
Prefix string `path:"prefix"`
|
||||
ID string `format:"uint64" query:"id"`
|
||||
}
|
||||
deleteApiKeyOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
func registerApiKeys(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "createApiKey",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/apikey",
|
||||
Summary: "Create API key",
|
||||
Tags: []string{"ApiKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *createApiKeyInput) (*createApiKeyOutput, error) {
|
||||
// CreateAPIKey requires a non-nil pointer; default a missing expiration
|
||||
// to the zero time as the gRPC handler does.
|
||||
var expiration time.Time
|
||||
if in.Body.Expiration != nil {
|
||||
expiration = *in.Body.Expiration
|
||||
}
|
||||
|
||||
keyStr, _, err := b.State.CreateAPIKey(&expiration)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("creating api key", err)
|
||||
}
|
||||
|
||||
out := &createApiKeyOutput{}
|
||||
out.Body.APIKey = keyStr
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "expireApiKey",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/apikey/expire",
|
||||
Summary: "Expire API key",
|
||||
Tags: []string{"ApiKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *expireApiKeyInput) (*expireApiKeyOutput, error) {
|
||||
key, err := lookupApiKey(b, in.Body.ID, in.Body.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.State.ExpireAPIKey(key)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("expiring api key", err)
|
||||
}
|
||||
|
||||
return &expireApiKeyOutput{}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "listApiKeys",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/apikey",
|
||||
Summary: "List API keys",
|
||||
Tags: []string{"ApiKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, _ *struct{}) (*listApiKeysOutput, error) {
|
||||
keys, err := b.State.ListAPIKeys()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("listing api keys", err)
|
||||
}
|
||||
|
||||
// Match the gRPC handler's ascending-ID ordering.
|
||||
slices.SortFunc(keys, func(a, b types.APIKey) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
out := &listApiKeysOutput{}
|
||||
|
||||
out.Body.APIKeys = make([]ApiKey, len(keys))
|
||||
for i := range keys {
|
||||
out.Body.APIKeys[i] = apiKeyFromState(&keys[i])
|
||||
}
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "deleteApiKey",
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/v1/apikey/{prefix}",
|
||||
Summary: "Delete API key",
|
||||
Tags: []string{"ApiKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *deleteApiKeyInput) (*deleteApiKeyOutput, error) {
|
||||
key, err := lookupApiKey(b, in.ID, in.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.State.DestroyAPIKey(*key)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("deleting api key", err)
|
||||
}
|
||||
|
||||
return &deleteApiKeyOutput{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// lookupApiKey resolves an API key by id or prefix; exactly one must be
|
||||
// supplied. An empty or zero id counts as "no id". Unknown id/prefix maps to
|
||||
// 404 via mapError.
|
||||
func lookupApiKey(b Backend, idStr, prefix string) (*types.APIKey, error) {
|
||||
id, err := parseApiKeyID(idStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasID := id != 0
|
||||
hasPrefix := prefix != ""
|
||||
|
||||
switch {
|
||||
case hasID && hasPrefix:
|
||||
return nil, huma.Error400BadRequest("provide either id or prefix, not both")
|
||||
case hasID:
|
||||
key, err := b.State.GetAPIKeyByID(id)
|
||||
if err != nil {
|
||||
return nil, mapError("getting api key", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
case hasPrefix:
|
||||
key, err := b.State.GetAPIKey(prefix)
|
||||
if err != nil {
|
||||
return nil, mapError("getting api key", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
default:
|
||||
return nil, huma.Error400BadRequest("must provide id or prefix")
|
||||
}
|
||||
}
|
||||
|
||||
// parseApiKeyID decodes the optional uint64 id. Empty maps to zero; non-numeric
|
||||
// is rejected with 400.
|
||||
func parseApiKeyID(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, huma.Error400BadRequest("invalid api key id", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// apiKeyFromState converts a domain API key into the v1 response shape, masking
|
||||
// the prefix so the secret is never returned.
|
||||
func apiKeyFromState(k *types.APIKey) ApiKey {
|
||||
return ApiKey{
|
||||
ID: formatID(k.ID),
|
||||
Prefix: apiKeyMaskedPrefix(k.Prefix),
|
||||
Expiration: k.Expiration,
|
||||
CreatedAt: k.CreatedAt,
|
||||
LastSeen: k.LastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
// apiKeyMaskedPrefix reproduces the unexported types.APIKey.maskedPrefix.
|
||||
func apiKeyMaskedPrefix(prefix string) string {
|
||||
if len(prefix) == types.NewAPIKeyPrefixLength {
|
||||
return "hskey-api-" + prefix + "-***"
|
||||
}
|
||||
|
||||
return prefix + "***"
|
||||
}
|
||||
163
hscontrol/api/v1/auth.go
Normal file
163
hscontrol/api/v1/auth.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerAuth)
|
||||
}
|
||||
|
||||
// errAuthRejected is the verdict handed to the waiting registration flow when
|
||||
// an auth session is rejected.
|
||||
var errAuthRejected = errors.New("auth request rejected")
|
||||
|
||||
// AuthRegisterRequestBody is the v1.AuthRegisterRequest body.
|
||||
type AuthRegisterRequestBody struct {
|
||||
User string `json:"user,omitempty"`
|
||||
AuthID string `json:"authId,omitempty"`
|
||||
}
|
||||
|
||||
// AuthApproveRequestBody is the v1.AuthApproveRequest body.
|
||||
type AuthApproveRequestBody struct {
|
||||
AuthID string `json:"authId,omitempty"`
|
||||
}
|
||||
|
||||
// AuthRejectRequestBody is the v1.AuthRejectRequest body.
|
||||
type AuthRejectRequestBody struct {
|
||||
AuthID string `json:"authId,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
authRegisterInput struct {
|
||||
Body AuthRegisterRequestBody
|
||||
}
|
||||
authRegisterOutput struct {
|
||||
Body struct {
|
||||
Node Node `json:"node"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
authApproveInput struct {
|
||||
Body AuthApproveRequestBody
|
||||
}
|
||||
authApproveOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
authRejectInput struct {
|
||||
Body AuthRejectRequestBody
|
||||
}
|
||||
authRejectOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
func registerAuth(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "authRegister",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/auth/register",
|
||||
Summary: "Register node via auth flow",
|
||||
Tags: []string{"Auth"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *authRegisterInput) (*authRegisterOutput, error) {
|
||||
// Malformed auth_id is 400; unknown user and missing pending session are
|
||||
// 404 via mapError, matching the Approve/Reject handlers.
|
||||
registrationID, err := types.AuthIDFromString(in.Body.AuthID)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("registering node", err)
|
||||
}
|
||||
|
||||
user, err := b.State.GetUserByName(in.Body.User)
|
||||
if err != nil {
|
||||
return nil, mapError("looking up user", err)
|
||||
}
|
||||
|
||||
node, nodeChange, err := b.State.HandleNodeFromAuthPath(
|
||||
registrationID,
|
||||
types.UserID(user.ID),
|
||||
nil,
|
||||
util.RegisterMethodCLI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapError("registering node", err)
|
||||
}
|
||||
|
||||
routeChange, err := b.State.AutoApproveRoutes(node)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("auto approving routes", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange, routeChange)
|
||||
|
||||
out := &authRegisterOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "authApprove",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/auth/approve",
|
||||
Summary: "Approve a pending auth session",
|
||||
Tags: []string{"Auth"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *authApproveInput) (*authApproveOutput, error) {
|
||||
authReq, err := pendingAuthRequest(b, in.Body.AuthID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{})
|
||||
|
||||
return &authApproveOutput{}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "authReject",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/auth/reject",
|
||||
Summary: "Reject a pending auth session",
|
||||
Tags: []string{"Auth"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *authRejectInput) (*authRejectOutput, error) {
|
||||
authReq, err := pendingAuthRequest(b, in.Body.AuthID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{
|
||||
Err: errAuthRejected,
|
||||
})
|
||||
|
||||
return &authRejectOutput{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// pendingAuthRequest looks up the pending session for auth_id. Malformed
|
||||
// auth_id is 400, unknown is 404.
|
||||
func pendingAuthRequest(b Backend, rawID string) (*types.AuthRequest, error) {
|
||||
authID, err := types.AuthIDFromString(rawID)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("invalid auth_id", err)
|
||||
}
|
||||
|
||||
authReq, ok := b.State.GetAuthCacheEntry(authID)
|
||||
if !ok {
|
||||
return nil, huma.Error404NotFound("no pending auth session for auth_id " + authID.String())
|
||||
}
|
||||
|
||||
return authReq, nil
|
||||
}
|
||||
49
hscontrol/api/v1/errors.go
Normal file
49
hscontrol/api/v1/errors.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// mapError translates a state/db-layer error into a Huma HTTP error
|
||||
// (NotFound→404, invalid input→400, conflict→409, everything else→500).
|
||||
// Handlers use this default mapping and may return a more specific huma.ErrorN
|
||||
// directly. msg is a human context prefix, e.g. "getting node".
|
||||
func mapError(msg string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound),
|
||||
errors.Is(err, state.ErrNodeNotFound),
|
||||
errors.Is(err, state.ErrNodeNotInNodeStore),
|
||||
errors.Is(err, db.ErrUserNotFound),
|
||||
errors.Is(err, db.ErrNodeNotFoundRegistrationCache),
|
||||
errors.Is(err, state.ErrRegistrationExpired):
|
||||
return huma.Error404NotFound(msg, err)
|
||||
|
||||
case errors.Is(err, state.ErrGivenNameInvalid),
|
||||
errors.Is(err, state.ErrGivenNameTaken),
|
||||
errors.Is(err, state.ErrNodeNameNotUnique),
|
||||
errors.Is(err, state.ErrNodeMarkedTaggedButHasNoTags),
|
||||
errors.Is(err, state.ErrNodeHasNeitherUserNorTags),
|
||||
errors.Is(err, state.ErrRequestedTagsInvalidOrNotPermitted),
|
||||
errors.Is(err, db.ErrUserStillHasNodes),
|
||||
errors.Is(err, db.ErrCannotChangeOIDCUser),
|
||||
errors.Is(err, db.ErrPreAuthKeyNotTaggedOrOwned),
|
||||
errors.Is(err, db.ErrSingleUseAuthKeyHasBeenUsed):
|
||||
return huma.Error400BadRequest(msg, err)
|
||||
|
||||
case errors.Is(err, state.ErrNodeKeyInUse),
|
||||
errors.Is(err, state.ErrAmbiguousNodeOwnership):
|
||||
return huma.Error409Conflict(msg, err)
|
||||
|
||||
default:
|
||||
return huma.Error500InternalServerError(msg, err)
|
||||
}
|
||||
}
|
||||
41
hscontrol/api/v1/health.go
Normal file
41
hscontrol/api/v1/health.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
)
|
||||
|
||||
// HealthResponseBody mirrors the v1 HealthResponse message. database_connectivity
|
||||
// is reported true only when the database responds to a ping.
|
||||
type HealthResponseBody struct {
|
||||
DatabaseConnectivity bool `json:"databaseConnectivity"`
|
||||
}
|
||||
|
||||
type healthOutput struct {
|
||||
Body HealthResponseBody
|
||||
}
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerHealth)
|
||||
}
|
||||
|
||||
func registerHealth(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "health",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/health",
|
||||
Summary: "Health check",
|
||||
Description: "Reports server health, including database connectivity.",
|
||||
Tags: []string{"Health"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, _ *struct{}) (*healthOutput, error) {
|
||||
err := b.State.PingDB(ctx)
|
||||
if err != nil {
|
||||
return nil, mapError("pinging database", err)
|
||||
}
|
||||
|
||||
return &healthOutput{Body: HealthResponseBody{DatabaseConnectivity: true}}, nil
|
||||
})
|
||||
}
|
||||
702
hscontrol/api/v1/nodes.go
Normal file
702
hscontrol/api/v1/nodes.go
Normal file
|
|
@ -0,0 +1,702 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerNodes)
|
||||
}
|
||||
|
||||
// errBackfillNotConfirmed guards BackfillNodeIPs behind explicit confirmed=true.
|
||||
var errBackfillNotConfirmed = errors.New("not confirmed, aborting")
|
||||
|
||||
// registerMethodToV1Enum maps the stored register method onto the
|
||||
// SCREAMING_SNAKE enum string the v1 contract emits.
|
||||
var registerMethodToV1Enum = map[string]string{
|
||||
util.RegisterMethodAuthKey: "REGISTER_METHOD_AUTH_KEY",
|
||||
util.RegisterMethodOIDC: "REGISTER_METHOD_OIDC",
|
||||
util.RegisterMethodCLI: "REGISTER_METHOD_CLI",
|
||||
}
|
||||
|
||||
// Node mirrors the v1 Node message. The protojson contract emits unpopulated
|
||||
// fields: scalars and slices always (no omitempty), nested messages and optional
|
||||
// timestamps as JSON null when unset.
|
||||
type Node struct {
|
||||
ID string `format:"uint64" json:"id"`
|
||||
MachineKey string `json:"machineKey"`
|
||||
NodeKey string `json:"nodeKey"`
|
||||
DiscoKey string `json:"discoKey"`
|
||||
IPAddresses []string `json:"ipAddresses" nullable:"false"`
|
||||
Name string `json:"name"`
|
||||
User *User `json:"user"`
|
||||
LastSeen *time.Time `json:"lastSeen" nullable:"true"`
|
||||
Expiry *time.Time `json:"expiry" nullable:"true"`
|
||||
PreAuthKey *NodePreAuthKey `json:"preAuthKey"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
RegisterMethod string `enum:"REGISTER_METHOD_UNSPECIFIED,REGISTER_METHOD_AUTH_KEY,REGISTER_METHOD_CLI,REGISTER_METHOD_OIDC" json:"registerMethod"`
|
||||
GivenName string `json:"givenName"`
|
||||
Online bool `json:"online"`
|
||||
ApprovedRoutes []string `json:"approvedRoutes" nullable:"false"`
|
||||
AvailableRoutes []string `json:"availableRoutes" nullable:"false"`
|
||||
SubnetRoutes []string `json:"subnetRoutes" nullable:"false"`
|
||||
Tags []string `json:"tags" nullable:"false"`
|
||||
}
|
||||
|
||||
// NodePreAuthKey is the PreAuthKey shape embedded in a Node response. The
|
||||
// /preauthkey endpoints own the standalone request/response surface.
|
||||
type NodePreAuthKey struct {
|
||||
User *User `json:"user"`
|
||||
ID string `format:"uint64" json:"id"`
|
||||
Key string `json:"key"`
|
||||
Reusable bool `json:"reusable"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
Used bool `json:"used"`
|
||||
Expiration *time.Time `json:"expiration" nullable:"true"`
|
||||
CreatedAt *time.Time `json:"createdAt" nullable:"true"`
|
||||
AclTags []string `json:"aclTags" nullable:"false"`
|
||||
}
|
||||
|
||||
// SetTagsRequestBody mirrors v1.SetTagsRequest.
|
||||
type SetTagsRequestBody struct {
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// SetApprovedRoutesRequestBody mirrors v1.SetApprovedRoutesRequest.
|
||||
type SetApprovedRoutesRequestBody struct {
|
||||
Routes []string `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
// DebugCreateNodeRequestBody mirrors v1.DebugCreateNodeRequest.
|
||||
type DebugCreateNodeRequestBody struct {
|
||||
User string `json:"user,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Routes []string `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
getNodeInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
}
|
||||
nodeOutput struct {
|
||||
Body struct {
|
||||
Node Node `json:"node"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
listNodesInput struct {
|
||||
User string `query:"user"`
|
||||
}
|
||||
listNodesOutput struct {
|
||||
Body struct {
|
||||
Nodes []Node `json:"nodes" nullable:"false"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
deleteNodeInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
}
|
||||
deleteNodeOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
// ExpireNodeRequestBody mirrors v1.ExpireNodeRequest. Both fields are optional;
|
||||
// an absent or all-zero body expires the node immediately, as gRPC does.
|
||||
type ExpireNodeRequestBody struct {
|
||||
Expiry *time.Time `json:"expiry,omitempty"`
|
||||
DisableExpiry bool `json:"disableExpiry,omitempty"`
|
||||
}
|
||||
|
||||
type expireNodeInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
Body *ExpireNodeRequestBody `required:"false"`
|
||||
}
|
||||
|
||||
type renameNodeInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
NewName string `path:"newName"`
|
||||
}
|
||||
|
||||
type setTagsInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
Body SetTagsRequestBody
|
||||
}
|
||||
|
||||
type setApprovedRoutesInput struct {
|
||||
NodeID string `format:"uint64" path:"nodeId"`
|
||||
Body SetApprovedRoutesRequestBody
|
||||
}
|
||||
|
||||
type registerNodeInput struct {
|
||||
User string `query:"user"`
|
||||
Key string `query:"key"`
|
||||
}
|
||||
|
||||
type backfillNodeIPsInput struct {
|
||||
Confirmed bool `query:"confirmed"`
|
||||
}
|
||||
|
||||
type backfillNodeIPsOutput struct {
|
||||
Body struct {
|
||||
Changes []string `json:"changes" nullable:"false"`
|
||||
}
|
||||
}
|
||||
|
||||
type debugCreateNodeInput struct {
|
||||
Body DebugCreateNodeRequestBody
|
||||
}
|
||||
|
||||
func registerNodes(api huma.API, b Backend) {
|
||||
registerNodeReadOps(api, b)
|
||||
registerNodeWriteOps(api, b)
|
||||
registerNodeAdminOps(api, b)
|
||||
}
|
||||
|
||||
func registerNodeReadOps(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getNode",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/node/{nodeId}",
|
||||
Summary: "Get node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *getNodeInput) (*nodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node, ok := b.State.GetNodeByID(nodeID)
|
||||
if !ok {
|
||||
return nil, huma.Error404NotFound("node not found")
|
||||
}
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "listNodes",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/node",
|
||||
Summary: "List nodes",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *listNodesInput) (*listNodesOutput, error) {
|
||||
nodes := b.State.ListNodes()
|
||||
if in.User != "" {
|
||||
user, err := b.State.GetUserByName(in.User)
|
||||
if err != nil {
|
||||
return nil, mapError("listing nodes", err)
|
||||
}
|
||||
|
||||
nodes = b.State.ListNodesByUser(types.UserID(user.ID))
|
||||
}
|
||||
|
||||
out := &listNodesOutput{}
|
||||
out.Body.Nodes = make([]Node, nodes.Len())
|
||||
|
||||
for i, node := range nodes.All() {
|
||||
n := nodeFromView(node)
|
||||
|
||||
// Tags-as-identity: tagged nodes are presented as the special
|
||||
// TaggedDevices user.
|
||||
if node.IsTagged() {
|
||||
user := userFromView(types.TaggedDevices.View())
|
||||
n.User = &user
|
||||
}
|
||||
|
||||
// SubnetRoutes is the routes actively served, exit routes included.
|
||||
n.SubnetRoutes = util.PrefixesToString(
|
||||
append(b.State.GetNodePrimaryRoutes(node.ID()), node.ExitRoutes()...),
|
||||
)
|
||||
|
||||
out.Body.Nodes[i] = n
|
||||
}
|
||||
|
||||
// Match the gRPC handler's ascending-ID ordering.
|
||||
slices.SortFunc(out.Body.Nodes, func(a, b Node) int {
|
||||
return cmpNodeID(a.ID, b.ID)
|
||||
})
|
||||
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
func registerNodeWriteOps(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "deleteNode",
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/v1/node/{nodeId}",
|
||||
Summary: "Delete node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *deleteNodeInput) (*deleteNodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node, ok := b.State.GetNodeByID(nodeID)
|
||||
if !ok {
|
||||
return nil, huma.Error404NotFound("node not found")
|
||||
}
|
||||
|
||||
nodeChange, err := b.State.DeleteNode(node)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("deleting node", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
return &deleteNodeOutput{}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "expireNode",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/{nodeId}/expire",
|
||||
Summary: "Expire node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *expireNodeInput) (*nodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// gRPC parity: disableExpiry => nil expiry (never expires); explicit
|
||||
// expiry honoured; absent/zero body expires now. Both set is a 400.
|
||||
var (
|
||||
disableExpiry bool
|
||||
customExpiry *time.Time
|
||||
)
|
||||
|
||||
if in.Body != nil {
|
||||
disableExpiry = in.Body.DisableExpiry
|
||||
customExpiry = in.Body.Expiry
|
||||
}
|
||||
|
||||
if disableExpiry && customExpiry != nil {
|
||||
return nil, huma.Error400BadRequest("cannot set both disable_expiry and expiry")
|
||||
}
|
||||
|
||||
expiry := time.Now()
|
||||
|
||||
switch {
|
||||
case disableExpiry:
|
||||
node, nodeChange, expErr := b.State.SetNodeExpiry(nodeID, nil)
|
||||
if expErr != nil {
|
||||
return nil, mapError("expiring node", expErr)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
case customExpiry != nil:
|
||||
expiry = *customExpiry
|
||||
}
|
||||
|
||||
node, nodeChange, err := b.State.SetNodeExpiry(nodeID, &expiry)
|
||||
if err != nil {
|
||||
return nil, mapError("expiring node", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "renameNode",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/{nodeId}/rename/{newName}",
|
||||
Summary: "Rename node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *renameNodeInput) (*nodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node, nodeChange, err := b.State.RenameNode(nodeID, in.NewName)
|
||||
if err != nil {
|
||||
return nil, mapError("renaming node", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "setTags",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/{nodeId}/tags",
|
||||
Summary: "Set tags",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *setTagsInput) (*nodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Tagged nodes must keep at least one tag, so reject an empty set
|
||||
// before touching state, as gRPC does.
|
||||
if len(in.Body.Tags) == 0 {
|
||||
return nil, huma.Error400BadRequest(
|
||||
"cannot remove all tags from a node - tagged nodes must have at least one tag",
|
||||
)
|
||||
}
|
||||
|
||||
for _, tag := range in.Body.Tags {
|
||||
tagErr := validateTag(tag)
|
||||
if tagErr != nil {
|
||||
return nil, huma.Error400BadRequest("setting tags", tagErr)
|
||||
}
|
||||
}
|
||||
|
||||
_, found := b.State.GetNodeByID(nodeID)
|
||||
if !found {
|
||||
return nil, huma.Error404NotFound("node not found")
|
||||
}
|
||||
|
||||
node, nodeChange, err := b.State.SetNodeTags(nodeID, in.Body.Tags)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("setting tags", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
func registerNodeAdminOps(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "setApprovedRoutes",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/{nodeId}/approve_routes",
|
||||
Summary: "Set approved routes",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *setApprovedRoutesInput) (*nodeOutput, error) {
|
||||
nodeID, err := parseNodeID(in.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var newApproved []netip.Prefix
|
||||
|
||||
for _, route := range in.Body.Routes {
|
||||
prefix, parseErr := netip.ParsePrefix(route)
|
||||
if parseErr != nil {
|
||||
return nil, huma.Error400BadRequest("parsing route", parseErr)
|
||||
}
|
||||
|
||||
// One exit route implies both families, else the client won't
|
||||
// annotate the node as an exit node.
|
||||
if prefix == tsaddr.AllIPv4() || prefix == tsaddr.AllIPv6() {
|
||||
newApproved = append(newApproved, tsaddr.AllIPv4(), tsaddr.AllIPv6())
|
||||
} else {
|
||||
newApproved = append(newApproved, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(newApproved, netip.Prefix.Compare)
|
||||
newApproved = slices.Compact(newApproved)
|
||||
|
||||
node, nodeChange, err := b.State.SetApprovedRoutes(nodeID, newApproved)
|
||||
if err != nil {
|
||||
return nil, mapError("setting approved routes", err)
|
||||
}
|
||||
|
||||
b.Change(nodeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
// SubnetRoutes here excludes exit routes, unlike the list handler.
|
||||
out.Body.Node.SubnetRoutes = util.PrefixesToString(
|
||||
b.State.GetNodePrimaryRoutes(node.ID()),
|
||||
)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "registerNode",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/register",
|
||||
Summary: "Register node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *registerNodeInput) (*nodeOutput, error) {
|
||||
registrationID, err := types.AuthIDFromString(in.Key)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("registering node", err)
|
||||
}
|
||||
|
||||
user, err := b.State.GetUserByName(in.User)
|
||||
if err != nil {
|
||||
return nil, mapError("looking up user", err)
|
||||
}
|
||||
|
||||
node, nodeChange, err := b.State.HandleNodeFromAuthPath(
|
||||
registrationID,
|
||||
types.UserID(user.ID),
|
||||
nil,
|
||||
util.RegisterMethodCLI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapError("registering node", err)
|
||||
}
|
||||
|
||||
routeChange, err := b.State.AutoApproveRoutes(node)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("auto approving routes", err)
|
||||
}
|
||||
|
||||
// Empty changes are ignored by the change sink.
|
||||
b.Change(nodeChange, routeChange)
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(node)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "backfillNodeIPs",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/node/backfillips",
|
||||
Summary: "Backfill node IPs",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *backfillNodeIPsInput) (*backfillNodeIPsOutput, error) {
|
||||
if !in.Confirmed {
|
||||
return nil, huma.Error400BadRequest("backfilling node IPs", errBackfillNotConfirmed)
|
||||
}
|
||||
|
||||
changes, err := b.State.BackfillNodeIPs()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("backfilling node IPs", err)
|
||||
}
|
||||
|
||||
out := &backfillNodeIPsOutput{}
|
||||
out.Body.Changes = changes
|
||||
|
||||
if out.Body.Changes == nil {
|
||||
out.Body.Changes = []string{}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "debugCreateNode",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/debug/node",
|
||||
Summary: "Debug create node",
|
||||
Tags: []string{"Nodes"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *debugCreateNodeInput) (*nodeOutput, error) {
|
||||
user, err := b.State.GetUserByName(in.Body.User)
|
||||
if err != nil {
|
||||
return nil, mapError("looking up user", err)
|
||||
}
|
||||
|
||||
routes, err := util.StringToIPPrefix(in.Body.Routes)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("parsing routes", err)
|
||||
}
|
||||
|
||||
registrationID, err := types.AuthIDFromString(in.Body.Key)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("debug creating node", err)
|
||||
}
|
||||
|
||||
regData := &types.RegistrationData{
|
||||
NodeKey: key.NewNode().Public(),
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
Hostname: in.Body.Name,
|
||||
Expiry: &time.Time{}, // zero time, not nil, to keep proto JSON round-trip semantics
|
||||
}
|
||||
|
||||
authRegReq := types.NewRegisterAuthRequest(regData)
|
||||
b.State.SetAuthCacheEntry(registrationID, authRegReq)
|
||||
|
||||
// Synthetic echo; the real node is created later via the auth path
|
||||
// from the cached registration data.
|
||||
echoNode := types.Node{
|
||||
NodeKey: regData.NodeKey,
|
||||
MachineKey: regData.MachineKey,
|
||||
Hostname: regData.Hostname,
|
||||
User: user,
|
||||
Expiry: &time.Time{},
|
||||
LastSeen: &time.Time{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: in.Body.Name,
|
||||
OS: "TestOS",
|
||||
RoutableIPs: routes,
|
||||
},
|
||||
}
|
||||
|
||||
out := &nodeOutput{}
|
||||
out.Body.Node = nodeFromView(echoNode.View())
|
||||
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
// nodeFromView builds the Node response from a NodeView, reading through the
|
||||
// view accessors. SubnetRoutes is left empty; callers that serve routes set it
|
||||
// explicitly.
|
||||
func nodeFromView(view types.NodeView) Node {
|
||||
n := Node{
|
||||
ID: view.StringID(),
|
||||
MachineKey: view.MachineKey().String(),
|
||||
NodeKey: view.NodeKey().String(),
|
||||
DiscoKey: view.DiscoKey().String(),
|
||||
IPAddresses: nonNilStrings(view.IPsAsString()),
|
||||
Name: view.Hostname(),
|
||||
CreatedAt: view.CreatedAt(),
|
||||
RegisterMethod: registerMethodEnum(view.RegisterMethod()),
|
||||
GivenName: view.GivenName(),
|
||||
Online: view.IsOnline().Valid() && view.IsOnline().Get(),
|
||||
ApprovedRoutes: nonNilStrings(util.PrefixesToString(view.ApprovedRoutes().AsSlice())),
|
||||
AvailableRoutes: nonNilStrings(util.PrefixesToString(view.AnnouncedRoutes())),
|
||||
SubnetRoutes: []string{},
|
||||
Tags: nonNilStrings(view.Tags().AsSlice()),
|
||||
}
|
||||
|
||||
if view.User().Valid() {
|
||||
user := userFromView(view.User())
|
||||
n.User = &user
|
||||
}
|
||||
|
||||
if view.AuthKey().Valid() {
|
||||
n.PreAuthKey = nodePreAuthKeyFromView(view.AuthKey())
|
||||
}
|
||||
|
||||
if view.LastSeen().Valid() {
|
||||
ls := view.LastSeen().Get()
|
||||
n.LastSeen = &ls
|
||||
}
|
||||
|
||||
if view.Expiry().Valid() {
|
||||
exp := view.Expiry().Get()
|
||||
n.Expiry = &exp
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// nodePreAuthKeyFromView builds the embedded NodePreAuthKey, masking the key to
|
||||
// its prefix (legacy plaintext keys are shown in full).
|
||||
func nodePreAuthKeyFromView(key types.PreAuthKeyView) *NodePreAuthKey {
|
||||
pak := &NodePreAuthKey{
|
||||
ID: formatID(key.ID()),
|
||||
Key: maskedPreAuthKey(key),
|
||||
Reusable: key.Reusable(),
|
||||
Ephemeral: key.Ephemeral(),
|
||||
Used: key.Used(),
|
||||
AclTags: nonNilStrings(key.Tags().AsSlice()),
|
||||
}
|
||||
|
||||
if key.User().Valid() {
|
||||
user := userFromView(key.User())
|
||||
pak.User = &user
|
||||
}
|
||||
|
||||
if key.Expiration().Valid() {
|
||||
exp := key.Expiration().Get()
|
||||
pak.Expiration = &exp
|
||||
}
|
||||
|
||||
if key.CreatedAt().Valid() {
|
||||
created := key.CreatedAt().Get()
|
||||
pak.CreatedAt = &created
|
||||
}
|
||||
|
||||
return pak
|
||||
}
|
||||
|
||||
// registerMethodEnum maps the stored register method onto the v1 enum string,
|
||||
// defaulting to REGISTER_METHOD_UNSPECIFIED for unknown values.
|
||||
func registerMethodEnum(method string) string {
|
||||
if enum, ok := registerMethodToV1Enum[method]; ok {
|
||||
return enum
|
||||
}
|
||||
|
||||
return "REGISTER_METHOD_UNSPECIFIED"
|
||||
}
|
||||
|
||||
func nonNilStrings(s []string) []string {
|
||||
if s == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// cmpNodeID orders two decimal node-ID strings numerically, matching the gRPC
|
||||
// handler's ascending-ID ordering.
|
||||
func cmpNodeID(a, b string) int {
|
||||
ai, _ := strconv.ParseUint(a, 10, 64)
|
||||
bi, _ := strconv.ParseUint(b, 10, 64)
|
||||
|
||||
switch {
|
||||
case ai < bi:
|
||||
return -1
|
||||
case ai > bi:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func parseNodeID(s string) (types.NodeID, error) {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, huma.Error400BadRequest(
|
||||
"type mismatch, parameter: node_id, error: " + err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
return types.NodeID(id), nil
|
||||
}
|
||||
191
hscontrol/api/v1/policy.go
Normal file
191
hscontrol/api/v1/policy.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerPolicy)
|
||||
}
|
||||
|
||||
// PolicyRequestBody carries the HuJSON policy document as a string, for both
|
||||
// v1.SetPolicyRequest and v1.CheckPolicyRequest.
|
||||
type PolicyRequestBody struct {
|
||||
Policy string `json:"policy,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyResponseBody is the v1.GetPolicyResponse/SetPolicyResponse body. Fields
|
||||
// carry no omitempty so zero values are emitted (EmitUnpopulated parity).
|
||||
type PolicyResponseBody struct {
|
||||
Policy string `json:"policy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type (
|
||||
getPolicyInput struct{}
|
||||
getPolicyOutput struct {
|
||||
Body PolicyResponseBody
|
||||
}
|
||||
|
||||
setPolicyInput struct {
|
||||
Body PolicyRequestBody
|
||||
}
|
||||
setPolicyOutput struct {
|
||||
Body PolicyResponseBody
|
||||
}
|
||||
|
||||
checkPolicyInput struct {
|
||||
Body PolicyRequestBody
|
||||
}
|
||||
checkPolicyOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
func registerPolicy(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "getPolicy",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/policy",
|
||||
Summary: "Get policy",
|
||||
Tags: []string{"Policy"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, _ *getPolicyInput) (*getPolicyOutput, error) {
|
||||
switch b.Cfg.Policy.Mode {
|
||||
case types.PolicyModeDB:
|
||||
p, err := b.State.GetPolicy()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("loading ACL from database", err)
|
||||
}
|
||||
|
||||
out := &getPolicyOutput{}
|
||||
out.Body.Policy = p.Data
|
||||
out.Body.UpdatedAt = p.UpdatedAt
|
||||
|
||||
return out, nil
|
||||
case types.PolicyModeFile:
|
||||
absPath := util.AbsolutePathFromConfigPath(b.Cfg.Policy.Path)
|
||||
|
||||
f, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError(
|
||||
fmt.Sprintf("reading policy from path %q", absPath), err,
|
||||
)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("reading policy from file", err)
|
||||
}
|
||||
|
||||
out := &getPolicyOutput{}
|
||||
out.Body.Policy = string(data)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return nil, huma.Error500InternalServerError(fmt.Sprintf(
|
||||
"no supported policy mode found in configuration, policy.mode: %q",
|
||||
b.Cfg.Policy.Mode,
|
||||
), nil)
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "setPolicy",
|
||||
Method: http.MethodPut,
|
||||
Path: "/api/v1/policy",
|
||||
Summary: "Set policy",
|
||||
Tags: []string{"Policy"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *setPolicyInput) (*setPolicyOutput, error) {
|
||||
if b.Cfg.Policy.Mode != types.PolicyModeDB {
|
||||
// Policy updates are only valid in DB mode; otherwise 400.
|
||||
return nil, huma.Error400BadRequest(
|
||||
types.ErrPolicyUpdateIsDisabled.Error(), types.ErrPolicyUpdateIsDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
p := in.Body.Policy
|
||||
|
||||
// Reject policy that would fail when building a map response. SSH rule
|
||||
// validation needs a node, so a server with no nodes can't catch every
|
||||
// case here.
|
||||
nodes := b.State.ListNodes()
|
||||
|
||||
_, err := b.State.SetPolicy([]byte(p))
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("setting policy", err)
|
||||
}
|
||||
|
||||
if nodes.Len() > 0 {
|
||||
_, err = b.State.SSHPolicy(nodes.At(0))
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest("verifying SSH rules", err)
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := b.State.SetPolicyInDB(p)
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("setting policy", err)
|
||||
}
|
||||
|
||||
// Reload even when content is unchanged: routes manually disabled before
|
||||
// may now qualify for auto-approval, so they must be re-evaluated.
|
||||
cs, err := b.State.ReloadPolicy()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("reloading policy", err)
|
||||
}
|
||||
|
||||
if len(cs) > 0 {
|
||||
b.Change(cs...)
|
||||
}
|
||||
|
||||
out := &setPolicyOutput{}
|
||||
out.Body.Policy = updated.Data
|
||||
out.Body.UpdatedAt = updated.UpdatedAt
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "checkPolicy",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/policy/check",
|
||||
Summary: "Check policy",
|
||||
Description: "Validates the given policy against the server's live users and nodes without persisting it.",
|
||||
Tags: []string{"Policy"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *checkPolicyInput) (*checkPolicyOutput, error) {
|
||||
polB := []byte(in.Body.Policy)
|
||||
|
||||
users, err := b.State.ListAllUsers()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("loading users", err)
|
||||
}
|
||||
|
||||
nodes := b.State.ListNodes()
|
||||
|
||||
pm, err := policyv2.NewPolicyManager(polB, users, nodes)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest(err.Error(), err)
|
||||
}
|
||||
|
||||
_, err = pm.SetPolicy(polB)
|
||||
if err != nil {
|
||||
return nil, huma.Error400BadRequest(err.Error(), err)
|
||||
}
|
||||
|
||||
return &checkPolicyOutput{}, nil
|
||||
})
|
||||
}
|
||||
318
hscontrol/api/v1/preauthkeys.go
Normal file
318
hscontrol/api/v1/preauthkeys.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registrations = append(registrations, registerPreAuthKeys)
|
||||
}
|
||||
|
||||
// PreAuthKey is the v1 PreAuthKey message. User is a pointer with no omitempty
|
||||
// so tagged (system-created) keys emit "user":null. Expiration and CreatedAt
|
||||
// are always emitted, zero-stamped when unset.
|
||||
type PreAuthKey struct {
|
||||
User *User `json:"user"`
|
||||
ID string `format:"uint64" json:"id"`
|
||||
Key string `json:"key"`
|
||||
Reusable bool `json:"reusable"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
Used bool `json:"used"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ACLTags []string `json:"aclTags" nullable:"false"`
|
||||
}
|
||||
|
||||
// CreatePreAuthKeyRequestBody is the v1.CreatePreAuthKeyRequest body. Every
|
||||
// field is optional, hence omitempty throughout.
|
||||
type CreatePreAuthKeyRequestBody struct {
|
||||
User string `format:"uint64" json:"user,omitempty"`
|
||||
Reusable bool `json:"reusable,omitempty"`
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
ACLTags []string `json:"aclTags,omitempty"`
|
||||
}
|
||||
|
||||
// ExpirePreAuthKeyRequestBody is the v1.ExpirePreAuthKeyRequest body.
|
||||
type ExpirePreAuthKeyRequestBody struct {
|
||||
ID string `format:"uint64" json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
createPreAuthKeyInput struct {
|
||||
Body CreatePreAuthKeyRequestBody
|
||||
}
|
||||
preAuthKeyOutput struct {
|
||||
Body struct {
|
||||
PreAuthKey PreAuthKey `json:"preAuthKey"`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
expirePreAuthKeyInput struct {
|
||||
Body ExpirePreAuthKeyRequestBody
|
||||
}
|
||||
expirePreAuthKeyOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
deletePreAuthKeyInput struct {
|
||||
ID string `format:"uint64" query:"id"`
|
||||
}
|
||||
deletePreAuthKeyOutput struct {
|
||||
Body struct{}
|
||||
}
|
||||
)
|
||||
|
||||
type listPreAuthKeysOutput struct {
|
||||
Body struct {
|
||||
PreAuthKeys []PreAuthKey `json:"preAuthKeys" nullable:"false"`
|
||||
}
|
||||
}
|
||||
|
||||
func registerPreAuthKeys(api huma.API, b Backend) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "createPreAuthKey",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/preauthkey",
|
||||
Summary: "Create pre-auth key",
|
||||
Tags: []string{"PreAuthKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *createPreAuthKeyInput) (*preAuthKeyOutput, error) {
|
||||
user, err := parsePreAuthKeyUser(in.Body.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, tag := range in.Body.ACLTags {
|
||||
tagErr := validateTag(tag)
|
||||
if tagErr != nil {
|
||||
return nil, huma.Error400BadRequest("invalid tag", tagErr)
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePreAuthKey requires a non-nil pointer; zero-stamp when unset.
|
||||
var expiration time.Time
|
||||
if in.Body.Expiration != nil {
|
||||
expiration = *in.Body.Expiration
|
||||
}
|
||||
|
||||
var userID *types.UserID
|
||||
|
||||
if user != 0 {
|
||||
u, getErr := b.State.GetUserByID(user)
|
||||
if getErr != nil {
|
||||
return nil, mapError("creating pre-auth key", getErr)
|
||||
}
|
||||
|
||||
userID = u.TypedID()
|
||||
}
|
||||
|
||||
preAuthKey, err := b.State.CreatePreAuthKey(
|
||||
userID,
|
||||
in.Body.Reusable,
|
||||
in.Body.Ephemeral,
|
||||
&expiration,
|
||||
in.Body.ACLTags,
|
||||
)
|
||||
if err != nil {
|
||||
// A key that is neither tagged nor user-owned is invalid input (400).
|
||||
return nil, mapError("creating pre-auth key", err)
|
||||
}
|
||||
|
||||
out := &preAuthKeyOutput{}
|
||||
out.Body.PreAuthKey = preAuthKeyNewToResponse(preAuthKey)
|
||||
|
||||
return out, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "expirePreAuthKey",
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/v1/preauthkey/expire",
|
||||
Summary: "Expire pre-auth key",
|
||||
Tags: []string{"PreAuthKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *expirePreAuthKeyInput) (*expirePreAuthKeyOutput, error) {
|
||||
id, err := parsePreAuthKeyID(in.Body.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.State.ExpirePreAuthKey(id)
|
||||
if err != nil {
|
||||
// An unknown key id maps to 404.
|
||||
return nil, mapError("expiring pre-auth key", err)
|
||||
}
|
||||
|
||||
return &expirePreAuthKeyOutput{}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "deletePreAuthKey",
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/v1/preauthkey",
|
||||
Summary: "Delete pre-auth key",
|
||||
Tags: []string{"PreAuthKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, in *deletePreAuthKeyInput) (*deletePreAuthKeyOutput, error) {
|
||||
// DELETE has no body: id is bound from the query string.
|
||||
id, err := parsePreAuthKeyID(in.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.State.DeletePreAuthKey(id)
|
||||
if err != nil {
|
||||
// An unknown key id maps to 404.
|
||||
return nil, mapError("deleting pre-auth key", err)
|
||||
}
|
||||
|
||||
return &deletePreAuthKeyOutput{}, nil
|
||||
})
|
||||
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "listPreAuthKeys",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/v1/preauthkey",
|
||||
Summary: "List pre-auth keys",
|
||||
Tags: []string{"PreAuthKeys"},
|
||||
Security: bearerAuth,
|
||||
}, func(ctx context.Context, _ *struct{}) (*listPreAuthKeysOutput, error) {
|
||||
preAuthKeys, err := b.State.ListPreAuthKeys()
|
||||
if err != nil {
|
||||
return nil, huma.Error500InternalServerError("listing pre-auth keys", err)
|
||||
}
|
||||
|
||||
// Match the gRPC handler's ascending-ID ordering.
|
||||
slices.SortFunc(preAuthKeys, func(a, b types.PreAuthKey) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
out := &listPreAuthKeysOutput{}
|
||||
|
||||
out.Body.PreAuthKeys = make([]PreAuthKey, len(preAuthKeys))
|
||||
for i := range preAuthKeys {
|
||||
out.Body.PreAuthKeys[i] = preAuthKeyToResponse(&preAuthKeys[i])
|
||||
}
|
||||
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
// preAuthKeyNewToResponse builds the v1 response for a freshly created key. The
|
||||
// plaintext key is returned only here; Used is always false.
|
||||
func preAuthKeyNewToResponse(key *types.PreAuthKeyNew) PreAuthKey {
|
||||
out := PreAuthKey{
|
||||
ID: formatID(key.ID),
|
||||
Key: key.Key,
|
||||
Reusable: key.Reusable,
|
||||
Ephemeral: key.Ephemeral,
|
||||
ACLTags: nonNilTags(key.Tags),
|
||||
}
|
||||
|
||||
if key.User != nil {
|
||||
u := userFromView(key.User.View())
|
||||
out.User = &u
|
||||
}
|
||||
|
||||
if key.Expiration != nil {
|
||||
out.Expiration = *key.Expiration
|
||||
}
|
||||
|
||||
if key.CreatedAt != nil {
|
||||
out.CreatedAt = *key.CreatedAt
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// preAuthKeyToResponse builds the v1 response for a stored key, with its key
|
||||
// field masked (see maskedPreAuthKey).
|
||||
func preAuthKeyToResponse(key *types.PreAuthKey) PreAuthKey {
|
||||
out := PreAuthKey{
|
||||
ID: formatID(key.ID),
|
||||
Key: maskedPreAuthKey(key.View()),
|
||||
Reusable: key.Reusable,
|
||||
Ephemeral: key.Ephemeral,
|
||||
Used: key.Used,
|
||||
ACLTags: nonNilTags(key.Tags),
|
||||
}
|
||||
|
||||
if key.User != nil {
|
||||
u := userFromView(key.User.View())
|
||||
out.User = &u
|
||||
}
|
||||
|
||||
if key.Expiration != nil {
|
||||
out.Expiration = *key.Expiration
|
||||
}
|
||||
|
||||
if key.CreatedAt != nil {
|
||||
out.CreatedAt = *key.CreatedAt
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// maskedPreAuthKey masks new keys (those with a stored prefix) so the secret is
|
||||
// never returned; legacy plaintext keys are returned in full for backwards
|
||||
// compatibility.
|
||||
func maskedPreAuthKey(key types.PreAuthKeyView) string {
|
||||
if key.Prefix() != "" {
|
||||
return "hskey-auth-" + key.Prefix() + "-***"
|
||||
}
|
||||
|
||||
return key.Key()
|
||||
}
|
||||
|
||||
// nonNilTags ensures aclTags serializes as [] rather than null, matching
|
||||
// EmitUnpopulated output.
|
||||
func nonNilTags(tags []string) []string {
|
||||
if tags == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
// parsePreAuthKeyUser parses the optional uint64 user field. Empty means "no
|
||||
// user" (user 0); non-numeric is rejected with 400.
|
||||
func parsePreAuthKeyUser(s string) (types.UserID, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, huma.Error400BadRequest("invalid user id", err)
|
||||
}
|
||||
|
||||
return types.UserID(id), nil
|
||||
}
|
||||
|
||||
// parsePreAuthKeyID parses the uint64 key id. Empty means id 0; non-numeric is
|
||||
// rejected with 400.
|
||||
func parsePreAuthKeyID(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, huma.Error400BadRequest("invalid pre-auth key id", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
29
hscontrol/api/v1/tags.go
Normal file
29
hscontrol/api/v1/tags.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ACL tag validation, shared by the node and pre-auth-key resources. These
|
||||
// reproduce the gRPC validateTag checks and messages.
|
||||
var (
|
||||
errTagMissingPrefix = errors.New("tag must start with the string 'tag:'")
|
||||
errTagNotLowercase = errors.New("tag should be lowercase")
|
||||
errTagHasSpaces = errors.New("tags must not contain spaces")
|
||||
)
|
||||
|
||||
// validateTag reports whether an ACL tag is well formed: it must start with
|
||||
// "tag:", be lowercase, and contain no spaces.
|
||||
func validateTag(tag string) error {
|
||||
switch {
|
||||
case !strings.HasPrefix(tag, "tag:"):
|
||||
return errTagMissingPrefix
|
||||
case strings.ToLower(tag) != tag:
|
||||
return errTagNotLowercase
|
||||
case len(strings.Fields(tag)) > 1:
|
||||
return errTagHasSpaces
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
52
hscontrol/api/v1/types.go
Normal file
52
hscontrol/api/v1/types.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
// The v1 contract follows protojson: 64-bit integers are JSON strings (avoiding
|
||||
// precision loss above 2^53), timestamps are RFC 3339, and zero values are
|
||||
// emitted. Hence response fields carry NO omitempty; request types keep
|
||||
// omitempty so their fields stay optional in the spec.
|
||||
|
||||
// formatID renders a uint64 identifier as the contract's decimal string.
|
||||
func formatID[T ~uint64 | ~uint](id T) string {
|
||||
return strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
|
||||
// User mirrors the v1 User message.
|
||||
type User struct {
|
||||
ID string `format:"uint64" json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
ProviderID string `json:"providerId"`
|
||||
Provider string `json:"provider"`
|
||||
ProfilePicURL string `json:"profilePicUrl"`
|
||||
}
|
||||
|
||||
// userFromView converts a domain user into the v1 response shape, reading
|
||||
// through the [types.UserView] accessors: Name falls back to Username()
|
||||
// (email/provider/id) when the stored Name is empty, so OIDC users display
|
||||
// their email.
|
||||
func userFromView(u types.UserView) User {
|
||||
name := u.Name()
|
||||
if name == "" {
|
||||
name = u.Username()
|
||||
}
|
||||
|
||||
return User{
|
||||
ID: formatID(u.ID()),
|
||||
Name: name,
|
||||
CreatedAt: u.CreatedAt(),
|
||||
DisplayName: u.DisplayName(),
|
||||
Email: u.Email(),
|
||||
ProviderID: u.ProviderIdentifier().String,
|
||||
Provider: u.Provider(),
|
||||
ProfilePicURL: u.ProfilePicURL(),
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue