mirror of
https://github.com/gomods/athens
synced 2026-02-03 11:00:32 +00:00
394 lines
12 KiB
Go
394 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/gomods/athens/pkg/download/mode"
|
|
"github.com/gomods/athens/pkg/errors"
|
|
"github.com/kelseyhightower/envconfig"
|
|
"gopkg.in/go-playground/validator.v9"
|
|
)
|
|
|
|
const defaultConfigFile = "athens.toml"
|
|
|
|
// Config provides configuration values for all components
|
|
type Config struct {
|
|
TimeoutConf
|
|
GoEnv string `validate:"required" envconfig:"GO_ENV"`
|
|
GoBinary string `validate:"required" envconfig:"GO_BINARY_PATH"`
|
|
GoProxy string `envconfig:"GOPROXY"`
|
|
GoBinaryEnvVars EnvList `envconfig:"ATHENS_GO_BINARY_ENV_VARS"`
|
|
GoGetWorkers int `validate:"required" envconfig:"ATHENS_GOGET_WORKERS"`
|
|
GoGetDir string `envconfig:"ATHENS_GOGET_DIR"`
|
|
ProtocolWorkers int `validate:"required" envconfig:"ATHENS_PROTOCOL_WORKERS"`
|
|
LogLevel string `validate:"required" envconfig:"ATHENS_LOG_LEVEL"`
|
|
CloudRuntime string `validate:"required" envconfig:"ATHENS_CLOUD_RUNTIME"`
|
|
EnablePprof bool `envconfig:"ATHENS_ENABLE_PPROF"`
|
|
PprofPort string `envconfig:"ATHENS_PPROF_PORT"`
|
|
FilterFile string `envconfig:"ATHENS_FILTER_FILE"`
|
|
TraceExporterURL string `envconfig:"ATHENS_TRACE_EXPORTER_URL"`
|
|
TraceExporter string `envconfig:"ATHENS_TRACE_EXPORTER"`
|
|
StatsExporter string `envconfig:"ATHENS_STATS_EXPORTER"`
|
|
StorageType string `validate:"required" envconfig:"ATHENS_STORAGE_TYPE"`
|
|
GlobalEndpoint string `envconfig:"ATHENS_GLOBAL_ENDPOINT"` // This feature is not yet implemented
|
|
Port string `envconfig:"ATHENS_PORT"`
|
|
BasicAuthUser string `envconfig:"BASIC_AUTH_USER"`
|
|
BasicAuthPass string `envconfig:"BASIC_AUTH_PASS"`
|
|
ForceSSL bool `envconfig:"PROXY_FORCE_SSL"`
|
|
ValidatorHook string `envconfig:"ATHENS_PROXY_VALIDATOR"`
|
|
PathPrefix string `envconfig:"ATHENS_PATH_PREFIX"`
|
|
NETRCPath string `envconfig:"ATHENS_NETRC_PATH"`
|
|
GithubToken string `envconfig:"ATHENS_GITHUB_TOKEN"`
|
|
HGRCPath string `envconfig:"ATHENS_HGRC_PATH"`
|
|
TLSCertFile string `envconfig:"ATHENS_TLSCERT_FILE"`
|
|
TLSKeyFile string `envconfig:"ATHENS_TLSKEY_FILE"`
|
|
SumDBs []string `envconfig:"ATHENS_SUM_DBS"`
|
|
NoSumPatterns []string `envconfig:"ATHENS_GONOSUM_PATTERNS"`
|
|
DownloadMode mode.Mode `envconfig:"ATHENS_DOWNLOAD_MODE"`
|
|
DownloadURL string `envconfig:"ATHENS_DOWNLOAD_URL"`
|
|
NetworkMode string `validate:"oneof=strict offline fallback" envconfig:"ATHENS_NETWORK_MODE"`
|
|
SingleFlightType string `envconfig:"ATHENS_SINGLE_FLIGHT_TYPE"`
|
|
RobotsFile string `envconfig:"ATHENS_ROBOTS_FILE"`
|
|
IndexType string `envconfig:"ATHENS_INDEX_TYPE"`
|
|
SingleFlight *SingleFlight
|
|
Storage *Storage
|
|
Index *Index
|
|
}
|
|
|
|
// EnvList is a list of key-value environment
|
|
// variables that are passed to the Go command
|
|
type EnvList []string
|
|
|
|
// HasKey returns whether a key-value entry
|
|
// is present by only checking the left of
|
|
// key=value
|
|
func (el EnvList) HasKey(key string) bool {
|
|
for _, env := range el {
|
|
if strings.HasPrefix(env, key+"=") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Add adds a key=value entry to the environment
|
|
// list
|
|
func (el *EnvList) Add(key, value string) {
|
|
*el = append(*el, key+"="+value)
|
|
}
|
|
|
|
// Decode implements envconfig.Decoder. Please see the below link for more information on
|
|
// that interface:
|
|
//
|
|
// https://github.com/kelseyhightower/envconfig#custom-decoders
|
|
//
|
|
// We are doing this to allow for very long lists of assignments to be set inside of
|
|
// a single environment variable. For example:
|
|
//
|
|
// ATHENS_GO_BINARY_ENV_VARS="GOPRIVATE=*.corp.example.com,rsc.io/private; GOPROXY=direct"
|
|
//
|
|
// See the below link for more information:
|
|
// https://github.com/gomods/athens/issues/1404
|
|
func (el *EnvList) Decode(value string) error {
|
|
const op errors.Op = "envList.Decode"
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
*el = EnvList{} // env vars must override config file
|
|
assignments := strings.Split(value, ";")
|
|
for _, assignment := range assignments {
|
|
*el = append(*el, strings.TrimSpace(assignment))
|
|
}
|
|
return el.Validate()
|
|
}
|
|
|
|
// Validate validates that all strings inside the
|
|
// list are of the key=value format
|
|
func (el EnvList) Validate() error {
|
|
const op errors.Op = "EnvList.Validate"
|
|
for _, env := range el {
|
|
// some strings can have multiple "=", such as GODEBUG=netdns=cgo
|
|
if strings.Count(env, "=") < 1 {
|
|
return errors.E(op, fmt.Errorf("incorrect env format: %v", env))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load loads the config from a file.
|
|
// If file is not present returns default config
|
|
func Load(configFile string) (*Config, error) {
|
|
// User explicitly specified a config file
|
|
if configFile != "" {
|
|
return ParseConfigFile(configFile)
|
|
}
|
|
|
|
// There is a config in the current directory
|
|
if fi, err := os.Stat(defaultConfigFile); err == nil {
|
|
return ParseConfigFile(fi.Name())
|
|
}
|
|
|
|
// Use default values
|
|
log.Println("Running dev mode with default settings, consult config when you're ready to run in production")
|
|
cfg := defaultConfig()
|
|
return cfg, envOverride(cfg)
|
|
}
|
|
|
|
func defaultConfig() *Config {
|
|
return &Config{
|
|
GoBinary: "go",
|
|
GoBinaryEnvVars: EnvList{"GOPROXY=direct"},
|
|
GoEnv: "development",
|
|
GoProxy: "direct",
|
|
GoGetWorkers: 10,
|
|
ProtocolWorkers: 30,
|
|
LogLevel: "debug",
|
|
CloudRuntime: "none",
|
|
EnablePprof: false,
|
|
PprofPort: ":3001",
|
|
StatsExporter: "prometheus",
|
|
TimeoutConf: TimeoutConf{Timeout: 300},
|
|
StorageType: "memory",
|
|
Port: ":3000",
|
|
SingleFlightType: "memory",
|
|
GlobalEndpoint: "http://localhost:3001",
|
|
TraceExporterURL: "http://localhost:14268",
|
|
SumDBs: []string{"https://sum.golang.org"},
|
|
NoSumPatterns: []string{},
|
|
DownloadMode: "sync",
|
|
DownloadURL: "",
|
|
NetworkMode: "strict",
|
|
RobotsFile: "robots.txt",
|
|
IndexType: "none",
|
|
SingleFlight: &SingleFlight{
|
|
Etcd: &Etcd{"localhost:2379,localhost:22379,localhost:32379"},
|
|
Redis: &Redis{"127.0.0.1:6379", ""},
|
|
RedisSentinel: &RedisSentinel{
|
|
Endpoints: []string{"127.0.0.1:26379"},
|
|
MasterName: "redis-1",
|
|
SentinelPassword: "sekret",
|
|
},
|
|
},
|
|
Index: &Index{
|
|
MySQL: &MySQL{
|
|
Protocol: "tcp",
|
|
Host: "localhost",
|
|
Port: 3306,
|
|
User: "root",
|
|
Password: "",
|
|
Database: "athens",
|
|
Params: map[string]string{
|
|
"parseTime": "true",
|
|
"timeout": "30s",
|
|
},
|
|
},
|
|
Postgres: &Postgres{
|
|
Host: "localhost",
|
|
Port: 5432,
|
|
User: "postgres",
|
|
Password: "",
|
|
Database: "athens",
|
|
Params: map[string]string{
|
|
"connect_timeout": "30",
|
|
"sslmode": "disable",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// BasicAuth returns BasicAuthUser and BasicAuthPassword
|
|
// and ok if neither of them are empty
|
|
func (c *Config) BasicAuth() (user, pass string, ok bool) {
|
|
user = c.BasicAuthUser
|
|
pass = c.BasicAuthPass
|
|
ok = user != "" && pass != ""
|
|
return user, pass, ok
|
|
}
|
|
|
|
// TLSCertFiles returns certificate and key files and an error if
|
|
// both files doesn't exist and have approperiate file permissions
|
|
func (c *Config) TLSCertFiles() (cert, key string, err error) {
|
|
if c.TLSCertFile == "" && c.TLSKeyFile == "" {
|
|
return "", "", nil
|
|
}
|
|
|
|
certFile, err := os.Stat(c.TLSCertFile)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("Could not access TLSCertFile: %v", err)
|
|
}
|
|
|
|
keyFile, err := os.Stat(c.TLSKeyFile)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("Could not access TLSKeyFile: %v", err)
|
|
}
|
|
|
|
if keyFile.Mode()&077 != 0 && runtime.GOOS != "windows" {
|
|
return "", "", fmt.Errorf("TLSKeyFile should not be accessible by others")
|
|
}
|
|
|
|
return certFile.Name(), keyFile.Name(), nil
|
|
}
|
|
|
|
// FilterOff returns true if the FilterFile is empty
|
|
func (c *Config) FilterOff() bool {
|
|
return c.FilterFile == ""
|
|
}
|
|
|
|
// ParseConfigFile parses the given file into an athens config struct
|
|
func ParseConfigFile(configFile string) (*Config, error) {
|
|
|
|
var config Config
|
|
// attempt to read the given config file
|
|
if _, err := toml.DecodeFile(configFile, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// override values with environment variables if specified
|
|
if err := envOverride(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check file perms from config
|
|
if config.GoEnv == "production" {
|
|
if err := checkFilePerms(configFile, config.FilterFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// validate all required fields have been populated
|
|
if err := validateConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
// envOverride uses Environment variables to override unspecified properties
|
|
func envOverride(config *Config) error {
|
|
const defaultPort = ":3000"
|
|
err := envconfig.Process("athens", config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
portEnv := os.Getenv("PORT")
|
|
// ATHENS_PORT takes precedence over PORT
|
|
if portEnv != "" && os.Getenv("ATHENS_PORT") == "" {
|
|
config.Port = portEnv
|
|
}
|
|
if config.Port == "" {
|
|
config.Port = defaultPort
|
|
}
|
|
config.Port = ensurePortFormat(config.Port)
|
|
return nil
|
|
}
|
|
|
|
func ensurePortFormat(s string) string {
|
|
if _, err := strconv.Atoi(s); err == nil {
|
|
return ":" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
func validateConfig(config Config) error {
|
|
validate := validator.New()
|
|
err := validate.StructExcept(config, "Storage", "Index")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = validateStorage(validate, config.StorageType, config.Storage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = validateIndex(validate, config.IndexType, config.Index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateStorage(validate *validator.Validate, storageType string, config *Storage) error {
|
|
switch storageType {
|
|
case "memory":
|
|
return nil
|
|
case "mongo":
|
|
return validate.Struct(config.Mongo)
|
|
case "disk":
|
|
return validate.Struct(config.Disk)
|
|
case "minio":
|
|
return validate.Struct(config.Minio)
|
|
case "gcp":
|
|
return validate.Struct(config.GCP)
|
|
case "s3":
|
|
return validate.Struct(config.S3)
|
|
case "azureblob":
|
|
return validate.Struct(config.AzureBlob)
|
|
case "external":
|
|
return validate.Struct(config.External)
|
|
default:
|
|
return fmt.Errorf("storage type %q is unknown", storageType)
|
|
}
|
|
}
|
|
|
|
func validateIndex(validate *validator.Validate, indexType string, config *Index) error {
|
|
switch indexType {
|
|
case "", "none", "memory":
|
|
return nil
|
|
case "mysql":
|
|
return validate.Struct(config.MySQL)
|
|
case "postgres":
|
|
return validate.Struct(config.Postgres)
|
|
default:
|
|
return fmt.Errorf("index type %q is unknown", indexType)
|
|
}
|
|
}
|
|
|
|
// GetConf accepts the path to a file, constructs an absolute path to the file,
|
|
// and attempts to parse it into a Config struct.
|
|
func GetConf(path string) (*Config, error) {
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to construct absolute path to test config file")
|
|
}
|
|
conf, err := ParseConfigFile(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to parse test config file: %s", err.Error())
|
|
}
|
|
return conf, nil
|
|
}
|
|
|
|
// checkFilePerms given a list of files
|
|
func checkFilePerms(files ...string) error {
|
|
const op = "config.checkFilePerms"
|
|
|
|
for _, f := range files {
|
|
if f == "" {
|
|
continue
|
|
}
|
|
|
|
// TODO: Do not ignore errors when a file is not found
|
|
// There is a subtle bug in the filter module which ignores the filter file if it does not find it.
|
|
// This check can be removed once that has been fixed
|
|
fInfo, err := os.Stat(f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Assume unix based system (MacOS and Linux)
|
|
// the bit mask is calculated using the umask command which tells which permissions
|
|
// should not be allowed for a particular user, group or world
|
|
if fInfo.Mode()&0077 != 0 && runtime.GOOS != "windows" {
|
|
return errors.E(op, f+" should have at most rwx,-, - (bit mask 077) as permission")
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|