mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-29 15:15:00 -04:00 
			
		
		
		
	Only load needed part of configuration (#2109)
This commit is contained in:
		
							parent
							
								
									f368ed01ed
								
							
						
					
					
						commit
						8a3a0fee3c
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -22,6 +22,7 @@ dist/ | ||||
| /headscale | ||||
| config.json | ||||
| config.yaml | ||||
| config*.yaml | ||||
| derp.yaml | ||||
| *.hujson | ||||
| *.key | ||||
|  | ||||
| @ -72,6 +72,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ | ||||
| - Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) | ||||
| - Fix for registering nodes using preauthkeys when running on a postgres database in a non-UTC timezone. [#764](https://github.com/juanfont/headscale/issues/764) | ||||
| - Make sure integration tests cover postgres for all scenarios | ||||
| - CLI commands (all except `serve`) only requires minimal configuration, no more errors or warnings from unset settings [#2109](https://github.com/juanfont/headscale/pull/2109) | ||||
| - CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109) | ||||
| 
 | ||||
| ## 0.22.3 (2023-05-12) | ||||
| 
 | ||||
|  | ||||
| @ -54,7 +54,7 @@ var listAPIKeys = &cobra.Command{ | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -67,14 +67,10 @@ var listAPIKeys = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting the list of keys: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response.GetApiKeys(), "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		tableData := pterm.TableData{ | ||||
| @ -102,8 +98,6 @@ var listAPIKeys = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to render pterm table: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -119,9 +113,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 
 | ||||
| 		log.Trace(). | ||||
| 			Msg("Preparing to create ApiKey") | ||||
| 
 | ||||
| 		request := &v1.CreateApiKeyRequest{} | ||||
| 
 | ||||
| 		durationStr, _ := cmd.Flags().GetString("expiration") | ||||
| @ -133,19 +124,13 @@ If you loose a key, create a new one and revoke (expire) the old one.`, | ||||
| 				fmt.Sprintf("Could not parse duration: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		expiration := time.Now().UTC().Add(time.Duration(duration)) | ||||
| 
 | ||||
| 		log.Trace(). | ||||
| 			Dur("expiration", time.Duration(duration)). | ||||
| 			Msg("expiration has been set") | ||||
| 
 | ||||
| 		request.Expiration = timestamppb.New(expiration) | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -156,8 +141,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, | ||||
| 				fmt.Sprintf("Cannot create Api Key: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response.GetApiKey(), response.GetApiKey(), output) | ||||
| @ -178,11 +161,9 @@ var expireAPIKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting prefix from CLI flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -197,8 +178,6 @@ var expireAPIKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot expire Api Key: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response, "Key expired", output) | ||||
| @ -219,11 +198,9 @@ var deleteAPIKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting prefix from CLI flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -238,8 +215,6 @@ var deleteAPIKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot delete Api Key: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response, "Key deleted", output) | ||||
|  | ||||
| @ -14,7 +14,7 @@ var configTestCmd = &cobra.Command{ | ||||
| 	Short: "Test the configuration.", | ||||
| 	Long:  "Run a test of the configuration and exit.", | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		_, err := getHeadscaleApp() | ||||
| 		_, err := newHeadscaleServerWithConfig() | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Caller().Err(err).Msg("Error initializing") | ||||
| 		} | ||||
|  | ||||
| @ -64,11 +64,9 @@ var createNodeCmd = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -79,8 +77,6 @@ var createNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting node from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		machineKey, err := cmd.Flags().GetString("key") | ||||
| @ -90,8 +86,6 @@ var createNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting key from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		var mkey key.MachinePublic | ||||
| @ -102,8 +96,6 @@ var createNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to parse machine key from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		routes, err := cmd.Flags().GetStringSlice("route") | ||||
| @ -113,8 +105,6 @@ var createNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting routes from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		request := &v1.DebugCreateNodeRequest{ | ||||
| @ -131,8 +121,6 @@ var createNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot create node: %s", status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response.GetNode(), "Node created", output) | ||||
|  | ||||
| @ -116,11 +116,9 @@ var registerNodeCmd = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -131,8 +129,6 @@ var registerNodeCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting node key from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		request := &v1.RegisterNodeRequest{ | ||||
| @ -150,8 +146,6 @@ var registerNodeCmd = &cobra.Command{ | ||||
| 				), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput( | ||||
| @ -169,17 +163,13 @@ var listNodesCmd = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 		showTags, err := cmd.Flags().GetBool("tags") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -194,21 +184,15 @@ var listNodesCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response.GetNodes(), "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		tableData, err := nodesToPtables(user, showTags, response.GetNodes()) | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() | ||||
| @ -218,8 +202,6 @@ var listNodesCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to render pterm table: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -243,7 +225,7 @@ var expireNodeCmd = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -286,7 +268,7 @@ var renameNodeCmd = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -335,7 +317,7 @@ var deleteNodeCmd = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -435,7 +417,7 @@ var moveNodeCmd = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -508,7 +490,7 @@ be assigned to nodes.`, | ||||
| 			return | ||||
| 		} | ||||
| 		if confirm { | ||||
| 			ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 			ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 			defer cancel() | ||||
| 			defer conn.Close() | ||||
| 
 | ||||
| @ -681,7 +663,7 @@ var tagCmd = &cobra.Command{ | ||||
| 	Aliases: []string{"tags", "t"}, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package cli | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 
 | ||||
| @ -30,7 +31,8 @@ var getPolicy = &cobra.Command{ | ||||
| 	Short:   "Print the current ACL Policy", | ||||
| 	Aliases: []string{"show", "view", "fetch"}, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -38,13 +40,13 @@ var getPolicy = &cobra.Command{ | ||||
| 
 | ||||
| 		response, err := client.GetPolicy(ctx, request) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Err(err).Msg("Failed to get the policy") | ||||
| 
 | ||||
| 			return | ||||
| 			ErrorOutput(err, fmt.Sprintf("Failed loading ACL Policy: %s", err), output) | ||||
| 		} | ||||
| 
 | ||||
| 		// TODO(pallabpain): Maybe print this better? | ||||
| 		SuccessOutput("", response.GetPolicy(), "hujson") | ||||
| 		// This does not pass output as we dont support yaml, json or json-line | ||||
| 		// output for this command. It is HuJSON already. | ||||
| 		SuccessOutput("", response.GetPolicy(), "") | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| @ -56,33 +58,28 @@ var setPolicy = &cobra.Command{ | ||||
| 	This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`, | ||||
| 	Aliases: []string{"put", "update"}, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 		policyPath, _ := cmd.Flags().GetString("file") | ||||
| 
 | ||||
| 		f, err := os.Open(policyPath) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Err(err).Msg("Error opening the policy file") | ||||
| 
 | ||||
| 			return | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error opening the policy file: %s", err), output) | ||||
| 		} | ||||
| 		defer f.Close() | ||||
| 
 | ||||
| 		policyBytes, err := io.ReadAll(f) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Err(err).Msg("Error reading the policy file") | ||||
| 
 | ||||
| 			return | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error reading the policy file: %s", err), output) | ||||
| 		} | ||||
| 
 | ||||
| 		request := &v1.SetPolicyRequest{Policy: string(policyBytes)} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| 		if _, err := client.SetPolicy(ctx, request); err != nil { | ||||
| 			log.Fatal().Err(err).Msg("Failed to set ACL Policy") | ||||
| 
 | ||||
| 			return | ||||
| 			ErrorOutput(err, fmt.Sprintf("Failed to set ACL Policy: %s", err), output) | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(nil, "Policy updated.", "") | ||||
|  | ||||
| @ -60,11 +60,9 @@ var listPreAuthKeys = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -85,8 +83,6 @@ var listPreAuthKeys = &cobra.Command{ | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response.GetPreAuthKeys(), "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		tableData := pterm.TableData{ | ||||
| @ -134,8 +130,6 @@ var listPreAuthKeys = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to render pterm table: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -150,20 +144,12 @@ var createPreAuthKeyCmd = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		reusable, _ := cmd.Flags().GetBool("reusable") | ||||
| 		ephemeral, _ := cmd.Flags().GetBool("ephemeral") | ||||
| 		tags, _ := cmd.Flags().GetStringSlice("tags") | ||||
| 
 | ||||
| 		log.Trace(). | ||||
| 			Bool("reusable", reusable). | ||||
| 			Bool("ephemeral", ephemeral). | ||||
| 			Str("user", user). | ||||
| 			Msg("Preparing to create preauthkey") | ||||
| 
 | ||||
| 		request := &v1.CreatePreAuthKeyRequest{ | ||||
| 			User:      user, | ||||
| 			Reusable:  reusable, | ||||
| @ -180,8 +166,6 @@ var createPreAuthKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Could not parse duration: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		expiration := time.Now().UTC().Add(time.Duration(duration)) | ||||
| @ -192,7 +176,7 @@ var createPreAuthKeyCmd = &cobra.Command{ | ||||
| 
 | ||||
| 		request.Expiration = timestamppb.New(expiration) | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -203,8 +187,6 @@ var createPreAuthKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output) | ||||
| @ -227,11 +209,9 @@ var expirePreAuthKeyCmd = &cobra.Command{ | ||||
| 		user, err := cmd.Flags().GetString("user") | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -247,8 +227,6 @@ var expirePreAuthKeyCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response, "Key expired", output) | ||||
|  | ||||
| @ -9,6 +9,7 @@ import ( | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"github.com/tcnksm/go-latest" | ||||
| ) | ||||
| 
 | ||||
| @ -49,11 +50,6 @@ func initConfig() { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	cfg, err := types.GetHeadscaleConfig() | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg("Failed to read headscale configuration") | ||||
| 	} | ||||
| 
 | ||||
| 	machineOutput := HasMachineOutputFlag() | ||||
| 
 | ||||
| 	// If the user has requested a "node" readable format, | ||||
| @ -62,11 +58,13 @@ func initConfig() { | ||||
| 		zerolog.SetGlobalLevel(zerolog.Disabled) | ||||
| 	} | ||||
| 
 | ||||
| 	if cfg.Log.Format == types.JSONLogFormat { | ||||
| 		log.Logger = log.Output(os.Stdout) | ||||
| 	} | ||||
| 	// logFormat := viper.GetString("log.format") | ||||
| 	// if logFormat == types.JSONLogFormat { | ||||
| 	// 	log.Logger = log.Output(os.Stdout) | ||||
| 	// } | ||||
| 
 | ||||
| 	if !cfg.DisableUpdateCheck && !machineOutput { | ||||
| 	disableUpdateCheck := viper.GetBool("disable_check_updates") | ||||
| 	if !disableUpdateCheck && !machineOutput { | ||||
| 		if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && | ||||
| 			Version != "dev" { | ||||
| 			githubTag := &latest.GithubTag{ | ||||
|  | ||||
| @ -64,11 +64,9 @@ var listRoutesCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting machine id from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -82,14 +80,10 @@ var listRoutesCmd = &cobra.Command{ | ||||
| 					fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), | ||||
| 					output, | ||||
| 				) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			if output != "" { | ||||
| 				SuccessOutput(response.GetRoutes(), "", output) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			routes = response.GetRoutes() | ||||
| @ -103,14 +97,10 @@ var listRoutesCmd = &cobra.Command{ | ||||
| 					fmt.Sprintf("Cannot get routes for node %d: %s", machineID, status.Convert(err).Message()), | ||||
| 					output, | ||||
| 				) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			if output != "" { | ||||
| 				SuccessOutput(response.GetRoutes(), "", output) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			routes = response.GetRoutes() | ||||
| @ -119,8 +109,6 @@ var listRoutesCmd = &cobra.Command{ | ||||
| 		tableData := routesToPtables(routes) | ||||
| 		if err != nil { | ||||
| 			ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() | ||||
| @ -130,8 +118,6 @@ var listRoutesCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to render pterm table: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -150,11 +136,9 @@ var enableRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting machine id from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -167,14 +151,10 @@ var enableRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response, "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -193,11 +173,9 @@ var disableRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting machine id from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -210,14 +188,10 @@ var disableRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot disable route %d: %s", routeID, status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response, "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -236,11 +210,9 @@ var deleteRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error getting machine id from flag: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -253,14 +225,10 @@ var deleteRouteCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot delete route %d: %s", routeID, status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response, "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| @ -16,7 +16,7 @@ var serveCmd = &cobra.Command{ | ||||
| 		return nil | ||||
| 	}, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		app, err := getHeadscaleApp() | ||||
| 		app, err := newHeadscaleServerWithConfig() | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Caller().Err(err).Msg("Error initializing") | ||||
| 		} | ||||
| @ -44,7 +44,7 @@ var createUserCmd = &cobra.Command{ | ||||
| 
 | ||||
| 		userName := args[0] | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -63,8 +63,6 @@ var createUserCmd = &cobra.Command{ | ||||
| 				), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response.GetUser(), "User created", output) | ||||
| @ -91,7 +89,7 @@ var destroyUserCmd = &cobra.Command{ | ||||
| 			Name: userName, | ||||
| 		} | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -102,8 +100,6 @@ var destroyUserCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Error: %s", status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		confirm := false | ||||
| @ -134,8 +130,6 @@ var destroyUserCmd = &cobra.Command{ | ||||
| 					), | ||||
| 					output, | ||||
| 				) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 			SuccessOutput(response, "User destroyed", output) | ||||
| 		} else { | ||||
| @ -151,7 +145,7 @@ var listUsersCmd = &cobra.Command{ | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -164,14 +158,10 @@ var listUsersCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Cannot get users: %s", status.Convert(err).Message()), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if output != "" { | ||||
| 			SuccessOutput(response.GetUsers(), "", output) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		tableData := pterm.TableData{{"ID", "Name", "Created"}} | ||||
| @ -192,8 +182,6 @@ var listUsersCmd = &cobra.Command{ | ||||
| 				fmt.Sprintf("Failed to render pterm table: %s", err), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -213,7 +201,7 @@ var renameUserCmd = &cobra.Command{ | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		output, _ := cmd.Flags().GetString("output") | ||||
| 
 | ||||
| 		ctx, client, conn, cancel := getHeadscaleCLIClient() | ||||
| 		ctx, client, conn, cancel := newHeadscaleCLIWithConfig() | ||||
| 		defer cancel() | ||||
| 		defer conn.Close() | ||||
| 
 | ||||
| @ -232,8 +220,6 @@ var renameUserCmd = &cobra.Command{ | ||||
| 				), | ||||
| 				output, | ||||
| 			) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		SuccessOutput(response.GetUser(), "User renamed", output) | ||||
|  | ||||
| @ -23,8 +23,8 @@ const ( | ||||
| 	SocketWritePermissions  = 0o666 | ||||
| ) | ||||
| 
 | ||||
| func getHeadscaleApp() (*hscontrol.Headscale, error) { | ||||
| 	cfg, err := types.GetHeadscaleConfig() | ||||
| func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { | ||||
| 	cfg, err := types.LoadServerConfig() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf( | ||||
| 			"failed to load configuration while creating headscale instance: %w", | ||||
| @ -40,8 +40,8 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) { | ||||
| 	return app, nil | ||||
| } | ||||
| 
 | ||||
| func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { | ||||
| 	cfg, err := types.GetHeadscaleConfig() | ||||
| func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { | ||||
| 	cfg, err := types.LoadCLIConfig() | ||||
| 	if err != nil { | ||||
| 		log.Fatal(). | ||||
| 			Err(err). | ||||
| @ -130,7 +130,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. | ||||
| 	return ctx, client, conn, cancel | ||||
| } | ||||
| 
 | ||||
| func SuccessOutput(result interface{}, override string, outputFormat string) { | ||||
| func output(result interface{}, override string, outputFormat string) string { | ||||
| 	var jsonBytes []byte | ||||
| 	var err error | ||||
| 	switch outputFormat { | ||||
| @ -151,21 +151,26 @@ func SuccessOutput(result interface{}, override string, outputFormat string) { | ||||
| 		} | ||||
| 	default: | ||||
| 		// nolint | ||||
| 		fmt.Println(override) | ||||
| 
 | ||||
| 		return | ||||
| 		return override | ||||
| 	} | ||||
| 
 | ||||
| 	// nolint | ||||
| 	fmt.Println(string(jsonBytes)) | ||||
| 	return string(jsonBytes) | ||||
| } | ||||
| 
 | ||||
| // SuccessOutput prints the result to stdout and exits with status code 0. | ||||
| func SuccessOutput(result interface{}, override string, outputFormat string) { | ||||
| 	fmt.Println(output(result, override, outputFormat)) | ||||
| 	os.Exit(0) | ||||
| } | ||||
| 
 | ||||
| // ErrorOutput prints an error message to stderr and exits with status code 1. | ||||
| func ErrorOutput(errResult error, override string, outputFormat string) { | ||||
| 	type errOutput struct { | ||||
| 		Error string `json:"error"` | ||||
| 	} | ||||
| 
 | ||||
| 	SuccessOutput(errOutput{errResult.Error()}, override, outputFormat) | ||||
| 	fmt.Fprintf(os.Stderr, "%s\n", output(errOutput{errResult.Error()}, override, outputFormat)) | ||||
| 	os.Exit(1) | ||||
| } | ||||
| 
 | ||||
| func HasMachineOutputFlag() bool { | ||||
|  | ||||
| @ -4,7 +4,6 @@ import ( | ||||
| 	"io/fs" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/juanfont/headscale/hscontrol/types" | ||||
| @ -113,60 +112,3 @@ func (*Suite) TestConfigLoading(c *check.C) { | ||||
| 	c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false) | ||||
| 	c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) | ||||
| } | ||||
| 
 | ||||
| func writeConfig(c *check.C, tmpDir string, configYaml []byte) { | ||||
| 	// Populate a custom config file | ||||
| 	configFile := filepath.Join(tmpDir, "config.yaml") | ||||
| 	err := os.WriteFile(configFile, configYaml, 0o600) | ||||
| 	if err != nil { | ||||
| 		c.Fatalf("Couldn't write file %s", configFile) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (*Suite) TestTLSConfigValidation(c *check.C) { | ||||
| 	tmpDir, err := os.MkdirTemp("", "headscale") | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 	// defer os.RemoveAll(tmpDir) | ||||
| 	configYaml := []byte(`--- | ||||
| tls_letsencrypt_hostname: example.com | ||||
| tls_letsencrypt_challenge_type: "" | ||||
| tls_cert_path: abc.pem | ||||
| noise: | ||||
|   private_key_path: noise_private.key`) | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 
 | ||||
| 	// Check configuration validation errors (1) | ||||
| 	err = types.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	// check.Matches can not handle multiline strings | ||||
| 	tmp := strings.ReplaceAll(err.Error(), "\n", "***") | ||||
| 	c.Assert( | ||||
| 		tmp, | ||||
| 		check.Matches, | ||||
| 		".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*", | ||||
| 	) | ||||
| 	c.Assert( | ||||
| 		tmp, | ||||
| 		check.Matches, | ||||
| 		".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", | ||||
| 	) | ||||
| 	c.Assert( | ||||
| 		tmp, | ||||
| 		check.Matches, | ||||
| 		".*Fatal config error: server_url must start with https:// or http://.*", | ||||
| 	) | ||||
| 
 | ||||
| 	// Check configuration validation errors (2) | ||||
| 	configYaml = []byte(`--- | ||||
| noise: | ||||
|   private_key_path: noise_private.key | ||||
| server_url: http://127.0.0.1:8080 | ||||
| tls_letsencrypt_hostname: example.com | ||||
| tls_letsencrypt_challenge_type: TLS-ALPN-01 | ||||
| `) | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 	err = types.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| } | ||||
|  | ||||
| @ -684,7 +684,7 @@ func (api headscaleV1APIServer) GetPolicy( | ||||
| 	case types.PolicyModeDB: | ||||
| 		p, err := api.h.db.GetPolicy() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, fmt.Errorf("loading ACL from database: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		return &v1.GetPolicyResponse{ | ||||
| @ -696,20 +696,20 @@ func (api headscaleV1APIServer) GetPolicy( | ||||
| 		absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path) | ||||
| 		f, err := os.Open(absPath) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, fmt.Errorf("reading policy from path %q: %w", absPath, err) | ||||
| 		} | ||||
| 
 | ||||
| 		defer f.Close() | ||||
| 
 | ||||
| 		b, err := io.ReadAll(f) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, fmt.Errorf("reading policy from file: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		return &v1.GetPolicyResponse{Policy: string(b)}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, nil | ||||
| 	return nil, fmt.Errorf("no supported policy mode found in configuration, policy.mode: %q", api.h.cfg.Policy.Mode) | ||||
| } | ||||
| 
 | ||||
| func (api headscaleV1APIServer) SetPolicy( | ||||
|  | ||||
| @ -212,6 +212,12 @@ type Tuning struct { | ||||
| 	NodeMapSessionBufferedChanSize int | ||||
| } | ||||
| 
 | ||||
| // LoadConfig prepares and loads the Headscale configuration into Viper. | ||||
| // This means it sets the default values, reads the configuration file and | ||||
| // environment variables, and handles deprecated configuration options. | ||||
| // It has to be called before LoadServerConfig and LoadCLIConfig. | ||||
| // The configuration is not validated and the caller should check for errors | ||||
| // using a validation function. | ||||
| func LoadConfig(path string, isFile bool) error { | ||||
| 	if isFile { | ||||
| 		viper.SetConfigFile(path) | ||||
| @ -284,14 +290,14 @@ func LoadConfig(path string, isFile bool) error { | ||||
| 
 | ||||
| 	viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential)) | ||||
| 
 | ||||
| 	if IsCLIConfigured() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if err := viper.ReadInConfig(); err != nil { | ||||
| 		return fmt.Errorf("fatal error reading config file: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func validateServerConfig() error { | ||||
| 	depr := deprecator{ | ||||
| 		warns:  make(set.Set[string]), | ||||
| 		fatals: make(set.Set[string]), | ||||
| @ -360,12 +366,12 @@ func LoadConfig(path string, isFile bool) error { | ||||
| 	if errorText != "" { | ||||
| 		// nolint | ||||
| 		return errors.New(strings.TrimSuffix(errorText, "\n")) | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func GetTLSConfig() TLSConfig { | ||||
| func tlsConfig() TLSConfig { | ||||
| 	return TLSConfig{ | ||||
| 		LetsEncrypt: LetsEncryptConfig{ | ||||
| 			Hostname: viper.GetString("tls_letsencrypt_hostname"), | ||||
| @ -384,7 +390,7 @@ func GetTLSConfig() TLSConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetDERPConfig() DERPConfig { | ||||
| func derpConfig() DERPConfig { | ||||
| 	serverEnabled := viper.GetBool("derp.server.enabled") | ||||
| 	serverRegionID := viper.GetInt("derp.server.region_id") | ||||
| 	serverRegionCode := viper.GetString("derp.server.region_code") | ||||
| @ -445,7 +451,7 @@ func GetDERPConfig() DERPConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetLogTailConfig() LogTailConfig { | ||||
| func logtailConfig() LogTailConfig { | ||||
| 	enabled := viper.GetBool("logtail.enabled") | ||||
| 
 | ||||
| 	return LogTailConfig{ | ||||
| @ -453,7 +459,7 @@ func GetLogTailConfig() LogTailConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetPolicyConfig() PolicyConfig { | ||||
| func policyConfig() PolicyConfig { | ||||
| 	policyPath := viper.GetString("policy.path") | ||||
| 	policyMode := viper.GetString("policy.mode") | ||||
| 
 | ||||
| @ -463,7 +469,7 @@ func GetPolicyConfig() PolicyConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetLogConfig() LogConfig { | ||||
| func logConfig() LogConfig { | ||||
| 	logLevelStr := viper.GetString("log.level") | ||||
| 	logLevel, err := zerolog.ParseLevel(logLevelStr) | ||||
| 	if err != nil { | ||||
| @ -473,9 +479,9 @@ func GetLogConfig() LogConfig { | ||||
| 	logFormatOpt := viper.GetString("log.format") | ||||
| 	var logFormat string | ||||
| 	switch logFormatOpt { | ||||
| 	case "json": | ||||
| 	case JSONLogFormat: | ||||
| 		logFormat = JSONLogFormat | ||||
| 	case "text": | ||||
| 	case TextLogFormat: | ||||
| 		logFormat = TextLogFormat | ||||
| 	case "": | ||||
| 		logFormat = TextLogFormat | ||||
| @ -491,7 +497,7 @@ func GetLogConfig() LogConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetDatabaseConfig() DatabaseConfig { | ||||
| func databaseConfig() DatabaseConfig { | ||||
| 	debug := viper.GetBool("database.debug") | ||||
| 
 | ||||
| 	type_ := viper.GetString("database.type") | ||||
| @ -543,7 +549,7 @@ func GetDatabaseConfig() DatabaseConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func DNS() (DNSConfig, error) { | ||||
| func dns() (DNSConfig, error) { | ||||
| 	var dns DNSConfig | ||||
| 
 | ||||
| 	// TODO: Use this instead of manually getting settings when | ||||
| @ -575,12 +581,12 @@ func DNS() (DNSConfig, error) { | ||||
| 	return dns, nil | ||||
| } | ||||
| 
 | ||||
| // GlobalResolvers returns the global DNS resolvers | ||||
| // globalResolvers returns the global DNS resolvers | ||||
| // defined in the config file. | ||||
| // If a nameserver is a valid IP, it will be used as a regular resolver. | ||||
| // If a nameserver is a valid URL, it will be used as a DoH resolver. | ||||
| // If a nameserver is neither a valid URL nor a valid IP, it will be ignored. | ||||
| func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { | ||||
| func (d *DNSConfig) globalResolvers() []*dnstype.Resolver { | ||||
| 	var resolvers []*dnstype.Resolver | ||||
| 
 | ||||
| 	for _, nsStr := range d.Nameservers.Global { | ||||
| @ -613,11 +619,11 @@ func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { | ||||
| 	return resolvers | ||||
| } | ||||
| 
 | ||||
| // SplitResolvers returns a map of domain to DNS resolvers. | ||||
| // splitResolvers returns a map of domain to DNS resolvers. | ||||
| // If a nameserver is a valid IP, it will be used as a regular resolver. | ||||
| // If a nameserver is a valid URL, it will be used as a DoH resolver. | ||||
| // If a nameserver is neither a valid URL nor a valid IP, it will be ignored. | ||||
| func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { | ||||
| func (d *DNSConfig) splitResolvers() map[string][]*dnstype.Resolver { | ||||
| 	routes := make(map[string][]*dnstype.Resolver) | ||||
| 	for domain, nameservers := range d.Nameservers.Split { | ||||
| 		var resolvers []*dnstype.Resolver | ||||
| @ -653,7 +659,7 @@ func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { | ||||
| 	return routes | ||||
| } | ||||
| 
 | ||||
| func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | ||||
| func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | ||||
| 	cfg := tailcfg.DNSConfig{} | ||||
| 
 | ||||
| 	if dns.BaseDomain == "" && dns.MagicDNS { | ||||
| @ -662,9 +668,9 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | ||||
| 
 | ||||
| 	cfg.Proxied = dns.MagicDNS | ||||
| 	cfg.ExtraRecords = dns.ExtraRecords | ||||
| 	cfg.Resolvers = dns.GlobalResolvers() | ||||
| 	cfg.Resolvers = dns.globalResolvers() | ||||
| 
 | ||||
| 	routes := dns.SplitResolvers() | ||||
| 	routes := dns.splitResolvers() | ||||
| 	cfg.Routes = routes | ||||
| 	if dns.BaseDomain != "" { | ||||
| 		cfg.Domains = []string{dns.BaseDomain} | ||||
| @ -674,7 +680,7 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | ||||
| 	return &cfg | ||||
| } | ||||
| 
 | ||||
| func PrefixV4() (*netip.Prefix, error) { | ||||
| func prefixV4() (*netip.Prefix, error) { | ||||
| 	prefixV4Str := viper.GetString("prefixes.v4") | ||||
| 
 | ||||
| 	if prefixV4Str == "" { | ||||
| @ -698,7 +704,7 @@ func PrefixV4() (*netip.Prefix, error) { | ||||
| 	return &prefixV4, nil | ||||
| } | ||||
| 
 | ||||
| func PrefixV6() (*netip.Prefix, error) { | ||||
| func prefixV6() (*netip.Prefix, error) { | ||||
| 	prefixV6Str := viper.GetString("prefixes.v6") | ||||
| 
 | ||||
| 	if prefixV6Str == "" { | ||||
| @ -723,27 +729,37 @@ func PrefixV6() (*netip.Prefix, error) { | ||||
| 	return &prefixV6, nil | ||||
| } | ||||
| 
 | ||||
| func GetHeadscaleConfig() (*Config, error) { | ||||
| 	if IsCLIConfigured() { | ||||
| 		return &Config{ | ||||
| 			CLI: CLIConfig{ | ||||
| 				Address:  viper.GetString("cli.address"), | ||||
| 				APIKey:   viper.GetString("cli.api_key"), | ||||
| 				Timeout:  viper.GetDuration("cli.timeout"), | ||||
| 				Insecure: viper.GetBool("cli.insecure"), | ||||
| 			}, | ||||
| 		}, nil | ||||
| // LoadCLIConfig returns the needed configuration for the CLI client | ||||
| // of Headscale to connect to a Headscale server. | ||||
| func LoadCLIConfig() (*Config, error) { | ||||
| 	return &Config{ | ||||
| 		DisableUpdateCheck: viper.GetBool("disable_check_updates"), | ||||
| 		UnixSocket:         viper.GetString("unix_socket"), | ||||
| 		CLI: CLIConfig{ | ||||
| 			Address:  viper.GetString("cli.address"), | ||||
| 			APIKey:   viper.GetString("cli.api_key"), | ||||
| 			Timeout:  viper.GetDuration("cli.timeout"), | ||||
| 			Insecure: viper.GetBool("cli.insecure"), | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // LoadServerConfig returns the full Headscale configuration to | ||||
| // host a Headscale server. This is called as part of `headscale serve`. | ||||
| func LoadServerConfig() (*Config, error) { | ||||
| 	if err := validateServerConfig(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	logConfig := GetLogConfig() | ||||
| 	logConfig := logConfig() | ||||
| 	zerolog.SetGlobalLevel(logConfig.Level) | ||||
| 
 | ||||
| 	prefix4, err := PrefixV4() | ||||
| 	prefix4, err := prefixV4() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	prefix6, err := PrefixV6() | ||||
| 	prefix6, err := prefixV6() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -763,13 +779,13 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) | ||||
| 	} | ||||
| 
 | ||||
| 	dnsConfig, err := DNS() | ||||
| 	dnsConfig, err := dns() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	derpConfig := GetDERPConfig() | ||||
| 	logTailConfig := GetLogTailConfig() | ||||
| 	derpConfig := derpConfig() | ||||
| 	logTailConfig := logtailConfig() | ||||
| 	randomizeClientPort := viper.GetBool("randomize_client_port") | ||||
| 
 | ||||
| 	oidcClientSecret := viper.GetString("oidc.client_secret") | ||||
| @ -806,7 +822,7 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		MetricsAddr:        viper.GetString("metrics_listen_addr"), | ||||
| 		GRPCAddr:           viper.GetString("grpc_listen_addr"), | ||||
| 		GRPCAllowInsecure:  viper.GetBool("grpc_allow_insecure"), | ||||
| 		DisableUpdateCheck: viper.GetBool("disable_check_updates"), | ||||
| 		DisableUpdateCheck: false, | ||||
| 
 | ||||
| 		PrefixV4:     prefix4, | ||||
| 		PrefixV6:     prefix6, | ||||
| @ -823,11 +839,11 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 			"ephemeral_node_inactivity_timeout", | ||||
| 		), | ||||
| 
 | ||||
| 		Database: GetDatabaseConfig(), | ||||
| 		Database: databaseConfig(), | ||||
| 
 | ||||
| 		TLS: GetTLSConfig(), | ||||
| 		TLS: tlsConfig(), | ||||
| 
 | ||||
| 		DNSConfig:             DNSToTailcfgDNS(dnsConfig), | ||||
| 		DNSConfig:             dnsToTailcfgDNS(dnsConfig), | ||||
| 		DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, | ||||
| 
 | ||||
| 		ACMEEmail: viper.GetString("acme_email"), | ||||
| @ -870,7 +886,7 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		LogTail:             logTailConfig, | ||||
| 		RandomizeClientPort: randomizeClientPort, | ||||
| 
 | ||||
| 		Policy: GetPolicyConfig(), | ||||
| 		Policy: policyConfig(), | ||||
| 
 | ||||
| 		CLI: CLIConfig{ | ||||
| 			Address:  viper.GetString("cli.address"), | ||||
| @ -890,10 +906,6 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func IsCLIConfigured() bool { | ||||
| 	return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" | ||||
| } | ||||
| 
 | ||||
| type deprecator struct { | ||||
| 	warns  set.Set[string] | ||||
| 	fatals set.Set[string] | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| package types | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| @ -22,7 +24,7 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "unmarshal-dns-full-config", | ||||
| 			configPath: "testdata/dns_full.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				dns, err := dns() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| @ -48,12 +50,12 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "dns-to-tailcfg.DNSConfig", | ||||
| 			configPath: "testdata/dns_full.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				dns, err := dns() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return DNSToTailcfgDNS(dns), nil | ||||
| 				return dnsToTailcfgDNS(dns), nil | ||||
| 			}, | ||||
| 			want: &tailcfg.DNSConfig{ | ||||
| 				Proxied: true, | ||||
| @ -79,7 +81,7 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "unmarshal-dns-full-no-magic", | ||||
| 			configPath: "testdata/dns_full_no_magic.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				dns, err := dns() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| @ -105,12 +107,12 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "dns-to-tailcfg.DNSConfig", | ||||
| 			configPath: "testdata/dns_full_no_magic.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				dns, err := dns() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return DNSToTailcfgDNS(dns), nil | ||||
| 				return dnsToTailcfgDNS(dns), nil | ||||
| 			}, | ||||
| 			want: &tailcfg.DNSConfig{ | ||||
| 				Proxied: false, | ||||
| @ -136,7 +138,7 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "base-domain-in-server-url-err", | ||||
| 			configPath: "testdata/base-domain-in-server-url.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				return GetHeadscaleConfig() | ||||
| 				return LoadServerConfig() | ||||
| 			}, | ||||
| 			want:    nil, | ||||
| 			wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.", | ||||
| @ -145,7 +147,7 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "base-domain-not-in-server-url", | ||||
| 			configPath: "testdata/base-domain-not-in-server-url.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				cfg, err := GetHeadscaleConfig() | ||||
| 				cfg, err := LoadServerConfig() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| @ -165,7 +167,7 @@ func TestReadConfig(t *testing.T) { | ||||
| 			name:       "policy-path-is-loaded", | ||||
| 			configPath: "testdata/policy-path-is-loaded.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				cfg, err := GetHeadscaleConfig() | ||||
| 				cfg, err := LoadServerConfig() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| @ -245,7 +247,7 @@ func TestReadConfigFromEnv(t *testing.T) { | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				t.Logf("all settings: %#v", viper.AllSettings()) | ||||
| 
 | ||||
| 				dns, err := DNS() | ||||
| 				dns, err := dns() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| @ -289,3 +291,49 @@ func TestReadConfigFromEnv(t *testing.T) { | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestTLSConfigValidation(t *testing.T) { | ||||
| 	tmpDir, err := os.MkdirTemp("", "headscale") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	// defer os.RemoveAll(tmpDir) | ||||
| 	configYaml := []byte(`--- | ||||
| tls_letsencrypt_hostname: example.com | ||||
| tls_letsencrypt_challenge_type: "" | ||||
| tls_cert_path: abc.pem | ||||
| noise: | ||||
|   private_key_path: noise_private.key`) | ||||
| 
 | ||||
| 	// Populate a custom config file | ||||
| 	configFilePath := filepath.Join(tmpDir, "config.yaml") | ||||
| 	err = os.WriteFile(configFilePath, configYaml, 0o600) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Couldn't write file %s", configFilePath) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check configuration validation errors (1) | ||||
| 	err = LoadConfig(tmpDir, false) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	err = validateServerConfig() | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Contains(t, err.Error(), "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both") | ||||
| 	assert.Contains(t, err.Error(), "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are") | ||||
| 	assert.Contains(t, err.Error(), "Fatal config error: server_url must start with https:// or http://") | ||||
| 
 | ||||
| 	// Check configuration validation errors (2) | ||||
| 	configYaml = []byte(`--- | ||||
| noise: | ||||
|   private_key_path: noise_private.key | ||||
| server_url: http://127.0.0.1:8080 | ||||
| tls_letsencrypt_hostname: example.com | ||||
| tls_letsencrypt_challenge_type: TLS-ALPN-01 | ||||
| `) | ||||
| 	err = os.WriteFile(configFilePath, configYaml, 0o600) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Couldn't write file %s", configFilePath) | ||||
| 	} | ||||
| 	err = LoadConfig(tmpDir, false) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
|  | ||||
| @ -4,6 +4,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| @ -735,13 +736,7 @@ func TestNodeTagCommand(t *testing.T) { | ||||
| 
 | ||||
| 	assert.Equal(t, []string{"tag:test"}, node.GetForcedTags()) | ||||
| 
 | ||||
| 	// try to set a wrong tag and retrieve the error | ||||
| 	type errOutput struct { | ||||
| 		Error string `json:"error"` | ||||
| 	} | ||||
| 	var errorOutput errOutput | ||||
| 	err = executeAndUnmarshal( | ||||
| 		headscale, | ||||
| 	_, err = headscale.Execute( | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"nodes", | ||||
| @ -750,10 +745,8 @@ func TestNodeTagCommand(t *testing.T) { | ||||
| 			"-t", "wrong-tag", | ||||
| 			"--output", "json", | ||||
| 		}, | ||||
| 		&errorOutput, | ||||
| 	) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Contains(t, errorOutput.Error, "tag must start with the string 'tag:'") | ||||
| 	assert.ErrorContains(t, err, "tag must start with the string 'tag:'") | ||||
| 
 | ||||
| 	// Test list all nodes after added seconds | ||||
| 	resultMachines := make([]*v1.Node, len(machineKeys)) | ||||
| @ -1398,18 +1391,17 @@ func TestNodeRenameCommand(t *testing.T) { | ||||
| 	assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5") | ||||
| 
 | ||||
| 	// Test failure for too long names | ||||
| 	result, err := headscale.Execute( | ||||
| 	_, err = headscale.Execute( | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"nodes", | ||||
| 			"rename", | ||||
| 			"--identifier", | ||||
| 			fmt.Sprintf("%d", listAll[4].GetId()), | ||||
| 			"testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", | ||||
| 			strings.Repeat("t", 64), | ||||
| 		}, | ||||
| 	) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Contains(t, result, "not be over 63 chars") | ||||
| 	assert.ErrorContains(t, err, "not be over 63 chars") | ||||
| 
 | ||||
| 	var listAllAfterRenameAttempt []v1.Node | ||||
| 	err = executeAndUnmarshal( | ||||
| @ -1536,7 +1528,7 @@ func TestNodeMoveCommand(t *testing.T) { | ||||
| 	assert.Equal(t, allNodes[0].GetUser(), node.GetUser()) | ||||
| 	assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user") | ||||
| 
 | ||||
| 	moveToNonExistingNSResult, err := headscale.Execute( | ||||
| 	_, err = headscale.Execute( | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"nodes", | ||||
| @ -1549,11 +1541,9 @@ func TestNodeMoveCommand(t *testing.T) { | ||||
| 			"json", | ||||
| 		}, | ||||
| 	) | ||||
| 	assert.Nil(t, err) | ||||
| 
 | ||||
| 	assert.Contains( | ||||
| 	assert.ErrorContains( | ||||
| 		t, | ||||
| 		moveToNonExistingNSResult, | ||||
| 		err, | ||||
| 		"user not found", | ||||
| 	) | ||||
| 	assert.Equal(t, node.GetUser().GetName(), "new-user") | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user