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:
parent
64c398f2c2
commit
e00c899219
@ -41,9 +41,9 @@ var apiKeysCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listAPIKeys = &cobra.Command{
|
var listAPIKeys = &cobra.Command{
|
||||||
Use: "list",
|
Use: cmdList,
|
||||||
Short: "List the Api keys for headscale",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -51,9 +51,8 @@ var listAPIKeys = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||||
tableData := pterm.TableData{
|
tableData := make(pterm.TableData, 1, 1+len(response.GetApiKeys()))
|
||||||
{"ID", "Prefix", "Expiration", "Created"},
|
tableData[0] = []string{"ID", "Prefix", colExpiration, colCreated}
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range response.GetApiKeys() {
|
for _, key := range response.GetApiKeys() {
|
||||||
expiration := "-"
|
expiration := "-"
|
||||||
@ -82,7 +81,7 @@ var createAPIKeyCmd = &cobra.Command{
|
|||||||
Creates a new Api key, the Api key is only visible on creation
|
Creates a new Api key, the Api key is only visible on creation
|
||||||
and cannot be retrieved again.
|
and cannot be retrieved again.
|
||||||
If you lose a key, create a new one and revoke (expire) the old one.`,
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
expiration, err := expirationFromFlag(cmd)
|
expiration, err := expirationFromFlag(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -117,9 +116,9 @@ func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expireAPIKeyCmd = &cobra.Command{
|
var expireAPIKeyCmd = &cobra.Command{
|
||||||
Use: "expire",
|
Use: cmdExpire,
|
||||||
Short: "Expire an ApiKey",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -139,9 +138,9 @@ var expireAPIKeyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var deleteAPIKeyCmd = &cobra.Command{
|
var deleteAPIKeyCmd = &cobra.Command{
|
||||||
Use: "delete",
|
Use: cmdDelete,
|
||||||
Short: "Delete an ApiKey",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -89,9 +89,9 @@ var registerNodeCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listNodesCmd = &cobra.Command{
|
var listNodesCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: cmdList,
|
||||||
Short: "List nodes",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
user, _ := cmd.Flags().GetString("user")
|
user, _ := cmd.Flags().GetString("user")
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ var listNodesCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return printListOutput(cmd, response.GetNodes(), func() error {
|
return printListOutput(cmd, response.GetNodes(), func() error {
|
||||||
tableData, err := nodesToPtables(user, response.GetNodes())
|
tableData, err := nodesToPtables(response.GetNodes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("converting to table: %w", err)
|
return fmt.Errorf("converting to table: %w", err)
|
||||||
}
|
}
|
||||||
@ -145,12 +145,12 @@ var listNodeRoutesCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expireNodeCmd = &cobra.Command{
|
var expireNodeCmd = &cobra.Command{
|
||||||
Use: "expire",
|
Use: cmdExpire,
|
||||||
Short: "Expire (log out) a node in your network",
|
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.
|
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).`,
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||||
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
||||||
@ -229,9 +229,9 @@ var renameNodeCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var deleteNodeCmd = &cobra.Command{
|
var deleteNodeCmd = &cobra.Command{
|
||||||
Use: "delete",
|
Use: cmdDelete,
|
||||||
Short: "Delete a node",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ var deleteNodeCmd = &cobra.Command{
|
|||||||
"Do you want to remove the node %s?",
|
"Do you want to remove the node %s?",
|
||||||
getResponse.GetNode().GetName(),
|
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)
|
_, err = client.DeleteNode(ctx, deleteRequest)
|
||||||
@ -262,7 +262,7 @@ var deleteNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
return printOutput(
|
return printOutput(
|
||||||
cmd,
|
cmd,
|
||||||
map[string]string{"Result": "Node deleted"},
|
map[string]string{colResult: "Node deleted"},
|
||||||
"Node deleted",
|
"Node deleted",
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@ -304,10 +304,7 @@ be assigned to nodes.`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodesToPtables(
|
func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
|
||||||
currentUser string,
|
|
||||||
nodes []*v1.Node,
|
|
||||||
) (pterm.TableData, error) {
|
|
||||||
tableHeader := []string{
|
tableHeader := []string{
|
||||||
"ID",
|
"ID",
|
||||||
"Hostname",
|
"Hostname",
|
||||||
@ -319,11 +316,12 @@ func nodesToPtables(
|
|||||||
"IP addresses",
|
"IP addresses",
|
||||||
"Ephemeral",
|
"Ephemeral",
|
||||||
"Last seen",
|
"Last seen",
|
||||||
"Expiration",
|
colExpiration,
|
||||||
"Connected",
|
"Connected",
|
||||||
"Expired",
|
"Expired",
|
||||||
}
|
}
|
||||||
tableData := pterm.TableData{tableHeader}
|
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||||
|
tableData[0] = tableHeader
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
var ephemeral bool
|
var ephemeral bool
|
||||||
@ -447,7 +445,8 @@ func nodeRoutesToPtables(
|
|||||||
"Available",
|
"Available",
|
||||||
"Serving (Primary)",
|
"Serving (Primary)",
|
||||||
}
|
}
|
||||||
tableData := pterm.TableData{tableHeader}
|
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||||
|
tableData[0] = tableHeader
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
nodeData := []string{
|
nodeData := []string{
|
||||||
|
|||||||
@ -61,7 +61,7 @@ var policyCmd = &cobra.Command{
|
|||||||
var getPolicy = &cobra.Command{
|
var getPolicy = &cobra.Command{
|
||||||
Use: "get",
|
Use: "get",
|
||||||
Short: "Print the current ACL Policy",
|
Short: "Print the current ACL Policy",
|
||||||
Aliases: []string{"show", "view", "fetch"},
|
Aliases: []string{cmdShow, "view", "fetch"},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
var policyData string
|
var policyData string
|
||||||
|
|
||||||
@ -205,9 +205,9 @@ var checkPolicy = &cobra.Command{
|
|||||||
return fmt.Errorf("loading nodes: %w", err)
|
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).
|
// 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.
|
// tests and sshTests blocks.
|
||||||
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
|
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -42,9 +42,9 @@ var preauthkeysCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listPreAuthKeys = &cobra.Command{
|
var listPreAuthKeys = &cobra.Command{
|
||||||
Use: "list",
|
Use: cmdList,
|
||||||
Short: "List all preauthkeys",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -52,17 +52,16 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
||||||
tableData := pterm.TableData{
|
tableData := make(pterm.TableData, 1, 1+len(response.GetPreAuthKeys()))
|
||||||
{
|
tableData[0] = []string{
|
||||||
"ID",
|
"ID",
|
||||||
"Key/Prefix",
|
"Key/Prefix",
|
||||||
"Reusable",
|
"Reusable",
|
||||||
"Ephemeral",
|
"Ephemeral",
|
||||||
"Used",
|
"Used",
|
||||||
"Expiration",
|
colExpiration,
|
||||||
"Created",
|
colCreated,
|
||||||
"Owner",
|
"Owner",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range response.GetPreAuthKeys() {
|
for _, key := range response.GetPreAuthKeys() {
|
||||||
@ -100,7 +99,7 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
var createPreAuthKeyCmd = &cobra.Command{
|
var createPreAuthKeyCmd = &cobra.Command{
|
||||||
Use: "create",
|
Use: "create",
|
||||||
Short: "Creates a new preauthkey",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
user, _ := cmd.Flags().GetUint64("user")
|
user, _ := cmd.Flags().GetUint64("user")
|
||||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||||
@ -130,9 +129,9 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expirePreAuthKeyCmd = &cobra.Command{
|
var expirePreAuthKeyCmd = &cobra.Command{
|
||||||
Use: "expire",
|
Use: cmdExpire,
|
||||||
Short: "Expire a preauthkey",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
|
|
||||||
@ -154,9 +153,9 @@ var expirePreAuthKeyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var deletePreAuthKeyCmd = &cobra.Command{
|
var deletePreAuthKeyCmd = &cobra.Command{
|
||||||
Use: "delete",
|
Use: cmdDelete,
|
||||||
Short: "Delete a preauthkey",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func init() {
|
|||||||
Bool("force", false, "Disable prompts and forces the execution")
|
Bool("force", false, "Disable prompts and forces the execution")
|
||||||
|
|
||||||
// Re-enable usage output only for flag-parsing errors; runtime errors
|
// 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 {
|
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||||
cmd.SilenceUsage = false
|
cmd.SilenceUsage = false
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,19 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -14,64 +27,64 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "stable version filters alpha tag",
|
name: "stable version filters alpha tag",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v0.24.0-alpha.1",
|
tag: v24Alpha1Tag,
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "When on stable release, alpha tags should be filtered",
|
description: "When on stable release, alpha tags should be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version filters beta tag",
|
name: "stable version filters beta tag",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v0.24.0-beta.2",
|
tag: "v0.24.0-beta.2",
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "When on stable release, beta tags should be filtered",
|
description: "When on stable release, beta tags should be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version filters rc tag",
|
name: "stable version filters rc tag",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v0.24.0-rc.1",
|
tag: v24RCTag,
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "When on stable release, rc tags should be filtered",
|
description: "When on stable release, rc tags should be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version allows stable tag",
|
name: "stable version allows stable tag",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on stable release, stable tags should not be filtered",
|
description: "When on stable release, stable tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpha version allows alpha tag",
|
name: "alpha version allows alpha tag",
|
||||||
currentVersion: "0.23.0-alpha.1",
|
currentVersion: v23Alpha1,
|
||||||
tag: "v0.24.0-alpha.2",
|
tag: "v0.24.0-alpha.2",
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on alpha release, alpha tags should not be filtered",
|
description: "When on alpha release, alpha tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpha version allows beta tag",
|
name: "alpha version allows beta tag",
|
||||||
currentVersion: "0.23.0-alpha.1",
|
currentVersion: v23Alpha1,
|
||||||
tag: "v0.24.0-beta.1",
|
tag: "v0.24.0-beta.1",
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on alpha release, beta tags should not be filtered",
|
description: "When on alpha release, beta tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpha version allows rc tag",
|
name: "alpha version allows rc tag",
|
||||||
currentVersion: "0.23.0-alpha.1",
|
currentVersion: v23Alpha1,
|
||||||
tag: "v0.24.0-rc.1",
|
tag: v24RCTag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on alpha release, rc tags should not be filtered",
|
description: "When on alpha release, rc tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpha version allows stable tag",
|
name: "alpha version allows stable tag",
|
||||||
currentVersion: "0.23.0-alpha.1",
|
currentVersion: v23Alpha1,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on alpha release, stable tags should not be filtered",
|
description: "When on alpha release, stable tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "beta version allows alpha tag",
|
name: "beta version allows alpha tag",
|
||||||
currentVersion: "0.23.0-beta.1",
|
currentVersion: v23Beta1,
|
||||||
tag: "v0.24.0-alpha.1",
|
tag: v24Alpha1Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on beta release, alpha tags should not be filtered",
|
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",
|
name: "beta version allows rc tag",
|
||||||
currentVersion: "0.23.0-beta.1",
|
currentVersion: v23Beta1,
|
||||||
tag: "v0.24.0-rc.1",
|
tag: v24RCTag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on beta release, rc tags should not be filtered",
|
description: "When on beta release, rc tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "beta version allows stable tag",
|
name: "beta version allows stable tag",
|
||||||
currentVersion: "0.23.0-beta.1",
|
currentVersion: v23Beta1,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on beta release, stable tags should not be filtered",
|
description: "When on beta release, stable tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rc version allows alpha tag",
|
name: "rc version allows alpha tag",
|
||||||
currentVersion: "0.23.0-rc.1",
|
currentVersion: v23RC1,
|
||||||
tag: "v0.24.0-alpha.1",
|
tag: v24Alpha1Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on rc release, alpha tags should not be filtered",
|
description: "When on rc release, alpha tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rc version allows beta tag",
|
name: "rc version allows beta tag",
|
||||||
currentVersion: "0.23.0-rc.1",
|
currentVersion: v23RC1,
|
||||||
tag: "v0.24.0-beta.1",
|
tag: "v0.24.0-beta.1",
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on rc release, beta tags should not be filtered",
|
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",
|
name: "rc version allows stable tag",
|
||||||
currentVersion: "0.23.0-rc.1",
|
currentVersion: v23RC1,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on rc release, stable tags should not be filtered",
|
description: "When on rc release, stable tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version with patch filters alpha",
|
name: "stable version with patch filters alpha",
|
||||||
currentVersion: "0.23.1",
|
currentVersion: v231,
|
||||||
tag: "v0.24.0-alpha.1",
|
tag: v24Alpha1Tag,
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "Stable version with patch number should filter alpha tags",
|
description: "Stable version with patch number should filter alpha tags",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version with patch allows stable",
|
name: "stable version with patch allows stable",
|
||||||
currentVersion: "0.23.1",
|
currentVersion: v231,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "Stable version with patch number should allow stable tags",
|
description: "Stable version with patch number should allow stable tags",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tag with alpha substring in version number",
|
name: "tag with alpha substring in version number",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v1.0.0-alpha.1",
|
tag: "v1.0.0-alpha.1",
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "Tags with alpha in version string should be filtered on stable",
|
description: "Tags with alpha in version string should be filtered on stable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tag with beta substring in version number",
|
name: "tag with beta substring in version number",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v1.0.0-beta.1",
|
tag: "v1.0.0-beta.1",
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "Tags with beta in version string should be filtered on stable",
|
description: "Tags with beta in version string should be filtered on stable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tag with rc substring in version number",
|
name: "tag with rc substring in version number",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v1.0.0-rc.1",
|
tag: "v1.0.0-rc.1",
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "Tags with rc in version string should be filtered on stable",
|
description: "Tags with rc in version string should be filtered on stable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty tag on stable version",
|
name: "empty tag on stable version",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "",
|
tag: "",
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "Empty tags should not be filtered",
|
description: "Empty tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dev version allows all tags",
|
name: "dev version allows all tags",
|
||||||
currentVersion: "0.23.0-dev",
|
currentVersion: v23Dev,
|
||||||
tag: "v0.24.0-alpha.1",
|
tag: v24Alpha1Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "Dev versions should not filter any tags (pre-release allows all)",
|
description: "Dev versions should not filter any tags (pre-release allows all)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stable version filters dev tag",
|
name: "stable version filters dev tag",
|
||||||
currentVersion: "0.23.0",
|
currentVersion: v23,
|
||||||
tag: "v0.24.0-dev",
|
tag: "v0.24.0-dev",
|
||||||
expectedFilter: true,
|
expectedFilter: true,
|
||||||
description: "When on stable release, dev tags should be filtered",
|
description: "When on stable release, dev tags should be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dev version allows dev tag",
|
name: "dev version allows dev tag",
|
||||||
currentVersion: "0.23.0-dev",
|
currentVersion: v23Dev,
|
||||||
tag: "v0.24.0-dev.1",
|
tag: "v0.24.0-dev.1",
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on dev release, dev tags should not be filtered",
|
description: "When on dev release, dev tags should not be filtered",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dev version allows stable tag",
|
name: "dev version allows stable tag",
|
||||||
currentVersion: "0.23.0-dev",
|
currentVersion: v23Dev,
|
||||||
tag: "v0.24.0",
|
tag: v24Tag,
|
||||||
expectedFilter: false,
|
expectedFilter: false,
|
||||||
description: "When on dev release, stable tags should not be filtered",
|
description: "When on dev release, stable tags should not be filtered",
|
||||||
},
|
},
|
||||||
@ -222,25 +235,25 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "stable version",
|
name: "stable version",
|
||||||
version: "0.23.0",
|
version: v23,
|
||||||
expected: false,
|
expected: false,
|
||||||
description: "Stable version should not be pre-release",
|
description: "Stable version should not be pre-release",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpha version",
|
name: "alpha version",
|
||||||
version: "0.23.0-alpha.1",
|
version: v23Alpha1,
|
||||||
expected: true,
|
expected: true,
|
||||||
description: "Alpha version should be pre-release",
|
description: "Alpha version should be pre-release",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "beta version",
|
name: "beta version",
|
||||||
version: "0.23.0-beta.1",
|
version: v23Beta1,
|
||||||
expected: true,
|
expected: true,
|
||||||
description: "Beta version should be pre-release",
|
description: "Beta version should be pre-release",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rc version",
|
name: "rc version",
|
||||||
version: "0.23.0-rc.1",
|
version: v23RC1,
|
||||||
expected: true,
|
expected: true,
|
||||||
description: "RC version should be pre-release",
|
description: "RC version should be pre-release",
|
||||||
},
|
},
|
||||||
@ -258,7 +271,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dev version",
|
name: "dev version",
|
||||||
version: "0.23.0-dev",
|
version: v23Dev,
|
||||||
expected: true,
|
expected: true,
|
||||||
description: "Dev version should be pre-release",
|
description: "Dev version should be pre-release",
|
||||||
},
|
},
|
||||||
@ -270,7 +283,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "version with patch number",
|
name: "version with patch number",
|
||||||
version: "0.23.1",
|
version: v231,
|
||||||
expected: false,
|
expected: false,
|
||||||
description: "Stable version with patch should not be pre-release",
|
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{
|
var createUserCmd = &cobra.Command{
|
||||||
Use: "create NAME",
|
Use: "create NAME",
|
||||||
Short: "Creates a new user",
|
Short: "Creates a new user",
|
||||||
Aliases: []string{"c", "new"},
|
Aliases: []string{"c", cmdNew},
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return errMissingParameter
|
return errMissingParameter
|
||||||
@ -115,7 +115,7 @@ var createUserCmd = &cobra.Command{
|
|||||||
var destroyUserCmd = &cobra.Command{
|
var destroyUserCmd = &cobra.Command{
|
||||||
Use: "destroy --identifier ID or --name NAME",
|
Use: "destroy --identifier ID or --name NAME",
|
||||||
Short: "Destroys a user",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
id, username, err := usernameAndIDFromFlag(cmd)
|
id, username, err := usernameAndIDFromFlag(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -142,7 +142,7 @@ var destroyUserCmd = &cobra.Command{
|
|||||||
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
||||||
user.GetName(), user.GetId(),
|
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()}
|
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
|
||||||
@ -157,9 +157,9 @@ var destroyUserCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listUsersCmd = &cobra.Command{
|
var listUsersCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: cmdList,
|
||||||
Short: "List all the users",
|
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 {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
request := &v1.ListUsersRequest{}
|
request := &v1.ListUsersRequest{}
|
||||||
|
|
||||||
@ -183,7 +183,9 @@ var listUsersCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
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() {
|
for _, user := range response.GetUsers() {
|
||||||
tableData = append(
|
tableData = append(
|
||||||
tableData,
|
tableData,
|
||||||
|
|||||||
@ -67,9 +67,9 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
|||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// grpcRunE wraps a cobra RunE func, injecting a ready gRPC client and
|
// grpcRunE wraps a cobra [cobra.Command.RunE] func, injecting a ready
|
||||||
// context. Connection lifecycle is managed by the wrapper — callers
|
// gRPC client and context. Connection lifecycle is managed by the
|
||||||
// never see the underlying conn or cancel func.
|
// wrapper — callers never see the underlying conn or cancel func.
|
||||||
func grpcRunE(
|
func grpcRunE(
|
||||||
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
|
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
|
||||||
) func(*cobra.Command, []string) error {
|
) func(*cobra.Command, []string) error {
|
||||||
@ -103,7 +103,7 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
|||||||
|
|
||||||
address := cfg.CLI.Address
|
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 == "" {
|
if address == "" {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("socket", cfg.UnixSocket).
|
Str("socket", cfg.UnixSocket).
|
||||||
@ -112,9 +112,9 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
|||||||
address = cfg.UnixSocket
|
address = cfg.UnixSocket
|
||||||
|
|
||||||
// Try to give the user better feedback if we cannot write to the headscale
|
// 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.
|
// 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
|
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsPermission(err) {
|
if os.IsPermission(err) {
|
||||||
@ -269,7 +269,7 @@ func printListOutput(
|
|||||||
|
|
||||||
// printError writes err to stderr, formatting it as JSON/YAML when the
|
// printError writes err to stderr, formatting it as JSON/YAML when the
|
||||||
// --output flag requests machine-readable output. Used exclusively by
|
// --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) {
|
func printError(err error, outputFormat string) {
|
||||||
type errOutput struct {
|
type errOutput struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
|||||||
@ -58,6 +58,14 @@ const (
|
|||||||
space3XL = "4rem" //nolint:unused // 64px - 3x extra large spacing
|
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
|
// Typography System
|
||||||
// EXTRACTED FROM: https://headscale.net/stable/assets/stylesheets/main.342714a4.min.css
|
// EXTRACTED FROM: https://headscale.net/stable/assets/stylesheets/main.342714a4.min.css
|
||||||
// Material for MkDocs typography - exact values from .md-typeset 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{
|
return elem.Div(attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Background: "var(--hs-bg)",
|
styles.Background: "var(--hs-bg)",
|
||||||
styles.Border: "1px solid var(--hs-border)",
|
styles.Border: cssBorderHS,
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.Padding: "clamp(1rem, 3vw, 1.5rem)",
|
styles.Padding: "clamp(1rem, 3vw, 1.5rem)",
|
||||||
styles.MarginBottom: spaceL,
|
styles.MarginBottom: spaceL,
|
||||||
styles.BoxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
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.FontSize: fontSizeCode, // 0.85em
|
||||||
styles.LineHeight: lineHeightCode, // 1.4
|
styles.LineHeight: lineHeightCode, // 1.4
|
||||||
styles.OverflowX: "auto", // Horizontal scroll
|
styles.OverflowX: "auto", // Horizontal scroll
|
||||||
"overflow-wrap": "break-word", // Word wrapping
|
cssOverflowWrap: cssBreakWord, // Word wrapping
|
||||||
"word-wrap": "break-word", // Legacy support
|
"word-wrap": cssBreakWord, // Legacy support
|
||||||
styles.WhiteSpace: "pre-wrap", // Preserve whitespace
|
styles.WhiteSpace: "pre-wrap", // Preserve whitespace
|
||||||
styles.MarginTop: spaceM, // 1em
|
styles.MarginTop: spaceM, // 1em
|
||||||
styles.MarginBottom: spaceM, // 1em
|
styles.MarginBottom: spaceM, // 1em
|
||||||
@ -171,7 +179,7 @@ func baseTypesetStyles() styles.Props {
|
|||||||
styles.LineHeight: lineHeightBase, // 1.6
|
styles.LineHeight: lineHeightBase, // 1.6
|
||||||
styles.Color: colorTextPrimary,
|
styles.Color: colorTextPrimary,
|
||||||
styles.FontFamily: fontFamilySystem,
|
styles.FontFamily: fontFamilySystem,
|
||||||
"overflow-wrap": "break-word",
|
cssOverflowWrap: cssBreakWord,
|
||||||
styles.TextAlign: "left",
|
styles.TextAlign: "left",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +198,7 @@ func h1Styles() styles.Props {
|
|||||||
styles.FontWeight: "300",
|
styles.FontWeight: "300",
|
||||||
"letter-spacing": "-0.01em",
|
"letter-spacing": "-0.01em",
|
||||||
styles.FontFamily: fontFamilySystem, // Roboto
|
styles.FontFamily: fontFamilySystem, // Roboto
|
||||||
"overflow-wrap": "break-word",
|
cssOverflowWrap: cssBreakWord,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +216,7 @@ func h2Styles() styles.Props {
|
|||||||
"letter-spacing": "-0.01em",
|
"letter-spacing": "-0.01em",
|
||||||
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
||||||
styles.FontFamily: fontFamilySystem, // Roboto
|
styles.FontFamily: fontFamilySystem, // Roboto
|
||||||
"overflow-wrap": "break-word",
|
cssOverflowWrap: cssBreakWord,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +234,7 @@ func h3Styles() styles.Props {
|
|||||||
"letter-spacing": "-0.01em",
|
"letter-spacing": "-0.01em",
|
||||||
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
||||||
styles.FontFamily: fontFamilySystem, // Roboto
|
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.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||||
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
||||||
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87)
|
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.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||||
styles.LineHeight: lineHeightBase, // 1.6 - 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
|
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.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
||||||
styles.LineHeight: lineHeightBase, // 1.6 - 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
|
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{
|
return styles.Props{
|
||||||
styles.Color: colorPrimaryAccent, // #4051b5 - var(--md-primary-fg-color)
|
styles.Color: colorPrimaryAccent, // #4051b5 - var(--md-primary-fg-color)
|
||||||
styles.TextDecoration: "none",
|
styles.TextDecoration: "none",
|
||||||
"word-break": "break-word",
|
"word-break": cssBreakWord,
|
||||||
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,7 +318,7 @@ func inlineCodeStyles() styles.Props {
|
|||||||
styles.FontSize: fontSizeCode, // 0.85em
|
styles.FontSize: fontSizeCode, // 0.85em
|
||||||
styles.FontFamily: fontFamilyCode, // Roboto Mono
|
styles.FontFamily: fontFamilyCode, // Roboto Mono
|
||||||
styles.Padding: "0 0.2941176471em",
|
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{
|
return elem.Div(attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Display: "flex",
|
styles.Display: "flex",
|
||||||
styles.AlignItems: "center",
|
styles.AlignItems: cssCenter,
|
||||||
styles.Gap: spaceM,
|
styles.Gap: spaceM,
|
||||||
styles.MarginTop: space2XL,
|
styles.MarginTop: space2XL,
|
||||||
styles.MarginBottom: space2XL,
|
styles.MarginBottom: space2XL,
|
||||||
@ -369,12 +377,12 @@ func successBox(heading string, children ...elem.Node) *elem.Element {
|
|||||||
attrs.Props{
|
attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Display: "flex",
|
styles.Display: "flex",
|
||||||
styles.AlignItems: "center",
|
styles.AlignItems: cssCenter,
|
||||||
styles.Gap: spaceM,
|
styles.Gap: spaceM,
|
||||||
styles.Padding: spaceL,
|
styles.Padding: spaceL,
|
||||||
styles.BackgroundColor: "var(--hs-success-bg)",
|
styles.BackgroundColor: "var(--hs-success-bg)",
|
||||||
styles.Border: "1px solid var(--hs-success)",
|
styles.Border: "1px solid var(--hs-success)",
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.MarginBottom: spaceXL,
|
styles.MarginBottom: spaceXL,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
attrs.Role: "status",
|
attrs.Role: "status",
|
||||||
@ -414,12 +422,12 @@ func errorBox(heading string, children ...elem.Node) *elem.Element {
|
|||||||
attrs.Props{
|
attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Display: "flex",
|
styles.Display: "flex",
|
||||||
styles.AlignItems: "center",
|
styles.AlignItems: cssCenter,
|
||||||
styles.Gap: spaceM,
|
styles.Gap: spaceM,
|
||||||
styles.Padding: spaceL,
|
styles.Padding: spaceL,
|
||||||
styles.BackgroundColor: "var(--hs-error-bg)",
|
styles.BackgroundColor: "var(--hs-error-bg)",
|
||||||
styles.Border: "1px solid var(--hs-error)",
|
styles.Border: "1px solid var(--hs-error)",
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.MarginBottom: spaceXL,
|
styles.MarginBottom: spaceXL,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
attrs.Role: "alert",
|
attrs.Role: "alert",
|
||||||
@ -462,7 +470,7 @@ func warningBox(title, message string) *elem.Element {
|
|||||||
styles.Padding: spaceL,
|
styles.Padding: spaceL,
|
||||||
styles.BackgroundColor: "var(--hs-warning-bg)",
|
styles.BackgroundColor: "var(--hs-warning-bg)",
|
||||||
styles.Border: "1px solid var(--hs-warning-border)",
|
styles.Border: "1px solid var(--hs-warning-border)",
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.MarginTop: spaceL,
|
styles.MarginTop: spaceL,
|
||||||
styles.MarginBottom: spaceL,
|
styles.MarginBottom: spaceL,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
@ -492,7 +500,7 @@ func downloadButton(href, text string) *elem.Element {
|
|||||||
attrs.Download: "headscale_macos.mobileconfig",
|
attrs.Download: "headscale_macos.mobileconfig",
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Display: "inline-flex",
|
styles.Display: "inline-flex",
|
||||||
styles.AlignItems: "center",
|
styles.AlignItems: cssCenter,
|
||||||
styles.Padding: "0.75rem 1.5rem",
|
styles.Padding: "0.75rem 1.5rem",
|
||||||
styles.BackgroundColor: "var(--md-primary-fg-color)",
|
styles.BackgroundColor: "var(--md-primary-fg-color)",
|
||||||
styles.Color: "#ffffff",
|
styles.Color: "#ffffff",
|
||||||
@ -542,8 +550,8 @@ func detailsBox(summary string, children ...elem.Node) *elem.Element {
|
|||||||
return elem.Details(attrs.Props{
|
return elem.Details(attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Background: "var(--hs-bg)",
|
styles.Background: "var(--hs-bg)",
|
||||||
styles.Border: "1px solid var(--hs-border)",
|
styles.Border: cssBorderHS,
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.Padding: spaceS + " " + spaceM,
|
styles.Padding: spaceS + " " + spaceM,
|
||||||
styles.MarginTop: spaceL,
|
styles.MarginTop: spaceL,
|
||||||
styles.MarginBottom: spaceL,
|
styles.MarginBottom: spaceL,
|
||||||
@ -584,7 +592,7 @@ func statusMessage(message string, isSuccess bool) *elem.Element {
|
|||||||
styles.Padding: spaceM,
|
styles.Padding: spaceM,
|
||||||
styles.BackgroundColor: bgColor,
|
styles.BackgroundColor: bgColor,
|
||||||
styles.Color: textColor,
|
styles.Color: textColor,
|
||||||
styles.BorderRadius: "0.5rem",
|
styles.BorderRadius: spaceS,
|
||||||
styles.Border: "1px solid " + textColor,
|
styles.Border: "1px solid " + textColor,
|
||||||
styles.MarginBottom: spaceL,
|
styles.MarginBottom: spaceL,
|
||||||
styles.FontSize: fontSizeBase,
|
styles.FontSize: fontSizeBase,
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type PingResult struct {
|
|||||||
// Status is "ok", "timeout", or "error".
|
// Status is "ok", "timeout", or "error".
|
||||||
Status string
|
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
|
Latency time.Duration
|
||||||
|
|
||||||
// NodeID is the ID of the pinged node.
|
// NodeID is the ID of the pinged node.
|
||||||
@ -27,7 +27,7 @@ type PingResult struct {
|
|||||||
Message string
|
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.
|
// displayed as a quick-ping link on the debug ping page.
|
||||||
type ConnectedNode struct {
|
type ConnectedNode struct {
|
||||||
ID types.NodeID
|
ID types.NodeID
|
||||||
@ -36,7 +36,7 @@ type ConnectedNode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PingPage renders the /debug/ping page with a form, optional result,
|
// 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 {
|
func PingPage(query string, result *PingResult, nodes []ConnectedNode) *elem.Element {
|
||||||
children := []elem.Node{
|
children := []elem.Node{
|
||||||
headscaleLogo(),
|
headscaleLogo(),
|
||||||
@ -94,7 +94,7 @@ func pingForm(query string) *elem.Element {
|
|||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Display: "flex",
|
styles.Display: "flex",
|
||||||
styles.Gap: spaceS,
|
styles.Gap: spaceS,
|
||||||
styles.AlignItems: "center",
|
styles.AlignItems: cssCenter,
|
||||||
styles.FlexWrap: "wrap",
|
styles.FlexWrap: "wrap",
|
||||||
styles.MarginTop: spaceM,
|
styles.MarginTop: spaceM,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
@ -107,7 +107,7 @@ func pingForm(query string) *elem.Element {
|
|||||||
attrs.Autofocus: "true",
|
attrs.Autofocus: "true",
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Padding: "0.75rem " + spaceM,
|
styles.Padding: "0.75rem " + spaceM,
|
||||||
styles.Border: "1px solid var(--hs-border)",
|
styles.Border: cssBorderHS,
|
||||||
styles.BorderRadius: "0.375rem",
|
styles.BorderRadius: "0.375rem",
|
||||||
styles.Width: "280px",
|
styles.Width: "280px",
|
||||||
styles.MaxWidth: "100%",
|
styles.MaxWidth: "100%",
|
||||||
|
|||||||
@ -31,11 +31,11 @@ type RegisterConfirmInfo struct {
|
|||||||
User string
|
User string
|
||||||
|
|
||||||
// Hostname is the hostname the registering tailscaled instance
|
// Hostname is the hostname the registering tailscaled instance
|
||||||
// reported in its RegisterRequest.
|
// reported in its [tailcfg.RegisterRequest].
|
||||||
Hostname string
|
Hostname string
|
||||||
|
|
||||||
// OS is the operating system the registering tailscaled reported.
|
// 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
|
OS string
|
||||||
|
|
||||||
// MachineKey is the short fingerprint of the registering machine
|
// 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.FontWeight: "600",
|
||||||
styles.WhiteSpace: "nowrap",
|
styles.WhiteSpace: "nowrap",
|
||||||
styles.Color: "var(--md-default-fg-color--light)",
|
styles.Color: "var(--md-default-fg-color--light)",
|
||||||
styles.BorderBottom: "1px solid var(--hs-border)",
|
styles.BorderBottom: cssBorderHS,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
}, elem.Text(row[0])),
|
}, elem.Text(row[0])),
|
||||||
elem.Td(attrs.Props{
|
elem.Td(attrs.Props{
|
||||||
attrs.Style: styles.Props{
|
attrs.Style: styles.Props{
|
||||||
styles.Padding: "0.5rem 0",
|
styles.Padding: "0.5rem 0",
|
||||||
styles.BorderBottom: "1px solid var(--hs-border)",
|
styles.BorderBottom: cssBorderHS,
|
||||||
}.ToInline(),
|
}.ToInline(),
|
||||||
}, val),
|
}, val),
|
||||||
))
|
))
|
||||||
|
|||||||
@ -32,14 +32,14 @@ func DefaultConfigEnv() map[string]string {
|
|||||||
|
|
||||||
// Embedded DERP is the default for test isolation.
|
// Embedded DERP is the default for test isolation.
|
||||||
// Tests should not depend on external DERP infrastructure.
|
// 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.
|
// need public DERP relays.
|
||||||
"HEADSCALE_DERP_URLS": "",
|
"HEADSCALE_DERP_URLS": "",
|
||||||
"HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "false",
|
"HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "false",
|
||||||
"HEADSCALE_DERP_UPDATE_FREQUENCY": "1m",
|
"HEADSCALE_DERP_UPDATE_FREQUENCY": "1m",
|
||||||
"HEADSCALE_DERP_SERVER_ENABLED": "true",
|
"HEADSCALE_DERP_SERVER_ENABLED": "true",
|
||||||
"HEADSCALE_DERP_SERVER_REGION_ID": "999",
|
"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_REGION_NAME": "Headscale Embedded DERP",
|
||||||
"HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478",
|
"HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR": "0.0.0.0:3478",
|
||||||
"HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key",
|
"HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH": "/tmp/derp.key",
|
||||||
|
|||||||
@ -48,6 +48,9 @@ const (
|
|||||||
headscaleDefaultPort = 8080
|
headscaleDefaultPort = 8080
|
||||||
IntegrationTestDockerFileName = "Dockerfile.integration"
|
IntegrationTestDockerFileName = "Dockerfile.integration"
|
||||||
defaultDirPerm = 0o755
|
defaultDirPerm = 0o755
|
||||||
|
binHeadscale = "headscale"
|
||||||
|
flagOutput = "--output"
|
||||||
|
acceptJSON = "Accept: application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -94,8 +97,8 @@ type HeadscaleInContainer struct {
|
|||||||
// Headscale instance.
|
// Headscale instance.
|
||||||
type Option = func(c *HeadscaleInContainer)
|
type Option = func(c *HeadscaleInContainer)
|
||||||
|
|
||||||
// WithACLPolicy adds a hscontrol.ACLPolicy policy to the
|
// WithACLPolicy adds a [policyv2.Policy] to the
|
||||||
// HeadscaleInContainer instance.
|
// [HeadscaleInContainer] instance.
|
||||||
func WithACLPolicy(acl *policyv2.Policy) Option {
|
func WithACLPolicy(acl *policyv2.Policy) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
if acl == nil {
|
if acl == nil {
|
||||||
@ -127,7 +130,7 @@ func WithoutTLS() Option {
|
|||||||
|
|
||||||
// WithCustomTLS uses the given certificates for the Headscale instance.
|
// WithCustomTLS uses the given certificates for the Headscale instance.
|
||||||
// The caCert is installed into the container's trust store and returned
|
// 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 {
|
func WithCustomTLS(caCert, cert, key []byte) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.tlsCACert = caCert
|
hsic.tlsCACert = caCert
|
||||||
@ -320,7 +323,7 @@ func (hsic *HeadscaleInContainer) buildEntrypoint() []string {
|
|||||||
return []string{"/bin/bash", "-c", strings.Join(commands, " ; ")}
|
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
|
//nolint:gocyclo // complex container setup with many options
|
||||||
func New(
|
func New(
|
||||||
@ -364,8 +367,8 @@ func New(
|
|||||||
|
|
||||||
// TLS is enabled by default for all integration tests.
|
// TLS is enabled by default for all integration tests.
|
||||||
// Generate a self-signed certificate if TLS was not explicitly
|
// Generate a self-signed certificate if TLS was not explicitly
|
||||||
// disabled via WithoutTLS() and no custom cert was provided
|
// disabled via [WithoutTLS] and no custom cert was provided
|
||||||
// via WithCustomTLS().
|
// via [WithCustomTLS].
|
||||||
if !hsic.noTLS && len(hsic.tlsCert) == 0 {
|
if !hsic.noTLS && len(hsic.tlsCert) == 0 {
|
||||||
caCert, cert, key, err := integrationutil.CreateCertificate(hsic.hostname)
|
caCert, cert, key, err := integrationutil.CreateCertificate(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -394,9 +397,9 @@ func New(
|
|||||||
if hsic.postgres {
|
if hsic.postgres {
|
||||||
hsic.env["HEADSCALE_DATABASE_TYPE"] = "postgres"
|
hsic.env["HEADSCALE_DATABASE_TYPE"] = "postgres"
|
||||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_HOST"] = "postgres-" + hash
|
hsic.env["HEADSCALE_DATABASE_POSTGRES_HOST"] = "postgres-" + hash
|
||||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_USER"] = "headscale"
|
hsic.env["HEADSCALE_DATABASE_POSTGRES_USER"] = binHeadscale
|
||||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_PASS"] = "headscale"
|
hsic.env["HEADSCALE_DATABASE_POSTGRES_PASS"] = binHeadscale
|
||||||
hsic.env["HEADSCALE_DATABASE_POSTGRES_NAME"] = "headscale"
|
hsic.env["HEADSCALE_DATABASE_POSTGRES_NAME"] = binHeadscale
|
||||||
delete(hsic.env, "HEADSCALE_DATABASE_SQLITE_PATH")
|
delete(hsic.env, "HEADSCALE_DATABASE_SQLITE_PATH")
|
||||||
|
|
||||||
// Determine postgres image - use prebuilt if available, otherwise pull from registry
|
// 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
|
// Add integration test labels if running under hi tool
|
||||||
dockertestutil.DockerAddIntegrationLabels(runOptions, "headscale")
|
dockertestutil.DockerAddIntegrationLabels(runOptions, binHeadscale)
|
||||||
|
|
||||||
var container *dockertest.Resource
|
var container *dockertest.Resource
|
||||||
|
|
||||||
@ -509,7 +512,7 @@ func New(
|
|||||||
prebuiltImage := os.Getenv("HEADSCALE_INTEGRATION_HEADSCALE_IMAGE")
|
prebuiltImage := os.Getenv("HEADSCALE_INTEGRATION_HEADSCALE_IMAGE")
|
||||||
|
|
||||||
if prebuiltImage != "" {
|
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
|
// Parse image into repository and tag
|
||||||
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -709,7 +712,7 @@ func (t *HeadscaleInContainer) Shutdown() (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteLogs writes the current stdout/stderr log of the container to
|
// 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 {
|
func (t *HeadscaleInContainer) WriteLogs(stdout, stderr io.Writer) error {
|
||||||
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
||||||
}
|
}
|
||||||
@ -1010,13 +1013,13 @@ func (t *HeadscaleInContainer) GetHostMetricsPort() string {
|
|||||||
return t.hostMetricsPort
|
return t.hostMetricsPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
|
// GetHealthEndpoint returns a health endpoint for the [HeadscaleInContainer]
|
||||||
// instance.
|
// instance.
|
||||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
||||||
return t.GetEndpoint() + "/health"
|
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 {
|
func (t *HeadscaleInContainer) GetEndpoint() string {
|
||||||
return t.getEndpoint(false)
|
return t.getEndpoint(false)
|
||||||
}
|
}
|
||||||
@ -1051,12 +1054,12 @@ func (t *HeadscaleInContainer) GetCert() []byte {
|
|||||||
return t.tlsCACert
|
return t.tlsCACert
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostname returns the hostname of the HeadscaleInContainer.
|
// GetHostname returns the hostname of the [HeadscaleInContainer].
|
||||||
func (t *HeadscaleInContainer) GetHostname() string {
|
func (t *HeadscaleInContainer) GetHostname() string {
|
||||||
return t.hostname
|
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 {
|
func (t *HeadscaleInContainer) GetIPInNetwork(network *dockertest.Network) string {
|
||||||
return t.container.GetIPInNetwork(network)
|
return t.container.GetIPInNetwork(network)
|
||||||
}
|
}
|
||||||
@ -1095,12 +1098,12 @@ func (t *HeadscaleInContainer) CreateUser(
|
|||||||
user string,
|
user string,
|
||||||
) (*v1.User, error) {
|
) (*v1.User, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
"users",
|
"users",
|
||||||
"create",
|
"create",
|
||||||
user,
|
user,
|
||||||
fmt.Sprintf("--email=%s@test.no", user),
|
fmt.Sprintf("--email=%s@test.no", user),
|
||||||
"--output",
|
flagOutput,
|
||||||
"json",
|
"json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1140,7 +1143,7 @@ type AuthKeyOptions struct {
|
|||||||
// This supports both user-owned and tags-only auth keys.
|
// This supports both user-owned and tags-only auth keys.
|
||||||
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
|
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add --user flag if User is specified
|
// Only add --user flag if User is specified
|
||||||
@ -1153,7 +1156,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
|
|||||||
"create",
|
"create",
|
||||||
"--expiration",
|
"--expiration",
|
||||||
"24h",
|
"24h",
|
||||||
"--output",
|
flagOutput,
|
||||||
"json",
|
"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
|
// 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(
|
func (t *HeadscaleInContainer) CreateAuthKey(
|
||||||
user uint64,
|
user uint64,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
@ -1223,12 +1226,12 @@ func (t *HeadscaleInContainer) DeleteAuthKey(
|
|||||||
id uint64,
|
id uint64,
|
||||||
) error {
|
) error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
"preauthkeys",
|
"preauthkeys",
|
||||||
"delete",
|
"delete",
|
||||||
"--id",
|
"--id",
|
||||||
strconv.FormatUint(id, 10),
|
strconv.FormatUint(id, 10),
|
||||||
"--output",
|
flagOutput,
|
||||||
"json",
|
"json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1275,13 +1278,13 @@ func (t *HeadscaleInContainer) ListNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
err := execUnmarshal([]string{"headscale", "nodes", "list", "--output", "json"})
|
err := execUnmarshal([]string{binHeadscale, "nodes", "list", flagOutput, "json"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, user := range users {
|
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)
|
err := execUnmarshal(command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1299,12 +1302,12 @@ func (t *HeadscaleInContainer) ListNodes(
|
|||||||
|
|
||||||
func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
|
func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
"nodes",
|
"nodes",
|
||||||
"delete",
|
"delete",
|
||||||
"--identifier",
|
"--identifier",
|
||||||
strconv.FormatUint(nodeID, 10),
|
strconv.FormatUint(nodeID, 10),
|
||||||
"--output",
|
flagOutput,
|
||||||
"json",
|
"json",
|
||||||
"--force",
|
"--force",
|
||||||
}
|
}
|
||||||
@ -1355,7 +1358,7 @@ func (t *HeadscaleInContainer) NodesByName() (map[string]*v1.Node, error) {
|
|||||||
|
|
||||||
// ListUsers returns a list of users from Headscale.
|
// ListUsers returns a list of users from Headscale.
|
||||||
func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
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(
|
result, _, err := dockertestutil.ExecuteCommand(
|
||||||
t.container,
|
t.container,
|
||||||
@ -1395,13 +1398,13 @@ func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
|
|||||||
// DeleteUser deletes a user from the Headscale instance.
|
// DeleteUser deletes a user from the Headscale instance.
|
||||||
func (t *HeadscaleInContainer) DeleteUser(userID uint64) error {
|
func (t *HeadscaleInContainer) DeleteUser(userID uint64) error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
"users",
|
"users",
|
||||||
"delete",
|
"delete",
|
||||||
"--identifier",
|
"--identifier",
|
||||||
strconv.FormatUint(userID, 10),
|
strconv.FormatUint(userID, 10),
|
||||||
"--force",
|
"--force",
|
||||||
"--output",
|
flagOutput,
|
||||||
"json",
|
"json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1444,7 +1447,7 @@ func (h *HeadscaleInContainer) SetPolicy(pol *policyv2.Policy) error {
|
|||||||
func (h *HeadscaleInContainer) reloadDatabasePolicy() error {
|
func (h *HeadscaleInContainer) reloadDatabasePolicy() error {
|
||||||
_, err := h.Execute(
|
_, err := h.Execute(
|
||||||
[]string{
|
[]string{
|
||||||
"headscale",
|
binHeadscale,
|
||||||
"policy",
|
"policy",
|
||||||
"set",
|
"set",
|
||||||
"-f",
|
"-f",
|
||||||
@ -1476,7 +1479,7 @@ func (h *HeadscaleInContainer) PID() (int, error) {
|
|||||||
// Use pidof to find the headscale process, which is more reliable than grep
|
// 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
|
// as it only looks for the actual binary name, not processes that contain
|
||||||
// "headscale" in their command line (like the dlv debugger).
|
// "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 {
|
if err != nil {
|
||||||
// pidof returns exit code 1 when no process is found
|
// pidof returns exit code 1 when no process is found
|
||||||
return 0, os.ErrNotExist
|
return 0, os.ErrNotExist
|
||||||
@ -1533,8 +1536,8 @@ func (h *HeadscaleInContainer) Reload() error {
|
|||||||
// ApproveRoutes approves routes for a node.
|
// ApproveRoutes approves routes for a node.
|
||||||
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
|
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale", "nodes", "approve-routes",
|
binHeadscale, "nodes", "approve-routes",
|
||||||
"--output", "json",
|
flagOutput, "json",
|
||||||
"--identifier", strconv.FormatUint(id, 10),
|
"--identifier", strconv.FormatUint(id, 10),
|
||||||
"--routes=" + strings.Join(util.PrefixesToString(routes), ","),
|
"--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>.
|
// 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 {
|
func (t *HeadscaleInContainer) SetNodeTags(nodeID uint64, tags []string) error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"headscale", "nodes", "tag",
|
binHeadscale, "nodes", "tag",
|
||||||
"--identifier", strconv.FormatUint(nodeID, 10),
|
"--identifier", strconv.FormatUint(nodeID, 10),
|
||||||
"--output", "json",
|
flagOutput, "json",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tags - the CLI expects -t flag for each tag or comma-separated
|
// 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 {
|
func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||||
pid, err := t.Execute([]string{"pidof", "headscale"})
|
pid, err := t.Execute([]string{"pidof", binHeadscale})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1621,7 +1624,7 @@ func (t *HeadscaleInContainer) SendInterrupt() error {
|
|||||||
func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error) {
|
func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error) {
|
||||||
// Execute curl inside the container to access the debug endpoint locally
|
// Execute curl inside the container to access the debug endpoint locally
|
||||||
command := []string{
|
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)
|
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) {
|
func (t *HeadscaleInContainer) PrimaryRoutes() (*types.DebugRoutes, error) {
|
||||||
// Execute curl inside the container to access the debug endpoint locally
|
// Execute curl inside the container to access the debug endpoint locally
|
||||||
command := []string{
|
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)
|
result, err := t.Execute(command)
|
||||||
@ -1661,7 +1664,7 @@ func (t *HeadscaleInContainer) PrimaryRoutes() (*types.DebugRoutes, error) {
|
|||||||
func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, error) {
|
func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, error) {
|
||||||
// Execute curl inside the container to access the debug endpoint locally
|
// Execute curl inside the container to access the debug endpoint locally
|
||||||
command := []string{
|
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)
|
result, err := t.Execute(command)
|
||||||
@ -1677,11 +1680,11 @@ func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, erro
|
|||||||
return &debugInfo, nil
|
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) {
|
func (t *HeadscaleInContainer) DebugNodeStore() (map[types.NodeID]types.Node, error) {
|
||||||
// Execute curl inside the container to access the debug endpoint locally
|
// Execute curl inside the container to access the debug endpoint locally
|
||||||
command := []string{
|
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)
|
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) {
|
func (t *HeadscaleInContainer) DebugFilter() ([]tailcfg.FilterRule, error) {
|
||||||
// Execute curl inside the container to access the debug endpoint locally
|
// Execute curl inside the container to access the debug endpoint locally
|
||||||
command := []string{
|
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)
|
result, err := t.Execute(command)
|
||||||
|
|||||||
@ -43,6 +43,7 @@ const (
|
|||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
caCertRoot = "/usr/local/share/ca-certificates"
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
dockerExecuteTimeout = 60 * time.Second
|
dockerExecuteTimeout = 60 * time.Second
|
||||||
|
tailscaleBin = "tailscale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultPingTimeoutVal returns the per-attempt timeout for tailscale ping.
|
// 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.
|
// the Tailscale instance.
|
||||||
func WithNetwork(network *dockertest.Network) Option {
|
func WithNetwork(network *dockertest.Network) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
@ -215,7 +216,7 @@ func WithBuildTag(tag string) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithExtraLoginArgs adds additional arguments to the `tailscale up` command
|
// 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 {
|
func WithExtraLoginArgs(args []string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.extraLoginArgs = append(tsic.extraLoginArgs, args...)
|
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")
|
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
|
// 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 {
|
if len(t.caCerts) > 0 {
|
||||||
commands = append(commands,
|
commands = append(commands,
|
||||||
fmt.Sprintf("while [ ! -f %s/user-0.crt ]; do sleep 0.1; done", caCertRoot))
|
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
|
// Add integration test labels if running under hi tool
|
||||||
dockertestutil.DockerAddIntegrationLabels(tailscaleOptions, "tailscale")
|
dockertestutil.DockerAddIntegrationLabels(tailscaleOptions, tailscaleBin)
|
||||||
|
|
||||||
var container *dockertest.Resource
|
var container *dockertest.Resource
|
||||||
|
|
||||||
@ -413,13 +414,13 @@ func New(
|
|||||||
// the pre-built image as it won't have the necessary code compiled in.
|
// the pre-built image as it won't have the necessary code compiled in.
|
||||||
hasBuildTags := len(tsic.buildConfig.tags) > 0
|
hasBuildTags := len(tsic.buildConfig.tags) > 0
|
||||||
if hasBuildTags && prebuiltImage != "" {
|
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, tsic.buildConfig.tags)
|
||||||
prebuiltImage = ""
|
prebuiltImage = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if 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
|
// Parse image into repository and tag
|
||||||
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
repo, tag, ok := strings.Cut(prebuiltImage, ":")
|
||||||
@ -609,7 +610,7 @@ func (t *TailscaleInContainer) Version() string {
|
|||||||
return t.version
|
return t.version
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerID returns the Docker container ID of the TailscaleInContainer
|
// ContainerID returns the Docker container ID of the [TailscaleInContainer]
|
||||||
// instance.
|
// instance.
|
||||||
func (t *TailscaleInContainer) ContainerID() string {
|
func (t *TailscaleInContainer) ContainerID() string {
|
||||||
return t.container.Container.ID
|
return t.container.Container.ID
|
||||||
@ -657,7 +658,7 @@ func (t *TailscaleInContainer) buildLoginCommand(
|
|||||||
loginServer, authKey string,
|
loginServer, authKey string,
|
||||||
) []string {
|
) []string {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"up",
|
"up",
|
||||||
"--login-server=" + loginServer,
|
"--login-server=" + loginServer,
|
||||||
"--hostname=" + t.hostname,
|
"--hostname=" + t.hostname,
|
||||||
@ -736,12 +737,12 @@ func (t *TailscaleInContainer) LoginWithURL(
|
|||||||
|
|
||||||
// Logout runs the logout routine on the given Tailscale instance.
|
// Logout runs the logout routine on the given Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Logout() error {
|
func (t *TailscaleInContainer) Logout() error {
|
||||||
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
_, _, err := t.Execute([]string{tailscaleBin, "logout"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, _ := t.Execute([]string{"tailscale", "status"})
|
stdout, stderr, _ := t.Execute([]string{tailscaleBin, "status"})
|
||||||
if !strings.Contains(stdout+stderr, "Logged out.") {
|
if !strings.Contains(stdout+stderr, "Logged out.") {
|
||||||
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr) //nolint:err113
|
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
|
// We use exponential backoff to poll until we can successfully execute a command
|
||||||
_, err = backoff.Retry(context.Background(), func() (struct{}, error) {
|
_, err = backoff.Retry(context.Background(), func() (struct{}, error) {
|
||||||
// Try to execute a simple command to verify the container is responsive
|
// 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 {
|
if err != nil {
|
||||||
return struct{}{}, fmt.Errorf("container not ready: %w", err)
|
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.
|
// Up runs `tailscale up` with no arguments.
|
||||||
func (t *TailscaleInContainer) Up() error {
|
func (t *TailscaleInContainer) Up() error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"up",
|
"up",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -804,7 +805,7 @@ func (t *TailscaleInContainer) Up() error {
|
|||||||
// Down runs `tailscale down` with no arguments.
|
// Down runs `tailscale down` with no arguments.
|
||||||
func (t *TailscaleInContainer) Down() error {
|
func (t *TailscaleInContainer) Down() error {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"down",
|
"down",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -835,7 +836,7 @@ func (t *TailscaleInContainer) ReconnectToNetwork(network *dockertest.Network) e
|
|||||||
return dockertestutil.ReconnectContainerToNetwork(t.pool, network, t.hostname)
|
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) {
|
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||||
if len(t.ips) != 0 {
|
if len(t.ips) != 0 {
|
||||||
return t.ips, nil
|
return t.ips, nil
|
||||||
@ -844,7 +845,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
|||||||
// Retry with exponential backoff to handle eventual consistency
|
// Retry with exponential backoff to handle eventual consistency
|
||||||
ips, err := backoff.Retry(context.Background(), func() ([]netip.Addr, error) {
|
ips, err := backoff.Retry(context.Background(), func() ([]netip.Addr, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"ip",
|
"ip",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,10 +927,10 @@ func (t *TailscaleInContainer) MustIPv6() netip.Addr {
|
|||||||
panic("no ipv6 found")
|
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) {
|
func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"status",
|
"status",
|
||||||
"--json",
|
"--json",
|
||||||
}
|
}
|
||||||
@ -954,7 +955,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
|
|||||||
return &status, err
|
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 {
|
func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -979,7 +980,7 @@ func (t *TailscaleInContainer) MustID() types.NodeID {
|
|||||||
return types.NodeID(id)
|
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.
|
// Only works with Tailscale 1.56 and newer.
|
||||||
// Panics if version is lower then minimum.
|
// Panics if version is lower then minimum.
|
||||||
func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
||||||
@ -988,7 +989,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"debug",
|
"debug",
|
||||||
"netmap",
|
"netmap",
|
||||||
}
|
}
|
||||||
@ -1014,7 +1015,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
|||||||
return &nm, err
|
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`
|
// 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
|
// as there seem to be some weirdness omitting endpoint and DERP info if we use
|
||||||
// Patch updates.
|
// Patch updates.
|
||||||
@ -1037,8 +1038,8 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
|||||||
// return notify.NetMap, nil
|
// return notify.NetMap, nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until
|
// watchIPN watches `tailscale debug watch-ipn` for a [ipn.Notify] object until
|
||||||
// it gets one that has a netmap.NetworkMap.
|
// it gets one that has a [netmap.NetworkMap].
|
||||||
//
|
//
|
||||||
//nolint:unused
|
//nolint:unused
|
||||||
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
|
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
|
||||||
@ -1115,7 +1116,7 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
|
|||||||
}
|
}
|
||||||
|
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"debug",
|
"debug",
|
||||||
"derp",
|
"derp",
|
||||||
region,
|
region,
|
||||||
@ -1138,10 +1139,10 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
|
|||||||
return &report, err
|
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) {
|
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
tailscaleBin,
|
||||||
"netcheck",
|
"netcheck",
|
||||||
"--format=json",
|
"--format=json",
|
||||||
}
|
}
|
||||||
@ -1258,7 +1259,7 @@ func (t *TailscaleInContainer) waitForBackendState(state string, timeout time.Du
|
|||||||
continue // Keep retrying on status errors
|
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
|
// https://github.com/tailscale/tailscale/pull/3865
|
||||||
//
|
//
|
||||||
// Before that, we can check the BackendState to see if the
|
// 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
|
// 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.
|
// TODO(kradalby): Make multiping, go routine magic.
|
||||||
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
||||||
args := pingArgs{
|
args := pingArgs{
|
||||||
@ -1397,7 +1398,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
|
|
||||||
command := make([]string, 0, 6)
|
command := make([]string, 0, 6)
|
||||||
command = append(command,
|
command = append(command,
|
||||||
"tailscale", "ping",
|
tailscaleBin, "ping",
|
||||||
fmt.Sprintf("--timeout=%s", args.timeout),
|
fmt.Sprintf("--timeout=%s", args.timeout),
|
||||||
fmt.Sprintf("--c=%d", args.count),
|
fmt.Sprintf("--c=%d", args.count),
|
||||||
"--until-direct="+strconv.FormatBool(args.direct),
|
"--until-direct="+strconv.FormatBool(args.direct),
|
||||||
@ -1494,7 +1495,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Curl executes the Tailscale curl command and curls a hostname
|
// 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) {
|
func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, error) {
|
||||||
args := curlArgs{
|
args := curlArgs{
|
||||||
connectionTimeout: defaultConnectionTimeout,
|
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
|
// curl exit 0 with an empty body usually means a mid-stream reset
|
||||||
// after headers (HTTP 200 with the connection torn down before the
|
// after headers (HTTP 200 with the connection torn down before the
|
||||||
// body arrived). Without this signal, callers wrapping the call in
|
// 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.
|
// no error to drive a retry.
|
||||||
if result == "" {
|
if result == "" {
|
||||||
return result, fmt.Errorf("%w: %s from %s", errCurlEmptyResponseBody, url, t.Hostname())
|
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) {
|
func (t *TailscaleInContainer) Traceroute(ip netip.Addr) (util.Traceroute, error) {
|
||||||
// -w 1: wait at most 1s for each probe response. busybox's default
|
// -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.
|
// 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
|
// -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
|
// 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
|
// 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 {
|
func (t *TailscaleInContainer) WriteLogs(stdout, stderr io.Writer) error {
|
||||||
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
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
|
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 {
|
func (t *TailscaleInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||||
return t.container.ConnectToNetwork(network)
|
return t.container.ConnectToNetwork(network)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user