Merge pull request #1472 from ansible-semaphore/config-validation

Config validation
This commit is contained in:
Denis Gukov 2023-09-14 20:18:48 +02:00 committed by GitHub
commit 9af6aa504f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 607 additions and 130 deletions

View File

@ -198,7 +198,7 @@ func (key *AccessKey) SerializeSecret() error {
return fmt.Errorf("invalid access token type")
}
encryptionString := util.Config.GetAccessKeyEncryption()
encryptionString := util.Config.AccessKeyEncryption
if encryptionString == "" {
secret := base64.StdEncoding.EncodeToString(plaintext)
@ -258,7 +258,7 @@ func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
//}
func (key *AccessKey) DeserializeSecret() error {
return key.DeserializeSecret2(util.Config.GetAccessKeyEncryption())
return key.DeserializeSecret2(util.Config.AccessKeyEncryption)
}
func (key *AccessKey) DeserializeSecret2(encryptionString string) error {

View File

@ -150,7 +150,7 @@ func connect() (*sql.DB, error) {
return nil, err
}
dialect := cfg.Dialect.String()
dialect := cfg.Dialect
return sql.Open(dialect, connectionString)
}
@ -169,7 +169,7 @@ func createDb() error {
return err
}
conn, err := sql.Open(cfg.Dialect.String(), connectionString)
conn, err := sql.Open(cfg.Dialect, connectionString)
if err != nil {
return err
}

4
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
go.etcd.io/bbolt v1.3.2
golang.org/x/crypto v0.3.0
golang.org/x/oauth2 v0.7.0
@ -32,6 +33,7 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
@ -47,6 +49,7 @@ require (
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
@ -58,4 +61,5 @@ require (
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)

7
go.sum
View File

@ -168,7 +168,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -449,8 +448,6 @@ golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5o
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
@ -535,14 +532,11 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
@ -555,7 +549,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -6,15 +6,18 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/google/go-github/github"
"io"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/google/go-github/github"
"github.com/gorilla/securecookie"
)
@ -24,21 +27,19 @@ var Cookie *securecookie.SecureCookie
// WebHostURL is the public route to the semaphore server
var WebHostURL *url.URL
type DbDriver string
const (
DbDriverMySQL DbDriver = "mysql"
DbDriverBolt DbDriver = "bolt"
DbDriverPostgres DbDriver = "postgres"
DbDriverMySQL = "mysql"
DbDriverBolt = "bolt"
DbDriverPostgres = "postgres"
)
type DbConfig struct {
Dialect DbDriver `json:"-"`
Dialect string `json:"-"`
Hostname string `json:"host"`
Username string `json:"user"`
Password string `json:"pass"`
DbName string `json:"name"`
Hostname string `json:"host" env:"SEMAPHORE_DB_HOST"`
Username string `json:"user" env:"SEMAPHORE_DB_USER"`
Password string `json:"pass" env:"SEMAPHORE_DB_PASS"`
DbName string `json:"name" env:"SEMAPHORE_DB"`
Options map[string]string `json:"options"`
}
@ -71,23 +72,32 @@ type oidcProvider struct {
EmailClaim string `json:"email_claim"`
}
type GitClientId string
const (
// GoGitClientId is builtin Git client. It is not require external dependencies and is preferred.
// Use it if you don't need external SSH authorization.
GoGitClientId GitClientId = "go_git"
GoGitClientId = "go_git"
// CmdGitClientId is external Git client.
// Default Git client. It is use external Git binary to clone repositories.
CmdGitClientId GitClientId = "cmd_git"
CmdGitClientId = "cmd_git"
)
// // basic config validation using regex
// /* NOTE: other basic regex could be used:
//
// ipv4: ^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$
// ipv6: ^(?:[A-Fa-f0-9]{1,4}:|:){3,7}[A-Fa-f0-9]{1,4}$
// domain: ^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$
// path+filename: ^([\\/[a-zA-Z0-9_\\-${}:~]*]*\\/)?[a-zA-Z0-9\\.~_${}\\-:]*$
// email address: ^(|.*@[A-Za-z0-9-\\.]*)$
//
// */
type RunnerSettings struct {
ApiURL string `json:"api_url"`
RegistrationToken string `json:"registration_token"`
ConfigFile string `json:"config_file"`
ApiURL string `json:"api_url" env:"SEMAPHORE_RUNNER_API_URL"`
RegistrationToken string `json:"registration_token" env:"SEMAPHORE_RUNNER_REGISTRATION_TOKEN"`
ConfigFile string `json:"config_file" env:"SEMAPHORE_RUNNER_CONFIG_FILE"`
// OneOff indicates than runner runs only one job and exit
OneOff bool `json:"one_off"`
OneOff bool `json:"one_off" env:"SEMAPHORE_RUNNER_ONE_OFF"`
}
// ConfigType mapping between Config and the json file that sets it
@ -96,75 +106,74 @@ type ConfigType struct {
BoltDb DbConfig `json:"bolt"`
Postgres DbConfig `json:"postgres"`
Dialect DbDriver `json:"dialect"`
Dialect string `json:"dialect" rule:"^mysql|bolt|postgres$" env:"SEMAPHORE_DB_DIALECT"`
// Format `:port_num` eg, :3000
// if : is missing it will be corrected
Port string `json:"port"`
Port string `json:"port" default:":3000" rule:"^:([0-9]{1,5})$" env:"SEMAPHORE_PORT"`
// Interface ip, put in front of the port.
// defaults to empty
Interface string `json:"interface"`
Interface string `json:"interface" env:"SEMAPHORE_INTERFACE"`
// semaphore stores ephemeral projects here
TmpPath string `json:"tmp_path"`
TmpPath string `json:"tmp_path" default:"/tmp/semaphore" env:"SEMAPHORE_TMP_PATH"`
// SshConfigPath is a path to the custom SSH config file.
// Default path is ~/.ssh/config.
SshConfigPath string `json:"ssh_config_path"`
SshConfigPath string `json:"ssh_config_path" env:"SEMAPHORE_TMP_PATH"`
GitClientId GitClientId `json:"git_client"`
GitClientId string `json:"git_client" rule:"^go_git|cmd_git$" env:"SEMAPHORE_GIT_CLIENT" default:"cmd_git"`
// web host
WebHost string `json:"web_host"`
WebHost string `json:"web_host" env:"SEMAPHORE_WEB_ROOT"`
// cookie hashing & encryption
CookieHash string `json:"cookie_hash"`
CookieEncryption string `json:"cookie_encryption"`
CookieHash string `json:"cookie_hash" env:"SEMAPHORE_COOKIE_HASH"`
CookieEncryption string `json:"cookie_encryption" env:"SEMAPHORE_COOKIE_ENCRYPTION"`
// AccessKeyEncryption is BASE64 encoded byte array used
// for encrypting and decrypting access keys stored in database.
// Do not use it! Use method GetAccessKeyEncryption instead of it.
AccessKeyEncryption string `json:"access_key_encryption"`
AccessKeyEncryption string `json:"access_key_encryption" env:"SEMAPHORE_ACCESS_KEY_ENCRYPTION"`
// email alerting
EmailAlert bool `json:"email_alert"`
EmailSender string `json:"email_sender"`
EmailHost string `json:"email_host"`
EmailPort string `json:"email_port"`
EmailUsername string `json:"email_username"`
EmailPassword string `json:"email_password"`
EmailSecure bool `json:"email_secure"`
EmailAlert bool `json:"email_alert" env:"SEMAPHORE_EMAIL_ALERT"`
EmailSender string `json:"email_sender" env:"SEMAPHORE_EMAIL_SENDER"`
EmailHost string `json:"email_host" env:"SEMAPHORE_EMAIL_HOST"`
EmailPort string `json:"email_port" rule:"^(|[0-9]{1,5})$" env:"SEMAPHORE_EMAIL_PORT"`
EmailUsername string `json:"email_username" env:"SEMAPHORE_EMAIL_USERNAME"`
EmailPassword string `json:"email_password" env:"SEMAPHORE_EMAIL_PASSWORD"`
EmailSecure bool `json:"email_secure" env:"SEMAPHORE_EMAIL_SECURE"`
// ldap settings
LdapEnable bool `json:"ldap_enable"`
LdapBindDN string `json:"ldap_binddn"`
LdapBindPassword string `json:"ldap_bindpassword"`
LdapServer string `json:"ldap_server"`
LdapSearchDN string `json:"ldap_searchdn"`
LdapSearchFilter string `json:"ldap_searchfilter"`
LdapEnable bool `json:"ldap_enable" env:"SEMAPHORE_LDAP_ENABLE"`
LdapBindDN string `json:"ldap_binddn" env:"SEMAPHORE_LDAP_BIND_DN"`
LdapBindPassword string `json:"ldap_bindpassword" env:"SEMAPHORE_LDAP_BIND_PASSWORD"`
LdapServer string `json:"ldap_server" env:"SEMAPHORE_LDAP_SERVER"`
LdapSearchDN string `json:"ldap_searchdn" env:"SEMAPHORE_LDAP_SEARCH_DN"`
LdapSearchFilter string `json:"ldap_searchfilter" env:"SEMAPHORE_LDAP_SEARCH_FILTER"`
LdapMappings ldapMappings `json:"ldap_mappings"`
LdapNeedTLS bool `json:"ldap_needtls"`
LdapNeedTLS bool `json:"ldap_needtls" env:"SEMAPHORE_LDAP_NEEDTLS"`
// telegram and slack alerting
TelegramAlert bool `json:"telegram_alert"`
TelegramChat string `json:"telegram_chat"`
TelegramToken string `json:"telegram_token"`
SlackAlert bool `json:"slack_alert"`
SlackUrl string `json:"slack_url"`
TelegramAlert bool `json:"telegram_alert" env:"SEMAPHORE_TELEGRAM_ALERT"`
TelegramChat string `json:"telegram_chat" env:"SEMAPHORE_TELEGRAM_CHAT"`
TelegramToken string `json:"telegram_token" env:"SEMAPHORE_TELEGRAM_TOKEN"`
SlackAlert bool `json:"slack_alert" env:"SEMAPHORE_SLACK_ALERT"`
SlackUrl string `json:"slack_url" env:"SEMAPHORE_SLACK_URL"`
// oidc settings
OidcProviders map[string]oidcProvider `json:"oidc_providers"`
// task concurrency
MaxParallelTasks int `json:"max_parallel_tasks"`
MaxParallelTasks int `json:"max_parallel_tasks" rule:"^[0-9]{1,10}$" env:"SEMAPHORE_MAX_PARALLEL_TASKS"`
RunnerRegistrationToken string `json:"runner_registration_token"`
RunnerRegistrationToken string `json:"runner_registration_token" env:"SEMAPHORE_RUNNER_REGISTRATION_TOKEN"`
// feature switches
PasswordLoginDisable bool `json:"password_login_disable"`
NonAdminCanCreateProject bool `json:"non_admin_can_create_project"`
PasswordLoginDisable bool `json:"password_login_disable" env:"SEMAPHORE_PASSWORD_LOGIN_DISABLED"`
NonAdminCanCreateProject bool `json:"non_admin_can_create_project" env:"SEMAPHORE_NON_ADMIN_CAN_CREATE_PROJECT"`
UseRemoteRunner bool `json:"use_remote_runner"`
UseRemoteRunner bool `json:"use_remote_runner" env:"SEMAPHORE_USE_REMOTE_RUNNER"`
Runner RunnerSettings `json:"runner"`
}
@ -177,19 +186,14 @@ func (conf *ConfigType) ToJSON() ([]byte, error) {
return json.MarshalIndent(&conf, " ", "\t")
}
func (conf *ConfigType) GetAccessKeyEncryption() string {
ret := os.Getenv("SEMAPHORE_ACCESS_KEY_ENCRYPTION")
if ret == "" {
ret = conf.AccessKeyEncryption
}
return ret
}
// ConfigInit reads in cli flags, and switches actions appropriately on them
func ConfigInit(configPath string) {
loadConfig(configPath)
fmt.Println("Loading config")
loadConfigFile(configPath)
loadConfigEnvironment()
loadConfigDefaults()
fmt.Println("Validating config")
validateConfig()
var encryption []byte
@ -206,7 +210,7 @@ func ConfigInit(configPath string) {
}
}
func loadConfig(configPath string) {
func loadConfigFile(configPath string) {
if configPath == "" {
configPath = os.Getenv("SEMAPHORE_CONFIG_PATH")
}
@ -216,7 +220,7 @@ func loadConfig(configPath string) {
if configPath == "" {
cwd, err := os.Getwd()
exitOnConfigError(err)
exitOnConfigFileError(err)
paths := []string{
path.Join(cwd, "config.json"),
"/usr/local/etc/semaphore/config.json",
@ -234,46 +238,215 @@ func loadConfig(configPath string) {
decodeConfig(file)
break
}
exitOnConfigError(err)
exitOnConfigFileError(err)
} else {
p := configPath
file, err := os.Open(p)
exitOnConfigError(err)
exitOnConfigFileError(err)
decodeConfig(file)
}
}
func loadDefaultsToObject(obj interface{}) error {
var t = reflect.TypeOf(obj)
var v = reflect.ValueOf(obj)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = reflect.Indirect(v)
}
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
fieldValue := v.Field(i)
if fieldType.Type.Kind() == reflect.Struct {
err := loadDefaultsToObject(fieldValue.Addr())
if err != nil {
return err
}
continue
}
defaultVar := fieldType.Tag.Get("default")
if defaultVar == "" {
continue
}
setConfigValue(fieldValue, defaultVar)
}
return nil
}
func loadConfigDefaults() {
err := loadDefaultsToObject(Config)
if err != nil {
panic(err)
}
}
func castStringToInt(value string) int {
valueInt, err := strconv.Atoi(value)
if err != nil {
panic(err)
}
return valueInt
}
func castStringToBool(value string) bool {
var valueBool bool
if value == "1" || strings.ToLower(value) == "true" {
valueBool = true
} else {
valueBool = false
}
return valueBool
}
func setConfigValue(attribute reflect.Value, value interface{}) {
if attribute.IsValid() {
switch attribute.Kind() {
case reflect.Int:
if reflect.ValueOf(value).Kind() != reflect.Int {
value = castStringToInt(fmt.Sprintf("%v", reflect.ValueOf(value)))
}
case reflect.Bool:
if reflect.ValueOf(value).Kind() != reflect.Bool {
value = castStringToBool(fmt.Sprintf("%v", reflect.ValueOf(value)))
}
}
attribute.Set(reflect.ValueOf(value))
} else {
panic(fmt.Errorf("got non-existent config attribute"))
}
}
func getConfigValue(path string) string {
attribute := reflect.ValueOf(Config)
nested_path := strings.Split(path, ".")
for i, nested := range nested_path {
attribute = reflect.Indirect(attribute).FieldByName(nested)
lastDepth := len(nested_path) == i+1
if !lastDepth && attribute.Kind() != reflect.Struct || lastDepth && attribute.Kind() == reflect.Invalid {
panic(fmt.Errorf("got non-existent config attribute '%v'", path))
}
}
return fmt.Sprintf("%v", attribute)
}
func validate(value interface{}) error {
var t = reflect.TypeOf(value)
var v = reflect.ValueOf(value)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = reflect.Indirect(v)
}
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
fieldValue := v.Field(i)
rule := fieldType.Tag.Get("rule")
if rule == "" {
continue
}
var value string
if fieldType.Type.Kind() == reflect.Int {
value = strconv.FormatInt(fieldValue.Int(), 10)
} else if fieldType.Type.Kind() == reflect.Uint {
value = strconv.FormatUint(fieldValue.Uint(), 10)
} else {
value = fieldValue.String()
}
match, _ := regexp.MatchString(rule, value)
if !match {
return fmt.Errorf(
"value of field '%v' is not valid! (Must match regex: '%v')",
fieldType.Name, rule,
)
}
}
return nil
}
func validateConfig() {
validatePort()
err := validate(Config)
if len(Config.TmpPath) == 0 {
Config.TmpPath = "/tmp/semaphore"
}
if Config.MaxParallelTasks < 1 {
Config.MaxParallelTasks = 10
}
}
func validatePort() {
//TODO - why do we do this only with this variable?
if len(os.Getenv("PORT")) > 0 {
Config.Port = ":" + os.Getenv("PORT")
}
if len(Config.Port) == 0 {
Config.Port = ":3000"
}
if !strings.HasPrefix(Config.Port, ":") {
Config.Port = ":" + Config.Port
}
}
func exitOnConfigError(err error) {
if err != nil {
fmt.Println("Cannot Find configuration! Use --config parameter to point to a JSON file generated by `semaphore setup`.")
os.Exit(1)
panic(err)
}
}
func loadEnvironmentToObject(obj interface{}) error {
var t = reflect.TypeOf(obj)
var v = reflect.ValueOf(obj)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = reflect.Indirect(v)
}
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
fieldValue := v.Field(i)
if fieldType.Type.Kind() == reflect.Struct {
err := loadEnvironmentToObject(fieldValue.Addr().Interface())
if err != nil {
return err
}
continue
}
envVar := fieldType.Tag.Get("env")
if envVar == "" {
continue
}
envValue, exists := os.LookupEnv(envVar)
if !exists {
continue
}
setConfigValue(fieldValue, envValue)
}
return nil
}
func loadConfigEnvironment() {
err := loadEnvironmentToObject(Config)
if err != nil {
panic(err)
}
}
func exitOnConfigError(msg string) {
fmt.Println(msg)
os.Exit(1)
}
func exitOnConfigFileError(err error) {
if err != nil {
exitOnConfigError("Cannot Find configuration! Use --config parameter to point to a JSON file generated by `semaphore setup`.")
}
}
@ -335,11 +508,6 @@ func CheckUpdate() (updateAvailable *github.RepositoryRelease, err error) {
return
}
// String returns dialect name for GORP.
func (d DbDriver) String() string {
return string(d)
}
func (d *DbConfig) IsPresent() bool {
return d.GetHostname() != ""
}
@ -451,7 +619,7 @@ func (conf *ConfigType) PrintDbInfo() {
}
}
func (conf *ConfigType) GetDialect() (dialect DbDriver, err error) {
func (conf *ConfigType) GetDialect() (dialect string, err error) {
if conf.Dialect == "" {
switch {
case conf.MySQL.IsPresent():
@ -471,7 +639,7 @@ func (conf *ConfigType) GetDialect() (dialect DbDriver, err error) {
}
func (conf *ConfigType) GetDBConfig() (dbConfig DbConfig, err error) {
var dialect DbDriver
var dialect string
dialect, err = conf.GetDialect()
if err != nil {

View File

@ -1,28 +1,340 @@
package util
import (
"fmt"
"os"
"reflect"
"testing"
)
func TestValidatePort(t *testing.T) {
func mockError(msg string) {
panic(msg)
}
Config = new(ConfigType)
Config.Port = ""
validatePort()
if Config.Port != ":3000" {
t.Error("no port should get set to default")
func TestValidate(t *testing.T) {
var val struct {
Test string `rule:"^\\d+$"`
}
val.Test = "45243524"
Config.Port = "4000"
validatePort()
if Config.Port != ":4000" {
t.Error("Port without : suffix should have it added")
}
os.Setenv("PORT", "5000")
validatePort()
if Config.Port != ":5000" {
t.Error("Port value should be overwritten by env var, and it should be prefixed appropriately")
err := validate(val)
if err != nil {
t.Error(err)
}
}
func TestLoadEnvironmentToObject(t *testing.T) {
var val struct {
Test string `env:"TEST_ENV_VAR"`
Subfield struct {
Value string `env:"TEST_VALUE_ENV_VAR"`
}
}
err := os.Setenv("TEST_ENV_VAR", "758478")
if err != nil {
panic(err)
}
err = os.Setenv("TEST_VALUE_ENV_VAR", "test_value")
if err != nil {
panic(err)
}
err = loadEnvironmentToObject(&val)
if err != nil {
t.Error(err)
}
if val.Test != "758478" {
t.Error("Invalid value")
}
if val.Subfield.Value != "test_value" {
t.Error("Invalid value")
}
}
func TestCastStringToInt(t *testing.T) {
var errMsg string = "Cast string => int failed"
if castStringToInt("5") != 5 {
t.Error(errMsg)
}
if castStringToInt("0") != 0 {
t.Error(errMsg)
}
if castStringToInt("-1") != -1 {
t.Error(errMsg)
}
if castStringToInt("999") != 999 {
t.Error(errMsg)
}
defer func() {
if r := recover(); r == nil {
t.Errorf("Cast string => int did not panic on invalid input")
}
}()
castStringToInt("xxx")
}
func TestCastStringToBool(t *testing.T) {
var errMsg string = "Cast string => bool failed"
if castStringToBool("1") != true {
t.Error(errMsg)
}
if castStringToBool("0") != false {
t.Error(errMsg)
}
if castStringToBool("true") != true {
t.Error(errMsg)
}
if castStringToBool("false") != false {
t.Error(errMsg)
}
if castStringToBool("xxx") != false {
t.Error(errMsg)
}
if castStringToBool("") != false {
t.Error(errMsg)
}
}
func TestGetConfigValue(t *testing.T) {
Config = new(ConfigType)
var testPort string = "1337"
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 5
var testLdapNeedTls bool = true
var testDbHost string = "192.168.0.1"
Config.Port = testPort
Config.CookieHash = testCookieHash
Config.MaxParallelTasks = testMaxParallelTasks
Config.LdapNeedTLS = testLdapNeedTls
Config.BoltDb.Hostname = testDbHost
if getConfigValue("Port") != testPort {
t.Error("Could not get value for config attribute 'Port'!")
}
if getConfigValue("CookieHash") != testCookieHash {
t.Error("Could not get value for config attribute 'CookieHash'!")
}
if getConfigValue("MaxParallelTasks") != fmt.Sprintf("%v", testMaxParallelTasks) {
t.Error("Could not get value for config attribute 'MaxParallelTasks'!")
}
if getConfigValue("LdapNeedTLS") != fmt.Sprintf("%v", testLdapNeedTls) {
t.Error("Could not get value for config attribute 'LdapNeedTLS'!")
}
if getConfigValue("BoltDb.Hostname") != fmt.Sprintf("%v", testDbHost) {
t.Error("Could not get value for config attribute 'BoltDb.Hostname'!")
}
defer func() {
if r := recover(); r == nil {
t.Error("Did not fail on non-existent config attribute!")
}
}()
getConfigValue("NotExistent")
defer func() {
if r := recover(); r == nil {
t.Error("Did not fail on non-existent config attribute!")
}
}()
getConfigValue("Not.Existent")
}
func TestSetConfigValue(t *testing.T) {
Config = new(ConfigType)
configValue := reflect.ValueOf(Config).Elem()
var testPort string = "1337"
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 5
var testLdapNeedTls bool = true
//var testDbHost string = "192.168.0.1"
var testEmailSecure string = "1"
var expectEmailSecure bool = true
setConfigValue(configValue.FieldByName("Port"), testPort)
setConfigValue(configValue.FieldByName("CookieHash"), testCookieHash)
setConfigValue(configValue.FieldByName("MaxParallelTasks"), testMaxParallelTasks)
setConfigValue(configValue.FieldByName("LdapNeedTLS"), testLdapNeedTls)
//setConfigValue(configValue.FieldByName("BoltDb.Hostname"), testDbHost)
setConfigValue(configValue.FieldByName("EmailSecure"), testEmailSecure)
if Config.Port != testPort {
t.Error("Could not set value for config attribute 'Port'!")
}
if Config.CookieHash != testCookieHash {
t.Error("Could not set value for config attribute 'CookieHash'!")
}
if Config.MaxParallelTasks != testMaxParallelTasks {
t.Error("Could not set value for config attribute 'MaxParallelTasks'!")
}
if Config.LdapNeedTLS != testLdapNeedTls {
t.Error("Could not set value for config attribute 'LdapNeedTls'!")
}
//if Config.BoltDb.Hostname != testDbHost {
// t.Error("Could not set value for config attribute 'BoltDb.Hostname'!")
//}
if Config.EmailSecure != expectEmailSecure {
t.Error("Could not set value for config attribute 'EmailSecure'!")
}
defer func() {
if r := recover(); r == nil {
t.Error("Did not fail on non-existent config attribute!")
}
}()
setConfigValue(configValue.FieldByName("NotExistent"), "someValue")
defer func() {
if r := recover(); r == nil {
t.Error("Did not fail on non-existent config attribute!")
}
}()
//setConfigValue(configValue.FieldByName("Not.Existent"), "someValue")
}
func TestLoadConfigEnvironmet(t *testing.T) {
Config = new(ConfigType)
Config.Dialect = DbDriverBolt
var envPort string = "1337"
var envCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var envAccessKeyEncryption string = "1/wRYXQltDGwbzNZRP9ZfJb2IoWcn1hYrxA0vOdvVos="
var envMaxParallelTasks string = "5"
var expectMaxParallelTasks int = 5
var expectLdapNeedTls bool = true
var envLdapNeedTls string = "1"
var envDbHost string = "192.168.0.1"
os.Setenv("SEMAPHORE_PORT", envPort)
os.Setenv("SEMAPHORE_COOKIE_HASH", envCookieHash)
os.Setenv("SEMAPHORE_ACCESS_KEY_ENCRYPTION", envAccessKeyEncryption)
os.Setenv("SEMAPHORE_MAX_PARALLEL_TASKS", envMaxParallelTasks)
os.Setenv("SEMAPHORE_LDAP_NEEDTLS", envLdapNeedTls)
os.Setenv("SEMAPHORE_DB_HOST", envDbHost)
loadConfigEnvironment()
if Config.Port != envPort {
t.Error("Setting 'Port' was not loaded from environment-vars!")
}
if Config.CookieHash != envCookieHash {
t.Error("Setting 'CookieHash' was not loaded from environment-vars!")
}
if Config.AccessKeyEncryption != envAccessKeyEncryption {
t.Error("Setting 'AccessKeyEncryption' was not loaded from environment-vars!")
}
if Config.MaxParallelTasks != expectMaxParallelTasks {
t.Error("Setting 'MaxParallelTasks' was not loaded from environment-vars!")
}
if Config.LdapNeedTLS != expectLdapNeedTls {
t.Error("Setting 'LdapNeedTLS' was not loaded from environment-vars!")
}
if Config.BoltDb.Hostname != envDbHost {
t.Error("Setting 'BoltDb.Hostname' was not loaded from environment-vars!")
}
//if Config.MySQL.Hostname == envDbHost || Config.Postgres.Hostname == envDbHost {
// // inactive db-dialects could be set as they share the same env-vars; but should be ignored
// t.Error("DB-Hostname was loaded for inactive DB-dialects!")
//}
}
func TestLoadConfigDefaults(t *testing.T) {
Config = new(ConfigType)
var errMsg string = "Failed to load config-default"
loadConfigDefaults()
if Config.Port != ":3000" {
t.Error(errMsg)
}
if Config.TmpPath != "/tmp/semaphore" {
t.Error(errMsg)
}
}
func ensureConfigValidationFailure(t *testing.T, attribute string, value interface{}) {
defer func() {
if r := recover(); r == nil {
t.Errorf(
"Config validation for attribute '%v' did not fail! (value '%v')",
attribute, value,
)
}
}()
validateConfig()
}
func TestValidateConfig(t *testing.T) {
//assert := assert.New(t)
Config = new(ConfigType)
var testPort string = ":3000"
var testDbDialect = DbDriverBolt
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 0
Config.Port = testPort
Config.Dialect = testDbDialect
Config.CookieHash = testCookieHash
Config.MaxParallelTasks = testMaxParallelTasks
Config.GitClientId = GoGitClientId
Config.CookieEncryption = testCookieHash
Config.AccessKeyEncryption = testCookieHash
validateConfig()
Config.Port = "INVALID"
ensureConfigValidationFailure(t, "Port", Config.Port)
Config.Port = ":100000"
ensureConfigValidationFailure(t, "Port", Config.Port)
Config.Port = testPort
Config.MaxParallelTasks = -1
ensureConfigValidationFailure(t, "MaxParallelTasks", Config.MaxParallelTasks)
ensureConfigValidationFailure(t, "MaxParallelTasks", Config.MaxParallelTasks)
Config.MaxParallelTasks = testMaxParallelTasks
//Config.CookieHash = "\"0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ=\"" // invalid with quotes (can happen when supplied as env-var)
//ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash)
//Config.CookieHash = "!)394340"
//ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash)
//Config.CookieHash = ""
//ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash)
//Config.CookieHash = "TQwjDZ5fIQtaIw==" // valid b64, but too small
//ensureConfigValidationFailure(t, "CookieHash", Config.CookieHash)
Config.CookieHash = testCookieHash
Config.Dialect = "someOtherDB"
ensureConfigValidationFailure(t, "Dialect", Config.Dialect)
Config.Dialect = testDbDialect
}