mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge pull request #612 from huskyii/enhance_cli_config
This commit is contained in:
		
						commit
						efca3daa5c
					
				| @ -22,6 +22,7 @@ | ||||
|   - This change disables the logs by default | ||||
| - Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598) | ||||
| - Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601) | ||||
| - Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285) [#612](https://github.com/juanfont/headscale/pull/601) | ||||
| 
 | ||||
| ## 0.15.0 (2022-03-20) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										28
									
								
								cmd/headscale/cli/dump_config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								cmd/headscale/cli/dump_config.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package cli | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	rootCmd.AddCommand(dumpConfigCmd) | ||||
| } | ||||
| 
 | ||||
| var dumpConfigCmd = &cobra.Command{ | ||||
| 	Use:    "dumpConfig", | ||||
| 	Short:  "dump current config to /etc/headscale/config.dump.yaml, integration test only", | ||||
| 	Hidden: true, | ||||
| 	Args: func(cmd *cobra.Command, args []string) error { | ||||
| 		return nil | ||||
| 	}, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		err := viper.WriteConfigAs("/etc/headscale/config.dump.yaml") | ||||
| 		if err != nil { | ||||
| 			//nolint
 | ||||
| 			fmt.Println("Failed to dump config") | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -3,17 +3,75 @@ package cli | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"runtime" | ||||
| 
 | ||||
| 	"github.com/juanfont/headscale" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/tcnksm/go-latest" | ||||
| ) | ||||
| 
 | ||||
| var cfgFile string = "" | ||||
| 
 | ||||
| func init() { | ||||
| 	cobra.OnInitialize(initConfig) | ||||
| 	rootCmd.PersistentFlags(). | ||||
| 		StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)") | ||||
| 	rootCmd.PersistentFlags(). | ||||
| 		StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'") | ||||
| 	rootCmd.PersistentFlags(). | ||||
| 		Bool("force", false, "Disable prompts and forces the execution") | ||||
| } | ||||
| 
 | ||||
| func initConfig() { | ||||
| 	if cfgFile != "" { | ||||
| 		err := headscale.LoadConfig(cfgFile, true) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Caller().Err(err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		err := headscale.LoadConfig("", false) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Caller().Err(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	cfg, err := headscale.GetHeadscaleConfig() | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Caller().Err(err) | ||||
| 	} | ||||
| 
 | ||||
| 	machineOutput := HasMachineOutputFlag() | ||||
| 
 | ||||
| 	zerolog.SetGlobalLevel(cfg.LogLevel) | ||||
| 
 | ||||
| 	// If the user has requested a "machine" readable format,
 | ||||
| 	// then disable login so the output remains valid.
 | ||||
| 	if machineOutput { | ||||
| 		zerolog.SetGlobalLevel(zerolog.Disabled) | ||||
| 	} | ||||
| 
 | ||||
| 	if !cfg.DisableUpdateCheck && !machineOutput { | ||||
| 		if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && | ||||
| 			Version != "dev" { | ||||
| 			githubTag := &latest.GithubTag{ | ||||
| 				Owner:      "juanfont", | ||||
| 				Repository: "headscale", | ||||
| 			} | ||||
| 			res, err := latest.Check(githubTag, Version) | ||||
| 			if err == nil && res.Outdated { | ||||
| 				//nolint
 | ||||
| 				fmt.Printf( | ||||
| 					"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n", | ||||
| 					res.Current, | ||||
| 					Version, | ||||
| 				) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Use:   "headscale", | ||||
| 	Short: "headscale - a Tailscale control server", | ||||
|  | ||||
| @ -1,17 +1,13 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"runtime" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/efekarakus/termcolor" | ||||
| 	"github.com/juanfont/headscale" | ||||
| 	"github.com/juanfont/headscale/cmd/headscale/cli" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/tcnksm/go-latest" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| @ -43,39 +39,5 @@ func main() { | ||||
| 		NoColor:    !colors, | ||||
| 	}) | ||||
| 
 | ||||
| 	cfg, err := headscale.GetHeadscaleConfig() | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Caller().Err(err) | ||||
| 	} | ||||
| 
 | ||||
| 	machineOutput := cli.HasMachineOutputFlag() | ||||
| 
 | ||||
| 	zerolog.SetGlobalLevel(cfg.LogLevel) | ||||
| 
 | ||||
| 	// If the user has requested a "machine" readable format,
 | ||||
| 	// then disable login so the output remains valid.
 | ||||
| 	if machineOutput { | ||||
| 		zerolog.SetGlobalLevel(zerolog.Disabled) | ||||
| 	} | ||||
| 
 | ||||
| 	if !cfg.DisableUpdateCheck && !machineOutput { | ||||
| 		if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && | ||||
| 			cli.Version != "dev" { | ||||
| 			githubTag := &latest.GithubTag{ | ||||
| 				Owner:      "juanfont", | ||||
| 				Repository: "headscale", | ||||
| 			} | ||||
| 			res, err := latest.Check(githubTag, cli.Version) | ||||
| 			if err == nil && res.Outdated { | ||||
| 				//nolint
 | ||||
| 				fmt.Printf( | ||||
| 					"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n", | ||||
| 					res.Current, | ||||
| 					cli.Version, | ||||
| 				) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	cli.Execute() | ||||
| } | ||||
|  | ||||
| @ -27,6 +27,51 @@ func (s *Suite) SetUpSuite(c *check.C) { | ||||
| func (s *Suite) TearDownSuite(c *check.C) { | ||||
| } | ||||
| 
 | ||||
| func (*Suite) TestConfigFileLoading(c *check.C) { | ||||
| 	tmpDir, err := ioutil.TempDir("", "headscale") | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	path, err := os.Getwd() | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	cfgFile := filepath.Join(tmpDir, "config.yaml") | ||||
| 
 | ||||
| 	// Symlink the example config file
 | ||||
| 	err = os.Symlink( | ||||
| 		filepath.Clean(path+"/../../config-example.yaml"), | ||||
| 		cfgFile, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Load example config, it should load without validation errors
 | ||||
| 	err = headscale.LoadConfig(cfgFile, true) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	// Test that config file was interpreted correctly
 | ||||
| 	c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080") | ||||
| 	c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080") | ||||
| 	c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") | ||||
| 	c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") | ||||
| 	c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") | ||||
| 	c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") | ||||
| 	c.Assert( | ||||
| 		headscale.GetFileMode("unix_socket_permission"), | ||||
| 		check.Equals, | ||||
| 		fs.FileMode(0o770), | ||||
| 	) | ||||
| 	c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false) | ||||
| } | ||||
| 
 | ||||
| func (*Suite) TestConfigLoading(c *check.C) { | ||||
| 	tmpDir, err := ioutil.TempDir("", "headscale") | ||||
| 	if err != nil { | ||||
| @ -49,7 +94,7 @@ func (*Suite) TestConfigLoading(c *check.C) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Load example config, it should load without validation errors
 | ||||
| 	err = headscale.LoadConfig(tmpDir) | ||||
| 	err = headscale.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	// Test that config file was interpreted correctly
 | ||||
| @ -92,7 +137,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Load example config, it should load without validation errors
 | ||||
| 	err = headscale.LoadConfig(tmpDir) | ||||
| 	err = headscale.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	dnsConfig, baseDomain := headscale.GetDNSConfig() | ||||
| @ -125,7 +170,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 
 | ||||
| 	// Check configuration validation errors (1)
 | ||||
| 	err = headscale.LoadConfig(tmpDir) | ||||
| 	err = headscale.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	// check.Matches can not handle multiline strings
 | ||||
| 	tmp := strings.ReplaceAll(err.Error(), "\n", "***") | ||||
| @ -150,6 +195,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { | ||||
| 		"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", | ||||
| 	) | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 	err = headscale.LoadConfig(tmpDir) | ||||
| 	err = headscale.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| } | ||||
|  | ||||
							
								
								
									
										25
									
								
								config.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								config.go
									
									
									
									
									
								
							| @ -114,15 +114,19 @@ type ACLConfig struct { | ||||
| 	PolicyPath string | ||||
| } | ||||
| 
 | ||||
| func LoadConfig(path string) error { | ||||
| 	viper.SetConfigName("config") | ||||
| 	if path == "" { | ||||
| 		viper.AddConfigPath("/etc/headscale/") | ||||
| 		viper.AddConfigPath("$HOME/.headscale") | ||||
| 		viper.AddConfigPath(".") | ||||
| func LoadConfig(path string, isFile bool) error { | ||||
| 	if isFile { | ||||
| 		viper.SetConfigFile(path) | ||||
| 	} else { | ||||
| 		// For testing
 | ||||
| 		viper.AddConfigPath(path) | ||||
| 		viper.SetConfigName("config") | ||||
| 		if path == "" { | ||||
| 			viper.AddConfigPath("/etc/headscale/") | ||||
| 			viper.AddConfigPath("$HOME/.headscale") | ||||
| 			viper.AddConfigPath(".") | ||||
| 		} else { | ||||
| 			// For testing
 | ||||
| 			viper.AddConfigPath(path) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	viper.SetEnvPrefix("headscale") | ||||
| @ -377,11 +381,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { | ||||
| } | ||||
| 
 | ||||
| func GetHeadscaleConfig() (*Config, error) { | ||||
| 	err := LoadConfig("") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	dnsConfig, baseDomain := GetDNSConfig() | ||||
| 	derpConfig := GetDERPConfig() | ||||
| 	logConfig := GetLogTailConfig() | ||||
|  | ||||
| @ -1721,3 +1721,43 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() { | ||||
| 
 | ||||
| 	assert.Equal(s.T(), machine.Namespace, oldNamespace) | ||||
| } | ||||
| 
 | ||||
| func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() { | ||||
| 	// TODO: make sure defaultConfig is not same as altConfig
 | ||||
| 	defaultConfig, err := os.ReadFile("integration_test/etc/config.dump.gold.yaml") | ||||
| 	assert.Nil(s.T(), err) | ||||
| 	altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml") | ||||
| 	assert.Nil(s.T(), err) | ||||
| 
 | ||||
| 	_, err = ExecuteCommand( | ||||
| 		&s.headscale, | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"dumpConfig", | ||||
| 		}, | ||||
| 		[]string{}, | ||||
| 	) | ||||
| 	assert.Nil(s.T(), err) | ||||
| 
 | ||||
| 	defaultDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml") | ||||
| 	assert.Nil(s.T(), err) | ||||
| 
 | ||||
| 	assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig)) | ||||
| 
 | ||||
| 	_, err = ExecuteCommand( | ||||
| 		&s.headscale, | ||||
| 		[]string{ | ||||
| 			"headscale", | ||||
| 			"-c", | ||||
| 			"/etc/headscale/alt-config.yaml", | ||||
| 			"dumpConfig", | ||||
| 		}, | ||||
| 		[]string{}, | ||||
| 	) | ||||
| 	assert.Nil(s.T(), err) | ||||
| 
 | ||||
| 	altDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml") | ||||
| 	assert.Nil(s.T(), err) | ||||
| 
 | ||||
| 	assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig)) | ||||
| } | ||||
|  | ||||
							
								
								
									
										46
									
								
								integration_test/etc/alt-config.dump.gold.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								integration_test/etc/alt-config.dump.gold.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| acl_policy_path: "" | ||||
| cli: | ||||
|   insecure: false | ||||
|   timeout: 5s | ||||
| db_path: /tmp/integration_test_db.sqlite3 | ||||
| db_type: sqlite3 | ||||
| derp: | ||||
|   auto_update_enabled: false | ||||
|   server: | ||||
|     enabled: false | ||||
|     stun: | ||||
|       enabled: true | ||||
|   update_frequency: 1m | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default | ||||
| dns_config: | ||||
|   base_domain: headscale.net | ||||
|   domains: [] | ||||
|   magic_dns: true | ||||
|   nameservers: | ||||
|     - 1.1.1.1 | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| grpc_allow_insecure: false | ||||
| grpc_listen_addr: :50443 | ||||
| ip_prefixes: | ||||
|   - fd7a:115c:a1e0::/48 | ||||
|   - 100.64.0.0/10 | ||||
| listen_addr: 0.0.0.0:18080 | ||||
| log_level: disabled | ||||
| logtail: | ||||
|   enabled: false | ||||
| metrics_listen_addr: 127.0.0.1:19090 | ||||
| oidc: | ||||
|   scope: | ||||
|     - openid | ||||
|     - profile | ||||
|     - email | ||||
|   strip_email_domain: true | ||||
| private_key_path: private.key | ||||
| server_url: http://headscale:18080 | ||||
| tls_client_auth_mode: relaxed | ||||
| tls_letsencrypt_cache_dir: /var/www/.cache | ||||
| tls_letsencrypt_challenge_type: HTTP-01 | ||||
| unix_socket: /var/run/headscale.sock | ||||
| unix_socket_permission: "0o770" | ||||
| 
 | ||||
							
								
								
									
										24
									
								
								integration_test/etc/alt-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								integration_test/etc/alt-config.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| log_level: trace | ||||
| acl_policy_path: "" | ||||
| db_type: sqlite3 | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| ip_prefixes: | ||||
|   - fd7a:115c:a1e0::/48 | ||||
|   - 100.64.0.0/10 | ||||
| dns_config: | ||||
|   base_domain: headscale.net | ||||
|   magic_dns: true | ||||
|   domains: [] | ||||
|   nameservers: | ||||
|     - 1.1.1.1 | ||||
| db_path: /tmp/integration_test_db.sqlite3 | ||||
| private_key_path: private.key | ||||
| listen_addr: 0.0.0.0:18080 | ||||
| metrics_listen_addr: 127.0.0.1:19090 | ||||
| server_url: http://headscale:18080 | ||||
| 
 | ||||
| derp: | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default | ||||
|   auto_update_enabled: false | ||||
|   update_frequency: 1m | ||||
							
								
								
									
										46
									
								
								integration_test/etc/config.dump.gold.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								integration_test/etc/config.dump.gold.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| acl_policy_path: "" | ||||
| cli: | ||||
|   insecure: false | ||||
|   timeout: 5s | ||||
| db_path: /tmp/integration_test_db.sqlite3 | ||||
| db_type: sqlite3 | ||||
| derp: | ||||
|   auto_update_enabled: false | ||||
|   server: | ||||
|     enabled: false | ||||
|     stun: | ||||
|       enabled: true | ||||
|   update_frequency: 1m | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default | ||||
| dns_config: | ||||
|   base_domain: headscale.net | ||||
|   domains: [] | ||||
|   magic_dns: true | ||||
|   nameservers: | ||||
|     - 1.1.1.1 | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| grpc_allow_insecure: false | ||||
| grpc_listen_addr: :50443 | ||||
| ip_prefixes: | ||||
|   - fd7a:115c:a1e0::/48 | ||||
|   - 100.64.0.0/10 | ||||
| listen_addr: 0.0.0.0:8080 | ||||
| log_level: disabled | ||||
| logtail: | ||||
|   enabled: false | ||||
| metrics_listen_addr: 127.0.0.1:9090 | ||||
| oidc: | ||||
|   scope: | ||||
|     - openid | ||||
|     - profile | ||||
|     - email | ||||
|   strip_email_domain: true | ||||
| private_key_path: private.key | ||||
| server_url: http://headscale:8080 | ||||
| tls_client_auth_mode: relaxed | ||||
| tls_letsencrypt_cache_dir: /var/www/.cache | ||||
| tls_letsencrypt_challenge_type: HTTP-01 | ||||
| unix_socket: /var/run/headscale.sock | ||||
| unix_socket_permission: "0o770" | ||||
| 
 | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user