mirror of
https://github.com/juanfont/headscale.git
synced 2026-05-24 02:58:42 +09:00
cmd, templates, integration: extract shared production constants
Constants the operator/test reader benefits from centralising.
Tests stay verbatim (the .golangci.yaml goconst tune skips them);
extraction here applies only where the same literal acts as
shared vocabulary across files.
cmd/headscale/cli/strings.go (new)
Cobra subcommand verbs and aliases shared by every list / show
/ new / delete / expire command across api_key, nodes, policy,
preauthkeys, users — plus the Result / Created / Expiration
column headers used in printOutput maps.
hscontrol/templates/design.go
cssBorderHS, cssBreakWord, cssCenter, cssOverflowWrap — shared
styles applied across design.go, ping.go, register_confirm.go.
spaceS already existed; switch raw "0.5rem" literals to it.
integration/hsic/hsic.go
binHeadscale, flagOutput, acceptJSON — names invoked across
hsic.go and config.go.
integration/tsic/tsic.go
tailscaleBin — used across docker exec call sites.
This commit is contained in:
@@ -41,9 +41,9 @@ var apiKeysCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listAPIKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List the Api keys for headscale",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
||||
if err != nil {
|
||||
@@ -51,9 +51,8 @@ var listAPIKeys = &cobra.Command{
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{"ID", "Prefix", "Expiration", "Created"},
|
||||
}
|
||||
tableData := make(pterm.TableData, 1, 1+len(response.GetApiKeys()))
|
||||
tableData[0] = []string{"ID", "Prefix", colExpiration, colCreated}
|
||||
|
||||
for _, key := range response.GetApiKeys() {
|
||||
expiration := "-"
|
||||
@@ -82,7 +81,7 @@ 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"},
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
@@ -117,9 +116,9 @@ 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"},
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
@@ -139,9 +138,9 @@ var expireAPIKeyCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var deleteAPIKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an ApiKey",
|
||||
Aliases: []string{"remove", "del"},
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
|
||||
@@ -89,9 +89,9 @@ var registerNodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listNodesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List nodes",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
|
||||
@@ -101,7 +101,7 @@ var listNodesCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetNodes(), func() error {
|
||||
tableData, err := nodesToPtables(user, response.GetNodes())
|
||||
tableData, err := nodesToPtables(response.GetNodes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting to table: %w", err)
|
||||
}
|
||||
@@ -145,12 +145,12 @@ 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"},
|
||||
Aliases: []string{"logout", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
||||
@@ -229,9 +229,9 @@ var renameNodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var deleteNodeCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a node",
|
||||
Aliases: []string{"del"},
|
||||
Aliases: []string{aliasDel},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
@@ -252,7 +252,7 @@ var deleteNodeCmd = &cobra.Command{
|
||||
"Do you want to remove the node %s?",
|
||||
getResponse.GetNode().GetName(),
|
||||
)) {
|
||||
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)
|
||||
@@ -262,7 +262,7 @@ var deleteNodeCmd = &cobra.Command{
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
map[string]string{"Result": "Node deleted"},
|
||||
map[string]string{colResult: "Node deleted"},
|
||||
"Node deleted",
|
||||
)
|
||||
}),
|
||||
@@ -304,10 +304,7 @@ be assigned to nodes.`,
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(
|
||||
currentUser string,
|
||||
nodes []*v1.Node,
|
||||
) (pterm.TableData, error) {
|
||||
func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
"Hostname",
|
||||
@@ -319,11 +316,12 @@ 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
|
||||
@@ -447,7 +445,8 @@ 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{
|
||||
|
||||
@@ -61,7 +61,7 @@ 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
|
||||
|
||||
@@ -205,9 +205,9 @@ 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
|
||||
// [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 {
|
||||
|
||||
@@ -42,9 +42,9 @@ var preauthkeysCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listPreAuthKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all preauthkeys",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
||||
if err != nil {
|
||||
@@ -52,17 +52,16 @@ var listPreAuthKeys = &cobra.Command{
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
"Expiration",
|
||||
"Created",
|
||||
"Owner",
|
||||
},
|
||||
tableData := make(pterm.TableData, 1, 1+len(response.GetPreAuthKeys()))
|
||||
tableData[0] = []string{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
colExpiration,
|
||||
colCreated,
|
||||
"Owner",
|
||||
}
|
||||
|
||||
for _, key := range response.GetPreAuthKeys() {
|
||||
@@ -100,7 +99,7 @@ var listPreAuthKeys = &cobra.Command{
|
||||
var createPreAuthKeyCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey",
|
||||
Aliases: []string{"c", "new"},
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetUint64("user")
|
||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||
@@ -130,9 +129,9 @@ var createPreAuthKeyCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var expirePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire a preauthkey",
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, _ := cmd.Flags().GetUint64("id")
|
||||
|
||||
@@ -154,9 +153,9 @@ var expirePreAuthKeyCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var deletePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a preauthkey",
|
||||
Aliases: []string{"del", "rm", "d"},
|
||||
Aliases: []string{aliasDel, "rm", "d"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, _ := cmd.Flags().GetUint64("id")
|
||||
|
||||
|
||||
@@ -30,7 +30,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
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
@@ -222,25 +235,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 +271,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 +283,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",
|
||||
},
|
||||
|
||||
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"
|
||||
)
|
||||
@@ -70,7 +70,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
|
||||
@@ -115,7 +115,7 @@ var createUserCmd = &cobra.Command{
|
||||
var destroyUserCmd = &cobra.Command{
|
||||
Use: "destroy --identifier ID or --name NAME",
|
||||
Short: "Destroys a user",
|
||||
Aliases: []string{"delete"},
|
||||
Aliases: []string{cmdDelete},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil {
|
||||
@@ -142,7 +142,7 @@ var destroyUserCmd = &cobra.Command{
|
||||
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
||||
user.GetName(), user.GetId(),
|
||||
)) {
|
||||
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()}
|
||||
@@ -157,9 +157,9 @@ var destroyUserCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listUsersCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all the users",
|
||||
Aliases: []string{"ls", "show"},
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
request := &v1.ListUsersRequest{}
|
||||
|
||||
@@ -183,7 +183,9 @@ var listUsersCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
||||
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
|
||||
tableData := make(pterm.TableData, 1, 1+len(response.GetUsers()))
|
||||
|
||||
tableData[0] = []string{"ID", "Name", "Username", "Email", colCreated}
|
||||
for _, user := range response.GetUsers() {
|
||||
tableData = append(
|
||||
tableData,
|
||||
|
||||
@@ -67,9 +67,9 @@ 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.
|
||||
// grpcRunE wraps a cobra [cobra.Command.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,
|
||||
) func(*cobra.Command, []string) error {
|
||||
@@ -103,7 +103,7 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
||||
|
||||
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).
|
||||
@@ -112,9 +112,9 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
||||
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
|
||||
// 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.
|
||||
// The actual gRPC connection uses [net.Dial] which handles sockets properly.
|
||||
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
@@ -269,7 +269,7 @@ 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"`
|
||||
|
||||
@@ -58,6 +58,14 @@ const (
|
||||
space3XL = "4rem" //nolint:unused // 64px - 3x extra large spacing
|
||||
)
|
||||
|
||||
// Shared CSS value constants used across templates.
|
||||
const (
|
||||
cssBorderHS = "1px solid var(--hs-border)" //nolint:unused // Shared HS border
|
||||
cssBreakWord = "break-word" //nolint:unused // Word wrapping
|
||||
cssCenter = "center" //nolint:unused // Center alignment
|
||||
cssOverflowWrap = "overflow-wrap" //nolint:unused // CSS property key
|
||||
)
|
||||
|
||||
// Typography System
|
||||
// EXTRACTED FROM: https://headscale.net/stable/assets/stylesheets/main.342714a4.min.css
|
||||
// Material for MkDocs typography - exact values from .md-typeset CSS.
|
||||
@@ -121,8 +129,8 @@ func card(title string, children ...elem.Node) *elem.Element {
|
||||
return elem.Div(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Background: "var(--hs-bg)",
|
||||
styles.Border: "1px solid var(--hs-border)",
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.Border: cssBorderHS,
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.Padding: "clamp(1rem, 3vw, 1.5rem)",
|
||||
styles.MarginBottom: spaceL,
|
||||
styles.BoxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
@@ -147,8 +155,8 @@ func codeBlock(code string) *elem.Element {
|
||||
styles.FontSize: fontSizeCode, // 0.85em
|
||||
styles.LineHeight: lineHeightCode, // 1.4
|
||||
styles.OverflowX: "auto", // Horizontal scroll
|
||||
"overflow-wrap": "break-word", // Word wrapping
|
||||
"word-wrap": "break-word", // Legacy support
|
||||
cssOverflowWrap: cssBreakWord, // Word wrapping
|
||||
"word-wrap": cssBreakWord, // Legacy support
|
||||
styles.WhiteSpace: "pre-wrap", // Preserve whitespace
|
||||
styles.MarginTop: spaceM, // 1em
|
||||
styles.MarginBottom: spaceM, // 1em
|
||||
@@ -171,7 +179,7 @@ func baseTypesetStyles() styles.Props {
|
||||
styles.LineHeight: lineHeightBase, // 1.6
|
||||
styles.Color: colorTextPrimary,
|
||||
styles.FontFamily: fontFamilySystem,
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
styles.TextAlign: "left",
|
||||
}
|
||||
}
|
||||
@@ -190,7 +198,7 @@ func h1Styles() styles.Props {
|
||||
styles.FontWeight: "300",
|
||||
"letter-spacing": "-0.01em",
|
||||
styles.FontFamily: fontFamilySystem, // Roboto
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +216,7 @@ func h2Styles() styles.Props {
|
||||
"letter-spacing": "-0.01em",
|
||||
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
||||
styles.FontFamily: fontFamilySystem, // Roboto
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +234,7 @@ func h3Styles() styles.Props {
|
||||
"letter-spacing": "-0.01em",
|
||||
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
||||
styles.FontFamily: fontFamilySystem, // Roboto
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +250,7 @@ func paragraphStyles() styles.Props {
|
||||
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
||||
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87)
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +268,7 @@ func orderedListStyles() styles.Props {
|
||||
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
||||
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87) - inherited from .md-typeset
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +286,7 @@ func unorderedListStyles() styles.Props {
|
||||
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
||||
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87) - inherited from .md-typeset
|
||||
"overflow-wrap": "break-word",
|
||||
cssOverflowWrap: cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +300,7 @@ func linkStyles() styles.Props {
|
||||
return styles.Props{
|
||||
styles.Color: colorPrimaryAccent, // #4051b5 - var(--md-primary-fg-color)
|
||||
styles.TextDecoration: "none",
|
||||
"word-break": "break-word",
|
||||
"word-break": cssBreakWord,
|
||||
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
||||
}
|
||||
}
|
||||
@@ -310,7 +318,7 @@ func inlineCodeStyles() styles.Props {
|
||||
styles.FontSize: fontSizeCode, // 0.85em
|
||||
styles.FontFamily: fontFamilyCode, // Roboto Mono
|
||||
styles.Padding: "0 0.2941176471em",
|
||||
"word-break": "break-word",
|
||||
"word-break": cssBreakWord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +346,7 @@ func orDivider() *elem.Element {
|
||||
return elem.Div(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "flex",
|
||||
styles.AlignItems: "center",
|
||||
styles.AlignItems: cssCenter,
|
||||
styles.Gap: spaceM,
|
||||
styles.MarginTop: space2XL,
|
||||
styles.MarginBottom: space2XL,
|
||||
@@ -369,12 +377,12 @@ func successBox(heading string, children ...elem.Node) *elem.Element {
|
||||
attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "flex",
|
||||
styles.AlignItems: "center",
|
||||
styles.AlignItems: cssCenter,
|
||||
styles.Gap: spaceM,
|
||||
styles.Padding: spaceL,
|
||||
styles.BackgroundColor: "var(--hs-success-bg)",
|
||||
styles.Border: "1px solid var(--hs-success)",
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.MarginBottom: spaceXL,
|
||||
}.ToInline(),
|
||||
attrs.Role: "status",
|
||||
@@ -414,12 +422,12 @@ func errorBox(heading string, children ...elem.Node) *elem.Element {
|
||||
attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "flex",
|
||||
styles.AlignItems: "center",
|
||||
styles.AlignItems: cssCenter,
|
||||
styles.Gap: spaceM,
|
||||
styles.Padding: spaceL,
|
||||
styles.BackgroundColor: "var(--hs-error-bg)",
|
||||
styles.Border: "1px solid var(--hs-error)",
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.MarginBottom: spaceXL,
|
||||
}.ToInline(),
|
||||
attrs.Role: "alert",
|
||||
@@ -462,7 +470,7 @@ func warningBox(title, message string) *elem.Element {
|
||||
styles.Padding: spaceL,
|
||||
styles.BackgroundColor: "var(--hs-warning-bg)",
|
||||
styles.Border: "1px solid var(--hs-warning-border)",
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.MarginTop: spaceL,
|
||||
styles.MarginBottom: spaceL,
|
||||
}.ToInline(),
|
||||
@@ -492,7 +500,7 @@ func downloadButton(href, text string) *elem.Element {
|
||||
attrs.Download: "headscale_macos.mobileconfig",
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "inline-flex",
|
||||
styles.AlignItems: "center",
|
||||
styles.AlignItems: cssCenter,
|
||||
styles.Padding: "0.75rem 1.5rem",
|
||||
styles.BackgroundColor: "var(--md-primary-fg-color)",
|
||||
styles.Color: "#ffffff",
|
||||
@@ -542,8 +550,8 @@ func detailsBox(summary string, children ...elem.Node) *elem.Element {
|
||||
return elem.Details(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Background: "var(--hs-bg)",
|
||||
styles.Border: "1px solid var(--hs-border)",
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.Border: cssBorderHS,
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.Padding: spaceS + " " + spaceM,
|
||||
styles.MarginTop: spaceL,
|
||||
styles.MarginBottom: spaceL,
|
||||
@@ -584,7 +592,7 @@ func statusMessage(message string, isSuccess bool) *elem.Element {
|
||||
styles.Padding: spaceM,
|
||||
styles.BackgroundColor: bgColor,
|
||||
styles.Color: textColor,
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.BorderRadius: spaceS,
|
||||
styles.Border: "1px solid " + textColor,
|
||||
styles.MarginBottom: spaceL,
|
||||
styles.FontSize: fontSizeBase,
|
||||
|
||||
@@ -17,7 +17,7 @@ type PingResult struct {
|
||||
// Status is "ok", "timeout", or "error".
|
||||
Status string
|
||||
|
||||
// Latency is the round-trip time (only meaningful when Status is "ok").
|
||||
// Latency is the round-trip time (only meaningful when [PingResult.Status] is "ok").
|
||||
Latency time.Duration
|
||||
|
||||
// NodeID is the ID of the pinged node.
|
||||
@@ -27,7 +27,7 @@ type PingResult struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// ConnectedNode is a node currently connected to the batcher,
|
||||
// ConnectedNode is a node currently connected to the [mapper.Batcher],
|
||||
// displayed as a quick-ping link on the debug ping page.
|
||||
type ConnectedNode struct {
|
||||
ID types.NodeID
|
||||
@@ -36,7 +36,7 @@ type ConnectedNode struct {
|
||||
}
|
||||
|
||||
// PingPage renders the /debug/ping page with a form, optional result,
|
||||
// and a list of connected nodes as quick-ping links.
|
||||
// and a list of connected nodes ([ConnectedNode]) as quick-ping links.
|
||||
func PingPage(query string, result *PingResult, nodes []ConnectedNode) *elem.Element {
|
||||
children := []elem.Node{
|
||||
headscaleLogo(),
|
||||
@@ -94,7 +94,7 @@ func pingForm(query string) *elem.Element {
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "flex",
|
||||
styles.Gap: spaceS,
|
||||
styles.AlignItems: "center",
|
||||
styles.AlignItems: cssCenter,
|
||||
styles.FlexWrap: "wrap",
|
||||
styles.MarginTop: spaceM,
|
||||
}.ToInline(),
|
||||
@@ -107,7 +107,7 @@ func pingForm(query string) *elem.Element {
|
||||
attrs.Autofocus: "true",
|
||||
attrs.Style: styles.Props{
|
||||
styles.Padding: "0.75rem " + spaceM,
|
||||
styles.Border: "1px solid var(--hs-border)",
|
||||
styles.Border: cssBorderHS,
|
||||
styles.BorderRadius: "0.375rem",
|
||||
styles.Width: "280px",
|
||||
styles.MaxWidth: "100%",
|
||||
|
||||
@@ -31,11 +31,11 @@ type RegisterConfirmInfo struct {
|
||||
User string
|
||||
|
||||
// Hostname is the hostname the registering tailscaled instance
|
||||
// reported in its RegisterRequest.
|
||||
// reported in its [tailcfg.RegisterRequest].
|
||||
Hostname string
|
||||
|
||||
// OS is the operating system the registering tailscaled reported.
|
||||
// May be the empty string when the client did not send Hostinfo.
|
||||
// May be the empty string when the client did not send [tailcfg.Hostinfo].
|
||||
OS string
|
||||
|
||||
// MachineKey is the short fingerprint of the registering machine
|
||||
@@ -110,13 +110,13 @@ func deviceTable(rows [4][2]string) *elem.Element {
|
||||
styles.FontWeight: "600",
|
||||
styles.WhiteSpace: "nowrap",
|
||||
styles.Color: "var(--md-default-fg-color--light)",
|
||||
styles.BorderBottom: "1px solid var(--hs-border)",
|
||||
styles.BorderBottom: cssBorderHS,
|
||||
}.ToInline(),
|
||||
}, elem.Text(row[0])),
|
||||
elem.Td(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Padding: "0.5rem 0",
|
||||
styles.BorderBottom: "1px solid var(--hs-border)",
|
||||
styles.BorderBottom: cssBorderHS,
|
||||
}.ToInline(),
|
||||
}, val),
|
||||
))
|
||||
|
||||
@@ -32,14 +32,14 @@ func DefaultConfigEnv() map[string]string {
|
||||
|
||||
// Embedded DERP is the default for test isolation.
|
||||
// Tests should not depend on external DERP infrastructure.
|
||||
// Use WithPublicDERP() to opt out for tests that explicitly
|
||||
// Use [WithPublicDERP] to opt out for tests that explicitly
|
||||
// need public DERP relays.
|
||||
"HEADSCALE_DERP_URLS": "",
|
||||
"HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "false",
|
||||
"HEADSCALE_DERP_UPDATE_FREQUENCY": "1m",
|
||||
"HEADSCALE_DERP_SERVER_ENABLED": "true",
|
||||
"HEADSCALE_DERP_SERVER_REGION_ID": "999",
|
||||
"HEADSCALE_DERP_SERVER_REGION_CODE": "headscale",
|
||||
"HEADSCALE_DERP_SERVER_REGION_CODE": binHeadscale,
|
||||
"HEADSCALE_DERP_SERVER_REGION_NAME": "Headscale Embedded DERP",
|
||||
"HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478",
|
||||
"HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key",
|
||||
|
||||
@@ -48,6 +48,9 @@ const (
|
||||
headscaleDefaultPort = 8080
|
||||
IntegrationTestDockerFileName = "Dockerfile.integration"
|
||||
defaultDirPerm = 0o755
|
||||
binHeadscale = "headscale"
|
||||
flagOutput = "--output"
|
||||
acceptJSON = "Accept: application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -94,8 +97,8 @@ type HeadscaleInContainer struct {
|
||||
// Headscale instance.
|
||||
type Option = func(c *HeadscaleInContainer)
|
||||
|
||||
// WithACLPolicy adds a hscontrol.ACLPolicy policy to the
|
||||
// HeadscaleInContainer instance.
|
||||
// WithACLPolicy adds a [policyv2.Policy] to the
|
||||
// [HeadscaleInContainer] instance.
|
||||
func WithACLPolicy(acl *policyv2.Policy) Option {
|
||||
return func(hsic *HeadscaleInContainer) {
|
||||
if acl == nil {
|
||||
@@ -127,7 +130,7 @@ func WithoutTLS() Option {
|
||||
|
||||
// WithCustomTLS uses the given certificates for the Headscale instance.
|
||||
// The caCert is installed into the container's trust store and returned
|
||||
// by GetCert() so that clients can trust this server.
|
||||
// by [HeadscaleInContainer.GetCert] so that clients can trust this server.
|
||||
func WithCustomTLS(caCert, cert, key []byte) Option {
|
||||
return func(hsic *HeadscaleInContainer) {
|
||||
hsic.tlsCACert = caCert
|
||||
@@ -320,7 +323,7 @@ func (hsic *HeadscaleInContainer) buildEntrypoint() []string {
|
||||
return []string{"/bin/bash", "-c", strings.Join(commands, " ; ")}
|
||||
}
|
||||
|
||||
// New returns a new HeadscaleInContainer instance.
|
||||
// New returns a new [HeadscaleInContainer] instance.
|
||||
//
|
||||
//nolint:gocyclo // complex container setup with many options
|
||||
func New(
|
||||
@@ -364,8 +367,8 @@ func New(
|
||||
|
||||
// TLS is enabled by default for all integration tests.
|
||||
// Generate a self-signed certificate if TLS was not explicitly
|
||||
// disabled via WithoutTLS() and no custom cert was provided
|
||||
// via WithCustomTLS().
|
||||
// disabled via [WithoutTLS] and no custom cert was provided
|
||||
// via [WithCustomTLS].
|
||||
if !hsic.noTLS && len(hsic.tlsCert) == 0 {
|
||||
caCert, cert, key, err := integrationutil.CreateCertificate(hsic.hostname)
|
||||
if err != nil {
|
||||
@@ -394,9 +397,9 @@ func New(
|
||||
if hsic.postgres {
|
||||
hsic.env["HEADSCALE_DATABASE_TYPE"] = "postgres"
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_HOST"] = "postgres-" + hash
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_USER"] = "headscale"
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_PASS"] = "headscale"
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_NAME"] = "headscale"
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_USER"] = binHeadscale
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_PASS"] = binHeadscale
|
||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_NAME"] = binHeadscale
|
||||
delete(hsic.env, "HEADSCALE_DATABASE_SQLITE_PATH")
|
||||
|
||||
// Determine postgres image - use prebuilt if available, otherwise pull from registry
|
||||
@@ -501,7 +504,7 @@ func New(
|
||||
}
|
||||
|
||||
// Add integration test labels if running under hi tool
|
||||
dockertestutil.DockerAddIntegrationLabels(runOptions, "headscale")
|
||||
dockertestutil.DockerAddIntegrationLabels(runOptions, binHeadscale)
|
||||
|
||||
var container *dockertest.Resource
|
||||
|
||||
@@ -509,7 +512,7 @@ func New(
|
||||
prebuiltImage := os.Getenv("HEADSCALE_INTEGRATION_HEADSCALE_IMAGE")
|
||||
|
||||
if prebuiltImage != "" {
|
||||
log.Printf("Using pre-built headscale image: %s", prebuiltImage)
|
||||
log.Printf("Using pre-built headscale image: %s", prebuiltImage) //nolint:gosec // G706: integration-only log of trusted env value
|
||||
// Parse image into repository and tag
|
||||
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
||||
if !ok {
|
||||
@@ -709,7 +712,7 @@ func (t *HeadscaleInContainer) Shutdown() (string, string, error) {
|
||||
}
|
||||
|
||||
// WriteLogs writes the current stdout/stderr log of the container to
|
||||
// the given io.Writers.
|
||||
// the given [io.Writer]s.
|
||||
func (t *HeadscaleInContainer) WriteLogs(stdout, stderr io.Writer) error {
|
||||
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
||||
}
|
||||
@@ -1010,13 +1013,13 @@ func (t *HeadscaleInContainer) GetHostMetricsPort() string {
|
||||
return t.hostMetricsPort
|
||||
}
|
||||
|
||||
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
|
||||
// GetHealthEndpoint returns a health endpoint for the [HeadscaleInContainer]
|
||||
// instance.
|
||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
||||
return t.GetEndpoint() + "/health"
|
||||
}
|
||||
|
||||
// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer.
|
||||
// GetEndpoint returns the Headscale endpoint for the [HeadscaleInContainer].
|
||||
func (t *HeadscaleInContainer) GetEndpoint() string {
|
||||
return t.getEndpoint(false)
|
||||
}
|
||||
@@ -1051,12 +1054,12 @@ func (t *HeadscaleInContainer) GetCert() []byte {
|
||||
return t.tlsCACert
|
||||
}
|
||||
|
||||
// GetHostname returns the hostname of the HeadscaleInContainer.
|
||||
// GetHostname returns the hostname of the [HeadscaleInContainer].
|
||||
func (t *HeadscaleInContainer) GetHostname() string {
|
||||
return t.hostname
|
||||
}
|
||||
|
||||
// GetIPInNetwork returns the IP address of the HeadscaleInContainer in the given network.
|
||||
// GetIPInNetwork returns the IP address of the [HeadscaleInContainer] in the given network.
|
||||
func (t *HeadscaleInContainer) GetIPInNetwork(network *dockertest.Network) string {
|
||||
return t.container.GetIPInNetwork(network)
|
||||
}
|
||||
@@ -1095,12 +1098,12 @@ func (t *HeadscaleInContainer) CreateUser(
|
||||
user string,
|
||||
) (*v1.User, error) {
|
||||
command := []string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
fmt.Sprintf("--email=%s@test.no", user),
|
||||
"--output",
|
||||
flagOutput,
|
||||
"json",
|
||||
}
|
||||
|
||||
@@ -1140,7 +1143,7 @@ type AuthKeyOptions struct {
|
||||
// This supports both user-owned and tags-only auth keys.
|
||||
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
|
||||
command := []string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
}
|
||||
|
||||
// Only add --user flag if User is specified
|
||||
@@ -1153,7 +1156,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
|
||||
"create",
|
||||
"--expiration",
|
||||
"24h",
|
||||
"--output",
|
||||
flagOutput,
|
||||
"json",
|
||||
)
|
||||
|
||||
@@ -1189,7 +1192,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
|
||||
}
|
||||
|
||||
// CreateAuthKey creates a new "authorisation key" for a User that can be used
|
||||
// to authorise a TailscaleClient with the Headscale instance.
|
||||
// to authorise a TailscaleClient with the [HeadscaleInContainer] instance.
|
||||
func (t *HeadscaleInContainer) CreateAuthKey(
|
||||
user uint64,
|
||||
reusable bool,
|
||||
@@ -1223,12 +1226,12 @@ func (t *HeadscaleInContainer) DeleteAuthKey(
|
||||
id uint64,
|
||||
) error {
|
||||
command := []string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
"preauthkeys",
|
||||
"delete",
|
||||
"--id",
|
||||
strconv.FormatUint(id, 10),
|
||||
"--output",
|
||||
flagOutput,
|
||||
"json",
|
||||
}
|
||||
|
||||
@@ -1275,13 +1278,13 @@ func (t *HeadscaleInContainer) ListNodes(
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
err := execUnmarshal([]string{"headscale", "nodes", "list", "--output", "json"})
|
||||
err := execUnmarshal([]string{binHeadscale, "nodes", "list", flagOutput, "json"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
for _, user := range users {
|
||||
command := []string{"headscale", "--user", user, "nodes", "list", "--output", "json"}
|
||||
command := []string{binHeadscale, "--user", user, "nodes", "list", flagOutput, "json"}
|
||||
|
||||
err := execUnmarshal(command)
|
||||
if err != nil {
|
||||
@@ -1299,12 +1302,12 @@ func (t *HeadscaleInContainer) ListNodes(
|
||||
|
||||
func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
|
||||
command := []string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
"nodes",
|
||||
"delete",
|
||||
"--identifier",
|
||||
strconv.FormatUint(nodeID, 10),
|
||||
"--output",
|
||||
flagOutput,
|
||||
"json",
|
||||
"--force",
|
||||
}
|
||||
@@ -1355,7 +1358,7 @@ func (t *HeadscaleInContainer) NodesByName() (map[string]*v1.Node, error) {
|
||||
|
||||
// ListUsers returns a list of users from Headscale.
|
||||
func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
||||
command := []string{"headscale", "users", "list", "--output", "json"}
|
||||
command := []string{binHeadscale, "users", "list", flagOutput, "json"}
|
||||
|
||||
result, _, err := dockertestutil.ExecuteCommand(
|
||||
t.container,
|
||||
@@ -1395,13 +1398,13 @@ func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
|
||||
// DeleteUser deletes a user from the Headscale instance.
|
||||
func (t *HeadscaleInContainer) DeleteUser(userID uint64) error {
|
||||
command := []string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
"users",
|
||||
"delete",
|
||||
"--identifier",
|
||||
strconv.FormatUint(userID, 10),
|
||||
"--force",
|
||||
"--output",
|
||||
flagOutput,
|
||||
"json",
|
||||
}
|
||||
|
||||
@@ -1444,7 +1447,7 @@ func (h *HeadscaleInContainer) SetPolicy(pol *policyv2.Policy) error {
|
||||
func (h *HeadscaleInContainer) reloadDatabasePolicy() error {
|
||||
_, err := h.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
binHeadscale,
|
||||
"policy",
|
||||
"set",
|
||||
"-f",
|
||||
@@ -1476,7 +1479,7 @@ func (h *HeadscaleInContainer) PID() (int, error) {
|
||||
// Use pidof to find the headscale process, which is more reliable than grep
|
||||
// as it only looks for the actual binary name, not processes that contain
|
||||
// "headscale" in their command line (like the dlv debugger).
|
||||
output, err := h.Execute([]string{"pidof", "headscale"})
|
||||
output, err := h.Execute([]string{"pidof", binHeadscale})
|
||||
if err != nil {
|
||||
// pidof returns exit code 1 when no process is found
|
||||
return 0, os.ErrNotExist
|
||||
@@ -1533,8 +1536,8 @@ func (h *HeadscaleInContainer) Reload() error {
|
||||
// ApproveRoutes approves routes for a node.
|
||||
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
|
||||
command := []string{
|
||||
"headscale", "nodes", "approve-routes",
|
||||
"--output", "json",
|
||||
binHeadscale, "nodes", "approve-routes",
|
||||
flagOutput, "json",
|
||||
"--identifier", strconv.FormatUint(id, 10),
|
||||
"--routes=" + strings.Join(util.PrefixesToString(routes), ","),
|
||||
}
|
||||
@@ -1568,9 +1571,9 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
|
||||
// SetTags API which is exposed via the CLI command: headscale nodes tag -i <id> -t <tags>.
|
||||
func (t *HeadscaleInContainer) SetNodeTags(nodeID uint64, tags []string) error {
|
||||
command := []string{
|
||||
"headscale", "nodes", "tag",
|
||||
binHeadscale, "nodes", "tag",
|
||||
"--identifier", strconv.FormatUint(nodeID, 10),
|
||||
"--output", "json",
|
||||
flagOutput, "json",
|
||||
}
|
||||
|
||||
// Add tags - the CLI expects -t flag for each tag or comma-separated
|
||||
@@ -1605,7 +1608,7 @@ func (t *HeadscaleInContainer) FetchPath(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||
pid, err := t.Execute([]string{"pidof", "headscale"})
|
||||
pid, err := t.Execute([]string{"pidof", binHeadscale})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1621,7 +1624,7 @@ func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||
func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error) {
|
||||
// Execute curl inside the container to access the debug endpoint locally
|
||||
command := []string{
|
||||
"curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/mapresponses",
|
||||
"curl", "-s", "-H", acceptJSON, "http://localhost:9090/debug/mapresponses",
|
||||
}
|
||||
|
||||
result, err := t.Execute(command)
|
||||
@@ -1641,7 +1644,7 @@ func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.M
|
||||
func (t *HeadscaleInContainer) PrimaryRoutes() (*types.DebugRoutes, error) {
|
||||
// Execute curl inside the container to access the debug endpoint locally
|
||||
command := []string{
|
||||
"curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/routes",
|
||||
"curl", "-s", "-H", acceptJSON, "http://localhost:9090/debug/routes",
|
||||
}
|
||||
|
||||
result, err := t.Execute(command)
|
||||
@@ -1661,7 +1664,7 @@ func (t *HeadscaleInContainer) PrimaryRoutes() (*types.DebugRoutes, error) {
|
||||
func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, error) {
|
||||
// Execute curl inside the container to access the debug endpoint locally
|
||||
command := []string{
|
||||
"curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/batcher",
|
||||
"curl", "-s", "-H", acceptJSON, "http://localhost:9090/debug/batcher",
|
||||
}
|
||||
|
||||
result, err := t.Execute(command)
|
||||
@@ -1677,11 +1680,11 @@ func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, erro
|
||||
return &debugInfo, nil
|
||||
}
|
||||
|
||||
// DebugNodeStore fetches the NodeStore data from the debug endpoint.
|
||||
// DebugNodeStore fetches the [state.NodeStore] data from the debug endpoint.
|
||||
func (t *HeadscaleInContainer) DebugNodeStore() (map[types.NodeID]types.Node, error) {
|
||||
// Execute curl inside the container to access the debug endpoint locally
|
||||
command := []string{
|
||||
"curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/nodestore",
|
||||
"curl", "-s", "-H", acceptJSON, "http://localhost:9090/debug/nodestore",
|
||||
}
|
||||
|
||||
result, err := t.Execute(command)
|
||||
@@ -1701,7 +1704,7 @@ func (t *HeadscaleInContainer) DebugNodeStore() (map[types.NodeID]types.Node, er
|
||||
func (t *HeadscaleInContainer) DebugFilter() ([]tailcfg.FilterRule, error) {
|
||||
// Execute curl inside the container to access the debug endpoint locally
|
||||
command := []string{
|
||||
"curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/filter",
|
||||
"curl", "-s", "-H", acceptJSON, "http://localhost:9090/debug/filter",
|
||||
}
|
||||
|
||||
result, err := t.Execute(command)
|
||||
|
||||
@@ -43,6 +43,7 @@ const (
|
||||
dockerContextPath = "../."
|
||||
caCertRoot = "/usr/local/share/ca-certificates"
|
||||
dockerExecuteTimeout = 60 * time.Second
|
||||
tailscaleBin = "tailscale"
|
||||
)
|
||||
|
||||
// defaultPingTimeoutVal returns the per-attempt timeout for tailscale ping.
|
||||
@@ -127,7 +128,7 @@ func WithCACert(cert []byte) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNetwork sets the Docker container network to use with
|
||||
// WithNetwork sets the Docker [dockertest.Network] to use with
|
||||
// the Tailscale instance.
|
||||
func WithNetwork(network *dockertest.Network) Option {
|
||||
return func(tsic *TailscaleInContainer) {
|
||||
@@ -215,7 +216,7 @@ func WithBuildTag(tag string) Option {
|
||||
}
|
||||
|
||||
// WithExtraLoginArgs adds additional arguments to the `tailscale up` command
|
||||
// as part of the Login function.
|
||||
// as part of the [TailscaleInContainer.Login] function.
|
||||
func WithExtraLoginArgs(args []string) Option {
|
||||
return func(tsic *TailscaleInContainer) {
|
||||
tsic.extraLoginArgs = append(tsic.extraLoginArgs, args...)
|
||||
@@ -270,7 +271,7 @@ func (t *TailscaleInContainer) buildEntrypoint() []string {
|
||||
commands = append(commands, "while ! ip route show default >/dev/null 2>&1; do sleep 0.1; done")
|
||||
|
||||
// If CA certs are configured, wait for them to be written by the Go code
|
||||
// (certs are written after container start via tsic.WriteFile)
|
||||
// (certs are written after container start via [TailscaleInContainer.WriteFile])
|
||||
if len(t.caCerts) > 0 {
|
||||
commands = append(commands,
|
||||
fmt.Sprintf("while [ ! -f %s/user-0.crt ]; do sleep 0.1; done", caCertRoot))
|
||||
@@ -389,7 +390,7 @@ func New(
|
||||
}
|
||||
|
||||
// Add integration test labels if running under hi tool
|
||||
dockertestutil.DockerAddIntegrationLabels(tailscaleOptions, "tailscale")
|
||||
dockertestutil.DockerAddIntegrationLabels(tailscaleOptions, tailscaleBin)
|
||||
|
||||
var container *dockertest.Resource
|
||||
|
||||
@@ -413,13 +414,13 @@ func New(
|
||||
// the pre-built image as it won't have the necessary code compiled in.
|
||||
hasBuildTags := len(tsic.buildConfig.tags) > 0
|
||||
if hasBuildTags && prebuiltImage != "" {
|
||||
log.Printf("Ignoring pre-built image %s because custom build tags are required: %v",
|
||||
log.Printf("Ignoring pre-built image %s because custom build tags are required: %v", //nolint:gosec // G706: integration-only log of trusted env value
|
||||
prebuiltImage, tsic.buildConfig.tags)
|
||||
prebuiltImage = ""
|
||||
}
|
||||
|
||||
if prebuiltImage != "" {
|
||||
log.Printf("Using pre-built tailscale image: %s", prebuiltImage)
|
||||
log.Printf("Using pre-built tailscale image: %s", prebuiltImage) //nolint:gosec // G706: integration-only log of trusted env value
|
||||
|
||||
// Parse image into repository and tag
|
||||
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
||||
@@ -609,7 +610,7 @@ func (t *TailscaleInContainer) Version() string {
|
||||
return t.version
|
||||
}
|
||||
|
||||
// ContainerID returns the Docker container ID of the TailscaleInContainer
|
||||
// ContainerID returns the Docker container ID of the [TailscaleInContainer]
|
||||
// instance.
|
||||
func (t *TailscaleInContainer) ContainerID() string {
|
||||
return t.container.Container.ID
|
||||
@@ -657,7 +658,7 @@ func (t *TailscaleInContainer) buildLoginCommand(
|
||||
loginServer, authKey string,
|
||||
) []string {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"up",
|
||||
"--login-server=" + loginServer,
|
||||
"--hostname=" + t.hostname,
|
||||
@@ -736,12 +737,12 @@ func (t *TailscaleInContainer) LoginWithURL(
|
||||
|
||||
// Logout runs the logout routine on the given Tailscale instance.
|
||||
func (t *TailscaleInContainer) Logout() error {
|
||||
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
||||
_, _, err := t.Execute([]string{tailscaleBin, "logout"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, stderr, _ := t.Execute([]string{"tailscale", "status"})
|
||||
stdout, stderr, _ := t.Execute([]string{tailscaleBin, "status"})
|
||||
if !strings.Contains(stdout+stderr, "Logged out.") {
|
||||
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr) //nolint:err113
|
||||
}
|
||||
@@ -768,7 +769,7 @@ func (t *TailscaleInContainer) Restart() error {
|
||||
// We use exponential backoff to poll until we can successfully execute a command
|
||||
_, err = backoff.Retry(context.Background(), func() (struct{}, error) {
|
||||
// Try to execute a simple command to verify the container is responsive
|
||||
_, _, err := t.Execute([]string{"tailscale", "version"}, dockertestutil.ExecuteCommandTimeout(5*time.Second))
|
||||
_, _, err := t.Execute([]string{tailscaleBin, "version"}, dockertestutil.ExecuteCommandTimeout(5*time.Second))
|
||||
if err != nil {
|
||||
return struct{}{}, fmt.Errorf("container not ready: %w", err)
|
||||
}
|
||||
@@ -785,7 +786,7 @@ func (t *TailscaleInContainer) Restart() error {
|
||||
// Up runs `tailscale up` with no arguments.
|
||||
func (t *TailscaleInContainer) Up() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"up",
|
||||
}
|
||||
|
||||
@@ -804,7 +805,7 @@ func (t *TailscaleInContainer) Up() error {
|
||||
// Down runs `tailscale down` with no arguments.
|
||||
func (t *TailscaleInContainer) Down() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"down",
|
||||
}
|
||||
|
||||
@@ -835,7 +836,7 @@ func (t *TailscaleInContainer) ReconnectToNetwork(network *dockertest.Network) e
|
||||
return dockertestutil.ReconnectContainerToNetwork(t.pool, network, t.hostname)
|
||||
}
|
||||
|
||||
// IPs returns the netip.Addr of the Tailscale instance.
|
||||
// IPs returns the [netip.Addr] of the Tailscale instance.
|
||||
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||
if len(t.ips) != 0 {
|
||||
return t.ips, nil
|
||||
@@ -844,7 +845,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||
// Retry with exponential backoff to handle eventual consistency
|
||||
ips, err := backoff.Retry(context.Background(), func() ([]netip.Addr, error) {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"ip",
|
||||
}
|
||||
|
||||
@@ -926,10 +927,10 @@ func (t *TailscaleInContainer) MustIPv6() netip.Addr {
|
||||
panic("no ipv6 found")
|
||||
}
|
||||
|
||||
// Status returns the ipnstate.Status of the Tailscale instance.
|
||||
// Status returns the [ipnstate.Status] of the Tailscale instance.
|
||||
func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"status",
|
||||
"--json",
|
||||
}
|
||||
@@ -954,7 +955,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
|
||||
return &status, err
|
||||
}
|
||||
|
||||
// MustStatus returns the ipnstate.Status of the Tailscale instance.
|
||||
// MustStatus returns the [ipnstate.Status] of the Tailscale instance.
|
||||
func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
|
||||
status, err := t.Status()
|
||||
if err != nil {
|
||||
@@ -979,7 +980,7 @@ func (t *TailscaleInContainer) MustID() types.NodeID {
|
||||
return types.NodeID(id)
|
||||
}
|
||||
|
||||
// Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
|
||||
// Netmap returns the current Netmap ([netmap.NetworkMap]) of the Tailscale instance.
|
||||
// Only works with Tailscale 1.56 and newer.
|
||||
// Panics if version is lower then minimum.
|
||||
func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
||||
@@ -988,7 +989,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
||||
}
|
||||
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"debug",
|
||||
"netmap",
|
||||
}
|
||||
@@ -1014,7 +1015,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
||||
return &nm, err
|
||||
}
|
||||
|
||||
// Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
|
||||
// Netmap returns the current Netmap ([netmap.NetworkMap]) of the Tailscale instance.
|
||||
// This implementation is based on getting the netmap from `tailscale debug watch-ipn`
|
||||
// as there seem to be some weirdness omitting endpoint and DERP info if we use
|
||||
// Patch updates.
|
||||
@@ -1037,8 +1038,8 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
||||
// return notify.NetMap, nil
|
||||
// }
|
||||
|
||||
// watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until
|
||||
// it gets one that has a netmap.NetworkMap.
|
||||
// watchIPN watches `tailscale debug watch-ipn` for a [ipn.Notify] object until
|
||||
// it gets one that has a [netmap.NetworkMap].
|
||||
//
|
||||
//nolint:unused
|
||||
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
|
||||
@@ -1115,7 +1116,7 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
|
||||
}
|
||||
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"debug",
|
||||
"derp",
|
||||
region,
|
||||
@@ -1138,10 +1139,10 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
|
||||
return &report, err
|
||||
}
|
||||
|
||||
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.
|
||||
// Netcheck returns the current Netcheck Report ([netcheck.Report]) of the Tailscale instance.
|
||||
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
tailscaleBin,
|
||||
"netcheck",
|
||||
"--format=json",
|
||||
}
|
||||
@@ -1258,7 +1259,7 @@ func (t *TailscaleInContainer) waitForBackendState(state string, timeout time.Du
|
||||
continue // Keep retrying on status errors
|
||||
}
|
||||
|
||||
// ipnstate.Status.CurrentTailnet was added in Tailscale 1.22.0
|
||||
// [ipnstate.Status.CurrentTailnet] was added in Tailscale 1.22.0
|
||||
// https://github.com/tailscale/tailscale/pull/3865
|
||||
//
|
||||
// Before that, we can check the BackendState to see if the
|
||||
@@ -1382,7 +1383,7 @@ func WithPingUntilDirect(direct bool) PingOption {
|
||||
}
|
||||
|
||||
// Ping executes the Tailscale ping command and pings a hostname
|
||||
// or IP. It accepts a series of PingOption.
|
||||
// or IP. It accepts a series of [PingOption].
|
||||
// TODO(kradalby): Make multiping, go routine magic.
|
||||
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
||||
args := pingArgs{
|
||||
@@ -1397,7 +1398,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
||||
|
||||
command := make([]string, 0, 6)
|
||||
command = append(command,
|
||||
"tailscale", "ping",
|
||||
tailscaleBin, "ping",
|
||||
fmt.Sprintf("--timeout=%s", args.timeout),
|
||||
fmt.Sprintf("--c=%d", args.count),
|
||||
"--until-direct="+strconv.FormatBool(args.direct),
|
||||
@@ -1494,7 +1495,7 @@ const (
|
||||
)
|
||||
|
||||
// Curl executes the Tailscale curl command and curls a hostname
|
||||
// or IP. It accepts a series of CurlOption.
|
||||
// or IP. It accepts a series of [CurlOption].
|
||||
func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, error) {
|
||||
args := curlArgs{
|
||||
connectionTimeout: defaultConnectionTimeout,
|
||||
@@ -1536,7 +1537,7 @@ func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, err
|
||||
// curl exit 0 with an empty body usually means a mid-stream reset
|
||||
// after headers (HTTP 200 with the connection torn down before the
|
||||
// body arrived). Without this signal, callers wrapping the call in
|
||||
// EventuallyWithT see assert.NoError pass and assert.Len fail with
|
||||
// [assert.EventuallyWithT] see [assert.NoError] pass and [assert.Len] fail with
|
||||
// no error to drive a retry.
|
||||
if result == "" {
|
||||
return result, fmt.Errorf("%w: %s from %s", errCurlEmptyResponseBody, url, t.Hostname())
|
||||
@@ -1558,7 +1559,7 @@ func (t *TailscaleInContainer) CurlFailFast(url string) (string, error) {
|
||||
|
||||
func (t *TailscaleInContainer) Traceroute(ip netip.Addr) (util.Traceroute, error) {
|
||||
// -w 1: wait at most 1s for each probe response. busybox's default
|
||||
// is 5s, which means an EventuallyWithT loop at 200ms ticks
|
||||
// is 5s, which means an [assert.EventuallyWithT] loop at 200ms ticks
|
||||
// can spend 25 ticks worth of budget on a single Traceroute.
|
||||
// -q 1: send 1 probe per hop instead of 3. The HA tests only care
|
||||
// about the first hop's identity; the other probes are dead
|
||||
@@ -1603,7 +1604,7 @@ func (t *TailscaleInContainer) SaveLog(path string) (string, string, error) {
|
||||
}
|
||||
|
||||
// WriteLogs writes the current stdout/stderr log of the container to
|
||||
// the given io.Writers.
|
||||
// the given [io.Writer]s.
|
||||
func (t *TailscaleInContainer) WriteLogs(stdout, stderr io.Writer) error {
|
||||
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
||||
}
|
||||
@@ -1677,7 +1678,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
|
||||
return &p.Persist.PrivateNodeKey, nil
|
||||
}
|
||||
|
||||
// ConnectToNetwork connects the Tailscale container to an additional Docker network.
|
||||
// ConnectToNetwork connects the Tailscale container to an additional Docker [dockertest.Network].
|
||||
func (t *TailscaleInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||
return t.container.ConnectToNetwork(network)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user