diff --git a/cmd/configure.go b/cmd/configure.go index 93a9341..eab150f 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -147,7 +147,7 @@ func configureCmdRun(cmd *cobra.Command, args []string) { b, err := ioutil.ReadAll(res.Body) - cfg, err := config.NewFromPath(configPath) + cfg, err := config.NewAtPath(configPath) if err != nil { panic(err) } @@ -156,7 +156,7 @@ func configureCmdRun(cmd *cobra.Command, args []string) { panic(err) } - if err = cfg.WriteToDisk(); err != nil { + if err = config.WriteToDisk(cfg); err != nil { panic(err) } diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 5514723..d67185f 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -102,7 +102,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) { } printHeader(output, "Wings Configuration") - cfg, err := config.ReadConfiguration(config.DefaultLocation) + cfg, err := config.FromFile(config.DefaultLocation) if cfg != nil { fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation)) fmt.Fprintln(output, "") diff --git a/cmd/root.go b/cmd/root.go index c1080b3..2d4e775 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" "emperror.dev/errors" @@ -26,13 +27,12 @@ import ( "github.com/pterodactyl/wings/sftp" "github.com/pterodactyl/wings/system" "github.com/spf13/cobra" - "github.com/spf13/viper" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" ) var ( - configPath = "" + configPath = config.DefaultLocation debug = false ) @@ -40,6 +40,8 @@ var rootCommand = &cobra.Command{ Use: "wings", Short: "Runs the API server allowing programatic control of game servers for Pterodactyl Panel.", PreRun: func(cmd *cobra.Command, args []string) { + initConfig() + initLogging() if tls, _ := cmd.Flags().GetBool("auto-tls"); tls { if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" { fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com") @@ -65,9 +67,7 @@ func Execute() { } func init() { - cobra.OnInitialize(initConfig, initLogging) - - rootCommand.PersistentFlags().StringVar(&configPath, "config", "", "set the location for the configuration file") + rootCommand.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file") rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode") // Flags specifically used when running the API. @@ -81,27 +81,6 @@ func init() { rootCommand.AddCommand(diagnosticsCmd) } -// Get the configuration path based on the arguments provided. -func readConfiguration() (*config.Configuration, error) { - p := configPath - if !strings.HasPrefix(p, "/") { - d, err := os.Getwd() - if err != nil { - return nil, err - } - - p = path.Clean(path.Join(d, configPath)) - } - - if s, err := os.Stat(p); err != nil { - return nil, err - } else if s.IsDir() { - return nil, errors.New("cannot use directory as configuration file path") - } - - return config.ReadConfiguration(p) -} - func rootCmdRun(cmd *cobra.Command, _ []string) { switch cmd.Flag("profiler").Value.String() { case "cpu": @@ -122,18 +101,9 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { defer profile.Start(profile.BlockProfile).Stop() } - c, err := readConfiguration() - if err != nil { - panic(err) - } - - if debug { - c.Debug = true - } - printLogo() - log.WithField("path", viper.ConfigFileUsed()).Info("loading configuration from file") log.Debug("running in debug mode") + log.WithField("config_file", configPath).Info("loading configuration from file") if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok { log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified") @@ -142,45 +112,39 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { } } - config.Set(c) - config.SetDebugViaFlag(debug) if err := config.ConfigureTimezone(); err != nil { log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value") } - log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone") - + log.WithField("timezone", config.Get().System.Timezone).Info("configured wings with system timezone") if err := config.ConfigureDirectories(); err != nil { log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl") return } - if err := config.EnableLogRotation(); err != nil { log.WithField("error", err).Fatal("failed to configure log rotation on the system") return } - log.WithField("username", c.System.Username).Info("checking for pterodactyl system user") + log.WithField("username", config.Get().System.User).Info("checking for pterodactyl system user") if err := config.EnsurePterodactylUser(); err != nil { log.WithField("error", err).Fatal("failed to create pterodactyl system user") } log.WithFields(log.Fields{ - "username": viper.GetString("system.username"), - "uid": viper.GetInt("system.user.uid"), - "gid": viper.GetInt("system.user.gid"), + "username": config.Get().System.Username, + "uid": config.Get().System.User.Uid, + "gid": config.Get().System.User.Gid, }).Info("configured system user successfully") if err := server.LoadDirectory(); err != nil { log.WithField("error", err).Fatal("failed to load server configurations") - return } if err := environment.ConfigureDocker(cmd.Context()); err != nil { log.WithField("error", err).Fatal("failed to configure docker environment") - return } - if err := viper.WriteConfig(); err != nil { - log.WithField("error", err).Error("failed to save configuration to disk") + if err := config.WriteToDisk(config.Get()); err != nil { + log.WithField("error", err).Fatal("failed to write configuration to disk") } // Just for some nice log output. @@ -197,7 +161,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { // on Wings. This allows us to ensure the environment exists, write configurations, // and reboot processes without causing a slow-down due to sequential booting. pool := workerpool.New(4) - for _, serv := range server.GetServers().All() { s := serv @@ -252,6 +215,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { // Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers. pool.StopWait() + defer func() { + // Cancel the context on all of the running servers at this point, even though the + // program is just shutting down. + for _, s := range server.GetServers().All() { + s.CtxCancel() + } + }() go func() { // Run the SFTP server. @@ -261,13 +231,14 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { } }() + sys := config.Get().System // Ensure the archive directory exists. - if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil { + if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil { log.WithField("error", err).Error("failed to create archive directory") } // Ensure the backup directory exists. - if err := os.MkdirAll(c.System.BackupDirectory, 0755); err != nil { + if err := os.MkdirAll(sys.BackupDirectory, 0755); err != nil { log.WithField("error", err).Error("failed to create backup directory") } @@ -277,47 +248,31 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { autotls = false } + api := config.Get().Api log.WithFields(log.Fields{ - "use_ssl": c.Api.Ssl.Enabled, + "use_ssl": api.Ssl.Enabled, "use_auto_tls": autotls, - "host_address": c.Api.Host, - "host_port": c.Api.Port, + "host_address": api.Host, + "host_port": api.Port, }).Info("configuring internal webserver") - // Configure the router. - r := router.Configure() - + // Create a new HTTP server instance to handle inbound requests from the Panel + // and external clients. s := &http.Server{ - Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port), - Handler: r, - TLSConfig: &tls.Config{ - NextProtos: []string{"h2", "http/1.1"}, - // @see https://blog.cloudflare.com/exposing-go-on-the-internet - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - }, - PreferServerCipherSuites: true, - MinVersion: tls.VersionTLS12, - MaxVersion: tls.VersionTLS13, - CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, - }, + Addr: api.Host + ":" + strconv.Itoa(api.Port), + Handler: router.Configure(), + TLSConfig: config.DefaultTLSConfig, } // Check if the server should run with TLS but using autocert. if autotls { m := autocert.Manager{ Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")), + Cache: autocert.DirCache(path.Join(sys.RootDirectory, "/.tls-cache")), HostPolicy: autocert.HostWhitelist(tlshostname), } - log.WithField("hostname", tlshostname). - Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt") + log.WithField("hostname", tlshostname).Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt") // Hook autocert into the main http server. s.TLSConfig.GetCertificate = m.GetCertificate @@ -329,59 +284,53 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { log.WithError(err).Error("failed to serve autocert http server") } }() - // Start the main http server with TLS using autocert. if err := s.ListenAndServeTLS("", ""); err != nil { - log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}). - Fatal("failed to configure HTTP server using auto-tls") + log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}).Fatal("failed to configure HTTP server using auto-tls") } - return } - // Check if main http server should run with TLS. - if c.Api.Ssl.Enabled { - if err := s.ListenAndServeTLS(strings.ToLower(c.Api.Ssl.CertificateFile), strings.ToLower(c.Api.Ssl.KeyFile)); err != nil { + // Check if main http server should run with TLS. Otherwise reset the TLS + // config on the server and then serve it over normal HTTP. + if api.Ssl.Enabled { + if err := s.ListenAndServeTLS(strings.ToLower(api.Ssl.CertificateFile), strings.ToLower(api.Ssl.KeyFile)); err != nil { log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server") } return } - - // Run the main http server without TLS. s.TLSConfig = nil if err := s.ListenAndServe(); err != nil { log.WithField("error", err).Fatal("failed to configure HTTP server") } - - // Cancel the context on all of the running servers at this point, even though the - // program is just shutting down. - for _, s := range server.GetServers().All() { - s.CtxCancel() - } } +// Reads the configuration from the disk and then sets up the global singleton +// with all of the configuration values. func initConfig() { - if configPath != "" { - viper.SetConfigName("config") - viper.SetConfigType("yaml") - viper.AddConfigPath("/etc/pterodactyl") - viper.AddConfigPath("$HOME/.pterodactyl") - viper.AddConfigPath(".") - } else { - viper.SetConfigFile(configPath) + if !strings.HasPrefix(configPath, "/") { + d, err := os.Getwd() + if err != nil { + log2.Fatalf("cmd/root: could not determine directory: %s", err) + } + configPath = path.Clean(path.Join(d, configPath)) } - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(*viper.ConfigFileNotFoundError); ok { + err := config.FromFile(configPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { exitWithConfigurationNotice() } - log2.Fatalf("cmd/root: failed to read configuration: %s", err) + log2.Fatalf("cmd/root: error while reading configuration file: %s", err) + } + if debug && !config.Get().Debug { + config.SetDebugViaFlag(debug) } } // Configures the global logger for Zap so that we can call it from any location // in the code without having to pass around a logger instance. func initLogging() { - dir := viper.GetString("system.log_directory") + dir := config.Get().System.LogDirectory if err := os.MkdirAll(path.Join(dir, "/install"), 0700); err != nil { log2.Fatalf("cmd/root: failed to create install directory path: %s", err) } @@ -390,12 +339,10 @@ func initLogging() { if err != nil { log2.Fatalf("cmd/root: failed to create wings log: %s", err) } - log.SetLevel(log.InfoLevel) - if viper.GetBool("debug") { + if config.Get().Debug { log.SetLevel(log.DebugLevel) } - log.SetHandler(multi.New(cli.Default, cli.New(w.File, false))) log.WithField("path", p).Info("writing log files to disk") } diff --git a/config/config.go b/config/config.go index 8d2d3d9..7138a38 100644 --- a/config/config.go +++ b/config/config.go @@ -1,35 +1,248 @@ package config import ( + "context" + "crypto/tls" "fmt" "io/ioutil" "os" "os/exec" "os/user" + "path" + "path/filepath" + "regexp" "strings" "sync" + "text/template" + "time" "emperror.dev/errors" + "github.com/apex/log" "github.com/cobaugh/osrelease" "github.com/creasty/defaults" "github.com/gbrlsnchs/jwt/v3" "github.com/pterodactyl/wings/system" - "github.com/spf13/viper" "gopkg.in/yaml.v2" ) const DefaultLocation = "/etc/pterodactyl/config.yml" -type Configuration struct { - sync.RWMutex `json:"-" yaml:"-"` +// DefaultTLSConfig sets sane defaults to use when configuring the internal +// webserver to listen for public connections. +// +// @see https://blog.cloudflare.com/exposing-go-on-the-internet +var DefaultTLSConfig = &tls.Config{ + NextProtos: []string{"h2", "http/1.1"}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }, + PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, +} +var mu sync.RWMutex +var _config *Configuration +var _jwtAlgo *jwt.HMACSHA +var _debugViaFlag bool + +// Locker specific to writing the configuration to the disk, this happens +// in areas that might already be locked so we don't want to crash the process. +var _writeLock sync.Mutex + +// SftpConfiguration defines the configuration of the internal SFTP server. +type SftpConfiguration struct { + // The bind address of the SFTP server. + Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"` + // The bind port of the SFTP server. + Port int `default:"2022" json:"bind_port" yaml:"bind_port"` + // If set to true, no write actions will be allowed on the SFTP server. + ReadOnly bool `default:"false" yaml:"read_only"` +} + +// ApiConfiguration defines the configuration for the internal API that is +// exposed by the Wings webserver. +type ApiConfiguration struct { + // The interface that the internal webserver should bind to. + Host string `default:"0.0.0.0" yaml:"host"` + + // The port that the internal webserver should bind to. + Port int `default:"8080" yaml:"port"` + + // SSL configuration for the daemon. + Ssl struct { + Enabled bool `json:"enabled" yaml:"enabled"` + CertificateFile string `json:"cert" yaml:"cert"` + KeyFile string `json:"key" yaml:"key"` + } + + // Determines if functionality for allowing remote download of files into server directories + // is enabled on this instance. If set to "true" remote downloads will not be possible for + // servers. + DisableRemoteDownload bool `json:"disable_remote_download" yaml:"disable_remote_download"` + + // The maximum size for files uploaded through the Panel in bytes. + UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"` +} + +// RemoteQueryConfiguration defines the configuration settings for remote requests +// from Wings to the Panel. +type RemoteQueryConfiguration struct { + // The amount of time in seconds that Wings should allow for a request to the Panel API + // to complete. If this time passes the request will be marked as failed. If your requests + // are taking longer than 30 seconds to complete it is likely a performance issue that + // should be resolved on the Panel, and not something that should be resolved by upping this + // number. + Timeout uint `default:"30" yaml:"timeout"` + + // The number of servers to load in a single request to the Panel API when booting the + // Wings instance. A single request is initially made to the Panel to get this number + // of servers, and then the pagination status is checked and additional requests are + // fired off in parallel to request the remaining pages. + // + // It is not recommended to change this from the default as you will likely encounter + // memory limits on your Panel instance. In the grand scheme of things 4 requests for + // 50 servers is likely just as quick as two for 100 or one for 400, and will certainly + // be less likely to cause performance issues on the Panel. + BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"` +} + +// SystemConfiguration defines basic system configuration settings. +type SystemConfiguration struct { + // The root directory where all of the pterodactyl data is stored at. + RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"` + + // Directory where logs for server installations and other wings events are logged. + LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"` + + // Directory where the server data is stored at. + Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"` + + // Directory where server archives for transferring will be stored. + ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"` + + // Directory where local backups will be stored on the machine. + BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"` + + // The user that should own all of the server files, and be used for containers. + Username string `default:"pterodactyl" yaml:"username"` + + // The timezone for this Wings instance. This is detected by Wings automatically if possible, + // and falls back to UTC if not able to be detected. If you need to set this manually, that + // can also be done. + // + // This timezone value is passed into all containers created by Wings. + Timezone string `yaml:"timezone"` + + // Definitions for the user that gets created to ensure that we can quickly access + // this information without constantly having to do a system lookup. + User struct { + Uid int + Gid int + } + + // The amount of time in seconds that can elapse before a server's disk space calculation is + // considered stale and a re-check should occur. DANGER: setting this value too low can seriously + // impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings + // process. + // + // Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used + // by a server and should only be set in extreme scenarios where performance is critical and + // disk usage is not a concern. + DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"` + + // If set to true, file permissions for a server will be checked when the process is + // booted. This can cause boot delays if the server has a large amount of files. In most + // cases disabling this should not have any major impact unless external processes are + // frequently modifying a servers' files. + CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"` + + // If set to false Wings will not attempt to write a log rotate configuration to the disk + // when it boots and one is not detected. + EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"` + + // The number of lines to send when a server connects to the websocket. + WebsocketLogCount int `default:"150" yaml:"websocket_log_count"` + + Sftp SftpConfiguration `yaml:"sftp"` + + CrashDetection CrashDetection `yaml:"crash_detection"` + + Backups Backups `yaml:"backups"` + + Transfers Transfers `yaml:"transfers"` +} + +type CrashDetection struct { + // Determines if Wings should detect a server that stops with a normal exit code of + // "0" as being crashed if the process stopped without any Wings interaction. E.g. + // the user did not press the stop button, but the process stopped cleanly. + DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"` + + // Timeout specifies the timeout between crashes that will not cause the server + // to be automatically restarted, this value is used to prevent servers from + // becoming stuck in a boot-loop after multiple consecutive crashes. + Timeout int `default:"60" json:"timeout"` +} + +type Backups struct { + // WriteLimit imposes a Disk I/O write limit on backups to the disk, this affects all + // backup drivers as the archiver must first write the file to the disk in order to + // upload it to any external storage provider. + // + // If the value is less than 1, the write speed is unlimited, + // if the value is greater than 0, the write speed is the value in MiB/s. + // + // Defaults to 0 (unlimited) + WriteLimit int `default:"0" yaml:"write_limit"` +} + +type Transfers struct { + // DownloadLimit imposes a Network I/O read limit when downloading a transfer archive. + // + // If the value is less than 1, the write speed is unlimited, + // if the value is greater than 0, the write speed is the value in MiB/s. + // + // Defaults to 0 (unlimited) + DownloadLimit int `default:"0" yaml:"download_limit"` +} + +type ConsoleThrottles struct { + // Whether or not the throttler is enabled for this instance. + Enabled bool `json:"enabled" yaml:"enabled" default:"true"` + + // The total number of lines that can be output in a given LineResetInterval period before + // a warning is triggered and counted against the server. + Lines uint64 `json:"lines" yaml:"lines" default:"2000"` + + // The total number of throttle activations that can accumulate before a server is considered + // to be breaching and will be stopped. This value is decremented by one every DecayInterval. + MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"` + + // The amount of time after which the number of lines processed is reset to 0. This runs in + // a constant loop and is not affected by the current console output volumes. By default, this + // will reset the processed line count back to 0 every 100ms. + LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"` + + // The amount of time in milliseconds that must pass without an output warning being triggered + // before a throttle activation is decremented. + DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"` + + // The amount of time that a server is allowed to be stopping for before it is terminated + // forcefully if it triggers output throttles. + StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"` +} + +type Configuration struct { // The location from which this configuration instance was instantiated. path string - // Locker specific to writing the configuration to the disk, this happens - // in areas that might already be locked so we don't want to crash the process. - writeLock sync.Mutex - // Determines if wings should be running in debug mode. This value is ignored // if the debug flag is passed through the command line arguments. Debug bool @@ -68,168 +281,110 @@ type Configuration struct { AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"` } -// Defines the configuration of the internal SFTP server. -type SftpConfiguration struct { - // The bind address of the SFTP server. - Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"` - // The bind port of the SFTP server. - Port int `default:"2022" json:"bind_port" yaml:"bind_port"` - // If set to true, no write actions will be allowed on the SFTP server. - ReadOnly bool `default:"false" yaml:"read_only"` -} - -// Defines the configuration for the internal API that is exposed by the -// daemon webserver. -type ApiConfiguration struct { - // The interface that the internal webserver should bind to. - Host string `default:"0.0.0.0" yaml:"host"` - - // The port that the internal webserver should bind to. - Port int `default:"8080" yaml:"port"` - - // SSL configuration for the daemon. - Ssl struct { - Enabled bool `json:"enabled" yaml:"enabled"` - CertificateFile string `json:"cert" yaml:"cert"` - KeyFile string `json:"key" yaml:"key"` - } - - // Determines if functionality for allowing remote download of files into server directories - // is enabled on this instance. If set to "true" remote downloads will not be possible for - // servers. - DisableRemoteDownload bool `json:"disable_remote_download" yaml:"disable_remote_download"` - - // The maximum size for files uploaded through the Panel in bytes. - UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"` -} - -// Defines the configuration settings for remote requests from Wings to the Panel. -type RemoteQueryConfiguration struct { - // The amount of time in seconds that Wings should allow for a request to the Panel API - // to complete. If this time passes the request will be marked as failed. If your requests - // are taking longer than 30 seconds to complete it is likely a performance issue that - // should be resolved on the Panel, and not something that should be resolved by upping this - // number. - Timeout uint `default:"30" yaml:"timeout"` - - // The number of servers to load in a single request to the Panel API when booting the - // Wings instance. A single request is initially made to the Panel to get this number - // of servers, and then the pagination status is checked and additional requests are - // fired off in parallel to request the remaining pages. - // - // It is not recommended to change this from the default as you will likely encounter - // memory limits on your Panel instance. In the grand scheme of things 4 requests for - // 50 servers is likely just as quick as two for 100 or one for 400, and will certainly - // be less likely to cause performance issues on the Panel. - BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"` -} - -// Reads the configuration from the provided file and returns the configuration -// object that can then be used. -func ReadConfiguration(path string) (*Configuration, error) { - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - c := new(Configuration) +// NewAtPath creates a new struct and set the path where it should be stored. +// This function does not modify the currently stored global configuration. +func NewAtPath(path string) (*Configuration, error) { + var c Configuration // Configures the default values for many of the configuration options present // in the structs. Values set in the configuration file take priority over the // default values. - if err := defaults.Set(c); err != nil { + if err := defaults.Set(&c); err != nil { return nil, err } - // Track the location where we created this configuration. - c.unsafeSetPath(path) - - // Replace environment variables within the configuration file with their - // values from the host system. - b = []byte(os.ExpandEnv(string(b))) - - if err := yaml.Unmarshal(b, c); err != nil { - return nil, err - } - - return c, nil + c.path = path + return &c, nil } -var mu sync.RWMutex - -var _config *Configuration -var _jwtAlgo *jwt.HMACSHA -var _debugViaFlag bool - // Set the global configuration instance. This is a blocking operation such that // anything trying to set a different configuration value, or read the configuration // will be paused until it is complete. func Set(c *Configuration) { mu.Lock() - if _config == nil || _config.AuthenticationToken != c.AuthenticationToken { _jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken)) } - _config = c mu.Unlock() } +// SetDebugViaFlag tracks if the application is running in debug mode because of +// a command line flag argument. If so we do not want to store that configuration +// change to the disk. func SetDebugViaFlag(d bool) { + mu.Lock() + _config.Debug = d _debugViaFlag = d + mu.Unlock() } -// Get the global configuration instance. This is a read-safe operation that will block -// if the configuration is presently being modified. +// Get returns the global configuration instance. This is a thread-safe operation +// that will block if the configuration is presently being modified. +// +// Be aware that you CANNOT make modifications to the currently stored configuration +// by modifying the struct returned by this function. The only way to make +// modifications is by using the Update() function and passing data through in +// the callback. func Get() *Configuration { mu.RLock() - defer mu.RUnlock() - - return _config + // Create a copy of the struct so that all modifications made beyond this + // point are immutable. + //goland:noinspection GoVetCopyLock + c := *_config + mu.RUnlock() + return &c } -// Returns the in-memory JWT algorithm. +// Update performs an in-situ update of the global configuration object using +// a thread-safe mutex lock. This is the correct way to make modifications to +// the global configuration. +func Update(callback func(c *Configuration)) { + mu.Lock() + defer mu.Unlock() + callback(_config) +} + +// GetJwtAlgorithm returns the in-memory JWT algorithm. func GetJwtAlgorithm() *jwt.HMACSHA { mu.RLock() defer mu.RUnlock() - return _jwtAlgo } -// Create a new struct and set the path where it should be stored. -func NewFromPath(path string) (*Configuration, error) { - c := new(Configuration) - if err := defaults.Set(c); err != nil { - return c, err +// WriteToDisk writes the configuration to the disk. This is a thread safe operation +// and will only allow one write at a time. Additional calls while writing are +// queued up. +func WriteToDisk(c *Configuration) error { + _writeLock.Lock() + defer _writeLock.Unlock() + + //goland:noinspection GoVetCopyLock + ccopy := *c + // If debugging is set with the flag, don't save that to the configuration file, + // otherwise you'll always end up in debug mode. + if _debugViaFlag { + ccopy.Debug = false } - - c.unsafeSetPath(path) - - return c, nil -} - -// Sets the path where the configuration file is located on the server. This function should -// not be called except by processes that are generating the configuration such as the configuration -// command shipped with this software. -func (c *Configuration) unsafeSetPath(path string) { - c.Lock() - c.path = path - c.Unlock() -} - -// Returns the path for this configuration file. -func (c *Configuration) GetPath() string { - c.RLock() - defer c.RUnlock() - - return c.path + if c.path == "" { + return errors.New("cannot write configuration, no path defined in struct") + } + b, err := yaml.Marshal(&ccopy) + if err != nil { + return err + } + if err := ioutil.WriteFile(c.path, b, 0600); err != nil { + return err + } + return nil } // EnsurePterodactylUser ensures that the Pterodactyl core user exists on the // system. This user will be the owner of all data in the root data directory -// and is used as the user within containers. +// and is used as the user within containers. If files are not owned by this +// user there will be issues with permissions on Docker mount points. // -// If files are not owned by this user there will be issues with permissions on -// Docker mount points. +// This function IS NOT thread safe and should only be called in the main thread +// when the application is booting. func EnsurePterodactylUser() error { sysName, err := getSystemName() if err != nil { @@ -238,14 +393,13 @@ func EnsurePterodactylUser() error { // Our way of detecting if wings is running inside of Docker. if sysName == "busybox" { - viper.Set("system.username", system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pterodactyl")) - viper.Set("system.user.uid", system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))) - viper.Set("system.user.gid", system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_GID"), "988"))) + _config.System.Username = system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pterodactyl") + _config.System.User.Uid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988")) + _config.System.User.Gid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988")) return nil } - username := viper.GetString("system.username") - u, err := user.Lookup(username) + u, err := user.Lookup(_config.System.Username) // If an error is returned but it isn't the unknown user error just abort // the process entirely. If we did find a user, return it immediately. if err != nil { @@ -253,19 +407,19 @@ func EnsurePterodactylUser() error { return err } } else { - viper.Set("system.user.uid", system.MustInt(u.Uid)) - viper.Set("system.user.gid", system.MustInt(u.Gid)) + _config.System.User.Uid = system.MustInt(u.Uid) + _config.System.User.Gid = system.MustInt(u.Gid) return nil } - command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", username) + command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", _config.System.Username) // Alpine Linux is the only OS we currently support that doesn't work with the useradd // command, so in those cases we just modify the command a bit to work as expected. if strings.HasPrefix(sysName, "alpine") { - command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", username) + command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", _config.System.Username) // We have to create the group first on Alpine, so do that here before continuing on // to the user creation process. - if _, err := exec.Command("addgroup", "-S", username).Output(); err != nil { + if _, err := exec.Command("addgroup", "-S", _config.System.Username).Output(); err != nil { return err } } @@ -274,53 +428,189 @@ func EnsurePterodactylUser() error { if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil { return err } - - u, err = user.Lookup(username) + u, err = user.Lookup(_config.System.Username) if err != nil { return err } - viper.Set("system.user.uid", system.MustInt(u.Uid)) - viper.Set("system.user.gid", system.MustInt(u.Gid)) + _config.System.User.Uid = system.MustInt(u.Uid) + _config.System.User.Gid = system.MustInt(u.Gid) return nil } -// Writes the configuration to the disk as a blocking operation by obtaining an exclusive -// lock on the file. This prevents something else from writing at the exact same time and -// leading to bad data conditions. -func (c *Configuration) WriteToDisk() error { - // Obtain an exclusive write against the configuration file. - c.writeLock.Lock() - defer c.writeLock.Unlock() - - ccopy := *c - // If debugging is set with the flag, don't save that to the configuration file, otherwise - // you'll always end up in debug mode. - if _debugViaFlag { - ccopy.Debug = false - } - - if c.path == "" { - return errors.New("cannot write configuration, no path defined in struct") - } - - b, err := yaml.Marshal(&ccopy) +// FromFile reads the configuration from the provided file and stores it in the +// global singleton for this instance. +func FromFile(path string) error { + b, err := ioutil.ReadFile(path) if err != nil { return err } + c, err := NewAtPath(path) + if err != nil { + return err + } + // Replace environment variables within the configuration file with their + // values from the host system. + b = []byte(os.ExpandEnv(string(b))) + if err := yaml.Unmarshal(b, c); err != nil { + return err + } + // Store this configuration in the global state. + Set(c) + return nil +} - if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil { +// ConfigureDirectories ensures that all of the system directories exist on the +// system. These directories are created so that only the owner can read the data, +// and no other users. +// +// This function IS NOT thread-safe. +func ConfigureDirectories() error { + root := _config.System.RootDirectory + log.WithField("path", root).Debug("ensuring root data directory exists") + if err := os.MkdirAll(root, 0700); err != nil { + return err + } + + // There are a non-trivial number of users out there whose data directories are actually a + // symlink to another location on the disk. If we do not resolve that final destination at this + // point things will appear to work, but endless errors will be encountered when we try to + // verify accessed paths since they will all end up resolving outside the expected data directory. + // + // For the sake of automating away as much of this as possible, see if the data directory is a + // symlink, and if so resolve to its final real path, and then update the configuration to use + // that. + if d, err := filepath.EvalSymlinks(_config.System.Data); err != nil { + if !os.IsNotExist(err) { + return err + } + } else if d != _config.System.Data { + _config.System.Data = d + } + + log.WithField("path", _config.System.Data).Debug("ensuring server data directory exists") + if err := os.MkdirAll(_config.System.Data, 0700); err != nil { + return err + } + + log.WithField("path", _config.System.ArchiveDirectory).Debug("ensuring archive data directory exists") + if err := os.MkdirAll(_config.System.ArchiveDirectory, 0700); err != nil { + return err + } + + log.WithField("path", _config.System.BackupDirectory).Debug("ensuring backup data directory exists") + if err := os.MkdirAll(_config.System.BackupDirectory, 0700); err != nil { return err } return nil } +// EnableLogRotation writes a logrotate file for wings to the system logrotate +// configuration directory if one exists and a logrotate file is not found. This +// allows us to basically automate away the log rotation for most installs, but +// also enable users to make modifications on their own. +// +// This function IS NOT thread-safe. +func EnableLogRotation() error { + if !_config.System.EnableLogRotate { + log.Info("skipping log rotate configuration, disabled in wings config file") + return nil + } + + if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) { + return err + } else if (err != nil && os.IsNotExist(err)) || !st.IsDir() { + return nil + } + if _, err := os.Stat("/etc/logrotate.d/wings"); err == nil || !os.IsNotExist(err) { + return err + } + + log.Info("no log rotation configuration found: adding file now") + // If we've gotten to this point it means the logrotate directory exists on the system + // but there is not a file for wings already. In that case, let us write a new file to + // it so files can be rotated easily. + f, err := os.Create("/etc/logrotate.d/wings") + if err != nil { + return err + } + defer f.Close() + + t, err := template.New("logrotate").Parse(` +{{.LogDirectory}}/wings.log { + size 10M + compress + delaycompress + dateext + maxage 7 + missingok + notifempty + create 0640 {{.User.Uid}} {{.User.Gid}} + postrotate + killall -SIGHUP wings + endscript +}`) + if err != nil { + return err + } + + return errors.Wrap(t.Execute(f, _config.System), "config: failed to write logrotate to disk") +} + +// GetStatesPath returns the location of the JSON file that tracks server states. +func (sc *SystemConfiguration) GetStatesPath() string { + return path.Join(sc.RootDirectory, "/states.json") +} + +// ConfigureTimezone sets the timezone data for the configuration if it is +// currently missing. If a value has been set, this functionality will only run +// to validate that the timezone being used is valid. +// +// This function IS NOT thread-safe. +func ConfigureTimezone() error { + if _config.System.Timezone == "" { + b, err := ioutil.ReadFile("/etc/timezone") + if err != nil { + if !os.IsNotExist(err) { + return errors.WithMessage(err, "config: failed to open timezone file") + } + + _config.System.Timezone = "UTC" + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + // Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this + // command fails, exit, but if it returns a value use that. If no value is returned we will + // fall through to UTC to get Wings booted at least. + out, err := exec.CommandContext(ctx, "timedatectl").Output() + if err != nil { + log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC") + return nil + } + + r := regexp.MustCompile(`Time zone: ([\w/]+)`) + matches := r.FindSubmatch(out) + if len(matches) != 2 || string(matches[1]) == "" { + log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC") + return nil + } + _config.System.Timezone = string(matches[1]) + } else { + _config.System.Timezone = string(b) + } + } + + _config.System.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(_config.System.Timezone, "") + _, err := time.LoadLocation(_config.System.Timezone) + + return errors.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", _config.System.Timezone)) +} + // Gets the system release name. func getSystemName() (string, error) { // use osrelease to get release version and ID - if release, err := osrelease.Read(); err != nil { + release, err := osrelease.Read() + if err != nil { return "", err - } else { - return release["ID"], nil } -} + return release["ID"], nil +} \ No newline at end of file diff --git a/config/config_docker.go b/config/config_docker.go index f538173..3db5a56 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -12,7 +12,6 @@ type dockerNetworkInterfaces struct { Subnet string `default:"172.18.0.0/16"` Gateway string `default:"172.18.0.1"` } - V6 struct { Subnet string `default:"fdba:17c8:6c94::/64"` Gateway string `default:"fdba:17c8:6c94::1011"` @@ -39,8 +38,8 @@ type DockerNetworkConfiguration struct { Interfaces dockerNetworkInterfaces `yaml:"interfaces"` } -// Defines the docker configuration used by the daemon when interacting with -// containers and networks on the system. +// DockerConfiguration defines the docker configuration used by the daemon when +// interacting with containers and networks on the system. type DockerConfiguration struct { // Network configuration that should be used when creating a new network // for containers run through the daemon. @@ -58,23 +57,22 @@ type DockerConfiguration struct { TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"` } -// RegistryConfiguration . +// RegistryConfiguration defines the authentication credentials for a given +// Docker registry. type RegistryConfiguration struct { Username string `yaml:"username"` Password string `yaml:"password"` } -// Base64 . +// Base64 returns the authentication for a given registry as a base64 encoded +// string value. func (c RegistryConfiguration) Base64() (string, error) { - authConfig := types.AuthConfig{ + b, err := json.Marshal(types.AuthConfig{ Username: c.Username, Password: c.Password, - } - - b, err := json.Marshal(authConfig) + }) if err != nil { return "", err } - return base64.URLEncoding.EncodeToString(b), nil } diff --git a/config/config_system.go b/config/config_system.go deleted file mode 100644 index 0b2fd9a..0000000 --- a/config/config_system.go +++ /dev/null @@ -1,279 +0,0 @@ -package config - -import ( - "context" - "fmt" - "html/template" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "time" - - "emperror.dev/errors" - "github.com/apex/log" - "github.com/spf13/viper" -) - -// Defines basic system configuration settings. -type SystemConfiguration struct { - // The root directory where all of the pterodactyl data is stored at. - RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"` - - // Directory where logs for server installations and other wings events are logged. - LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"` - - // Directory where the server data is stored at. - Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"` - - // Directory where server archives for transferring will be stored. - ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"` - - // Directory where local backups will be stored on the machine. - BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"` - - // The user that should own all of the server files, and be used for containers. - Username string `default:"pterodactyl" yaml:"username"` - - // The timezone for this Wings instance. This is detected by Wings automatically if possible, - // and falls back to UTC if not able to be detected. If you need to set this manually, that - // can also be done. - // - // This timezone value is passed into all containers created by Wings. - Timezone string `yaml:"timezone"` - - // Definitions for the user that gets created to ensure that we can quickly access - // this information without constantly having to do a system lookup. - User struct { - Uid int - Gid int - } - - // The amount of time in seconds that can elapse before a server's disk space calculation is - // considered stale and a re-check should occur. DANGER: setting this value too low can seriously - // impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings - // process. - // - // Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used - // by a server and should only be set in extreme scenarios where performance is critical and - // disk usage is not a concern. - DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"` - - // If set to true, file permissions for a server will be checked when the process is - // booted. This can cause boot delays if the server has a large amount of files. In most - // cases disabling this should not have any major impact unless external processes are - // frequently modifying a servers' files. - CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"` - - // If set to false Wings will not attempt to write a log rotate configuration to the disk - // when it boots and one is not detected. - EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"` - - // The number of lines to send when a server connects to the websocket. - WebsocketLogCount int `default:"150" yaml:"websocket_log_count"` - - Sftp SftpConfiguration `yaml:"sftp"` - - CrashDetection CrashDetection `yaml:"crash_detection"` - - Backups Backups `yaml:"backups"` - - Transfers Transfers `yaml:"transfers"` -} - -type CrashDetection struct { - // Determines if Wings should detect a server that stops with a normal exit code of - // "0" as being crashed if the process stopped without any Wings interaction. E.g. - // the user did not press the stop button, but the process stopped cleanly. - DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"` - - // Timeout specifies the timeout between crashes that will not cause the server - // to be automatically restarted, this value is used to prevent servers from - // becoming stuck in a boot-loop after multiple consecutive crashes. - Timeout int `default:"60" json:"timeout"` -} - -type Backups struct { - // WriteLimit imposes a Disk I/O write limit on backups to the disk, this affects all - // backup drivers as the archiver must first write the file to the disk in order to - // upload it to any external storage provider. - // - // If the value is less than 1, the write speed is unlimited, - // if the value is greater than 0, the write speed is the value in MiB/s. - // - // Defaults to 0 (unlimited) - WriteLimit int `default:"0" yaml:"write_limit"` -} - -type Transfers struct { - // DownloadLimit imposes a Network I/O read limit when downloading a transfer archive. - // - // If the value is less than 1, the write speed is unlimited, - // if the value is greater than 0, the write speed is the value in MiB/s. - // - // Defaults to 0 (unlimited) - DownloadLimit int `default:"0" yaml:"download_limit"` -} - -// ConfigureDirectories ensures that all of the system directories exist on the -// system. These directories are created so that only the owner can read the data, -// and no other users. -func ConfigureDirectories() error { - root := viper.GetString("system.root_directory") - log.WithField("path", root).Debug("ensuring root data directory exists") - if err := os.MkdirAll(root, 0700); err != nil { - return err - } - - // There are a non-trivial number of users out there whose data directories are actually a - // symlink to another location on the disk. If we do not resolve that final destination at this - // point things will appear to work, but endless errors will be encountered when we try to - // verify accessed paths since they will all end up resolving outside the expected data directory. - // - // For the sake of automating away as much of this as possible, see if the data directory is a - // symlink, and if so resolve to its final real path, and then update the configuration to use - // that. - data := viper.GetString("system.data") - if d, err := filepath.EvalSymlinks(data); err != nil { - if !os.IsNotExist(err) { - return err - } - } else if d != data { - data = d - viper.Set("system.data", d) - } - - log.WithField("path", data).Debug("ensuring server data directory exists") - if err := os.MkdirAll(data, 0700); err != nil { - return err - } - - log.WithField("path", viper.GetString("system.archive_directory")).Debug("ensuring archive data directory exists") - if err := os.MkdirAll(viper.GetString("system.archive_directory"), 0700); err != nil { - return err - } - - log.WithField("path", viper.GetString("system.backup_directory")).Debug("ensuring backup data directory exists") - if err := os.MkdirAll(viper.GetString("system.backup_directory"), 0700); err != nil { - return err - } - - return nil -} - -// EnableLogRotation writes a logrotate file for wings to the system logrotate -// configuration directory if one exists and a logrotate file is not found. This -// allows us to basically automate away the log rotation for most installs, but -// also enable users to make modifications on their own. -func EnableLogRotation() error { - // Do nothing if not enabled. - if !viper.GetBool("system.enable_log_rotate") { - log.Info("skipping log rotate configuration, disabled in wings config file") - return nil - } - - if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) { - return err - } else if (err != nil && os.IsNotExist(err)) || !st.IsDir() { - return nil - } - if _, err := os.Stat("/etc/logrotate.d/wings"); err == nil || !os.IsNotExist(err) { - return err - } - - log.Info("no log rotation configuration found: adding file now") - // If we've gotten to this point it means the logrotate directory exists on the system - // but there is not a file for wings already. In that case, let us write a new file to - // it so files can be rotated easily. - f, err := os.Create("/etc/logrotate.d/wings") - if err != nil { - return err - } - defer f.Close() - - type logrotateConfig struct { - Directory string - UserID int - GroupID int - } - - t, err := template.New("logrotate").Parse(` -{{.Directory}}/wings.log { - size 10M - compress - delaycompress - dateext - maxage 7 - missingok - notifempty - create 0640 {{.UserID}} {{.GroupID}} - postrotate - killall -SIGHUP wings - endscript -}`) - if err != nil { - return err - } - - err = t.Execute(f, logrotateConfig{ - Directory: viper.GetString("system.log_directory"), - UserID: viper.GetInt("system.user.uid"), - GroupID: viper.GetInt("system.user.gid"), - }) - return errors.Wrap(err, "config: failed to write logrotate to disk") -} - -// Returns the location of the JSON file that tracks server states. -func (sc *SystemConfiguration) GetStatesPath() string { - return path.Join(sc.RootDirectory, "states.json") -} - -// Returns the location of the JSON file that tracks server states. -func (sc *SystemConfiguration) GetInstallLogPath() string { - return path.Join(sc.LogDirectory, "install/") -} - -// ConfigureTimezone sets the timezone data for the configuration if it is -// currently missing. If a value has been set, this functionality will only run -// to validate that the timezone being used is valid. -func ConfigureTimezone() error { - tz := viper.GetString("system.timezone") - defer viper.Set("system.timezone", tz) - if tz == "" { - b, err := ioutil.ReadFile("/etc/timezone") - if err != nil { - if !os.IsNotExist(err) { - return errors.WithMessage(err, "config: failed to open timezone file") - } - - tz = "UTC" - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - // Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this - // command fails, exit, but if it returns a value use that. If no value is returned we will - // fall through to UTC to get Wings booted at least. - out, err := exec.CommandContext(ctx, "timedatectl").Output() - if err != nil { - log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC") - return nil - } - - r := regexp.MustCompile(`Time zone: ([\w/]+)`) - matches := r.FindSubmatch(out) - if len(matches) != 2 || string(matches[1]) == "" { - log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC") - return nil - } - tz = string(matches[1]) - } else { - tz = string(b) - } - } - - tz = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(tz, "") - _, err := time.LoadLocation(tz) - - return errors.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", tz)) -} diff --git a/config/config_throttles.go b/config/config_throttles.go deleted file mode 100644 index e9c5531..0000000 --- a/config/config_throttles.go +++ /dev/null @@ -1,27 +0,0 @@ -package config - -type ConsoleThrottles struct { - // Whether or not the throttler is enabled for this instance. - Enabled bool `json:"enabled" yaml:"enabled" default:"true"` - - // The total number of lines that can be output in a given LineResetInterval period before - // a warning is triggered and counted against the server. - Lines uint64 `json:"lines" yaml:"lines" default:"2000"` - - // The total number of throttle activations that can accumulate before a server is considered - // to be breaching and will be stopped. This value is decremented by one every DecayInterval. - MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"` - - // The amount of time after which the number of lines processed is reset to 0. This runs in - // a constant loop and is not affected by the current console output volumes. By default, this - // will reset the processed line count back to 0 every 100ms. - LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"` - - // The amount of time in milliseconds that must pass without an output warning being triggered - // before a throttle activation is decremented. - DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"` - - // The amount of time that a server is allowed to be stopping for before it is terminated - // forcefully if it triggers output throttles. - StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"` -} diff --git a/environment/docker.go b/environment/docker.go index 8b8b9c3..edb7b81 100644 --- a/environment/docker.go +++ b/environment/docker.go @@ -9,7 +9,6 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" - "github.com/spf13/viper" ) var _conce sync.Once diff --git a/go.mod b/go.mod index b25e755..4659f61 100644 --- a/go.mod +++ b/go.mod @@ -63,8 +63,6 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f github.com/sirupsen/logrus v1.7.0 // indirect github.com/spf13/cobra v1.1.1 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.7.1 github.com/ugorji/go v1.2.2 // indirect github.com/ulikunitz/xz v0.5.9 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad diff --git a/go.sum b/go.sum index ca88e19..ff4afb8 100644 --- a/go.sum +++ b/go.sum @@ -567,9 +567,8 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= diff --git a/server/install.go b/server/install.go index 7faf69d..d6d392d 100644 --- a/server/install.go +++ b/server/install.go @@ -326,7 +326,7 @@ func (ip *InstallationProcess) BeforeExecute() error { // Returns the log path for the installation process. func (ip *InstallationProcess) GetLogPath() string { - return filepath.Join(config.Get().System.GetInstallLogPath(), ip.Server.Id()+".log") + return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.Id()+".log") } // Cleans up after the execution of the installation process. This grabs the logs from the