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