2016-01-05 00:32:53 +01:00
|
|
|
package util
|
|
|
|
|
|
|
|
import (
|
2016-04-30 14:28:47 +02:00
|
|
|
"encoding/base64"
|
2016-01-05 00:32:53 +01:00
|
|
|
"encoding/json"
|
2020-11-28 22:49:44 +01:00
|
|
|
"errors"
|
2016-01-05 00:32:53 +01:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2021-07-13 08:27:22 +02:00
|
|
|
"net/url"
|
2016-03-16 22:49:43 +01:00
|
|
|
"os"
|
2016-04-24 20:11:43 +02:00
|
|
|
"path"
|
2016-03-16 22:49:43 +01:00
|
|
|
|
2019-07-09 19:49:17 +02:00
|
|
|
"io"
|
|
|
|
"strings"
|
|
|
|
|
2016-04-30 14:28:47 +02:00
|
|
|
"github.com/gorilla/securecookie"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
2016-01-05 00:32:53 +01:00
|
|
|
)
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// Cookie is a runtime generated secure cookie used for authentication
|
2016-04-30 14:28:47 +02:00
|
|
|
var Cookie *securecookie.SecureCookie
|
2019-07-09 19:49:17 +02:00
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// Migration indicates that the user wishes to run database migrations, deprecated
|
2016-01-05 00:32:53 +01:00
|
|
|
var Migration bool
|
2019-07-09 19:49:17 +02:00
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// InteractiveSetup indicates that the cli should perform interactive setup mode
|
2016-04-18 02:58:29 +02:00
|
|
|
var InteractiveSetup bool
|
2019-07-09 19:49:17 +02:00
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// Upgrade indicates that we should perform an upgrade action
|
2016-04-26 20:18:28 +02:00
|
|
|
var Upgrade bool
|
2019-07-09 19:49:17 +02:00
|
|
|
|
2021-07-13 08:27:22 +02:00
|
|
|
type UserAddArgs struct {
|
|
|
|
Username string
|
|
|
|
Name string
|
|
|
|
Email string
|
|
|
|
Password string
|
|
|
|
}
|
|
|
|
|
|
|
|
var UserAdd *UserAddArgs
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// WebHostURL is the public route to the semaphore server
|
2017-05-20 16:14:36 +02:00
|
|
|
var WebHostURL *url.URL
|
2016-01-05 00:32:53 +01:00
|
|
|
|
2020-11-28 22:49:44 +01:00
|
|
|
type DbDriver int
|
|
|
|
|
|
|
|
const (
|
|
|
|
DbDriverMySQL DbDriver = iota
|
2021-05-13 21:45:54 +02:00
|
|
|
DbDriverBolt
|
2020-11-28 22:49:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type DbConfig struct {
|
|
|
|
Dialect DbDriver `json:"-"`
|
|
|
|
Hostname string `json:"host"`
|
|
|
|
Username string `json:"user"`
|
|
|
|
Password string `json:"pass"`
|
|
|
|
DbName string `json:"name"`
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|
|
|
|
|
2017-04-04 14:27:06 +02:00
|
|
|
type ldapMappings struct {
|
|
|
|
DN string `json:"dn"`
|
|
|
|
Mail string `json:"mail"`
|
2018-03-27 22:12:47 +02:00
|
|
|
UID string `json:"uid"`
|
2017-04-04 14:27:06 +02:00
|
|
|
CN string `json:"cn"`
|
|
|
|
}
|
|
|
|
|
2019-07-09 19:49:17 +02:00
|
|
|
//ConfigType mapping between Config and the json file that sets it
|
|
|
|
type ConfigType struct {
|
2021-07-15 22:23:59 +02:00
|
|
|
MySQL DbConfig `json:"mysql"`
|
|
|
|
BoltDb DbConfig `json:"bolt"`
|
2020-11-28 22:49:44 +01:00
|
|
|
|
2016-01-05 00:32:53 +01:00
|
|
|
// Format `:port_num` eg, :3000
|
2018-05-14 21:37:07 +02:00
|
|
|
// if : is missing it will be corrected
|
2017-05-20 16:14:36 +02:00
|
|
|
Port string `json:"port"`
|
2016-01-05 00:32:53 +01:00
|
|
|
|
2018-05-14 21:37:07 +02:00
|
|
|
// Interface ip, put in front of the port.
|
|
|
|
// defaults to empty
|
|
|
|
Interface string `json:"interface"`
|
|
|
|
|
2018-03-15 00:13:45 +01:00
|
|
|
// semaphore stores ephemeral projects here
|
2016-01-05 00:32:53 +01:00
|
|
|
TmpPath string `json:"tmp_path"`
|
2016-04-30 14:28:47 +02:00
|
|
|
|
|
|
|
// cookie hashing & encryption
|
|
|
|
CookieHash string `json:"cookie_hash"`
|
|
|
|
CookieEncryption string `json:"cookie_encryption"`
|
2017-02-22 09:46:42 +01:00
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// email alerting
|
2017-02-22 09:46:42 +01:00
|
|
|
EmailSender string `json:"email_sender"`
|
|
|
|
EmailHost string `json:"email_host"`
|
|
|
|
EmailPort string `json:"email_port"`
|
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// web host
|
2017-02-22 09:46:42 +01:00
|
|
|
WebHost string `json:"web_host"`
|
2017-03-27 06:53:00 +02:00
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// ldap settings
|
2017-04-04 14:27:06 +02:00
|
|
|
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"`
|
|
|
|
LdapMappings ldapMappings `json:"ldap_mappings"`
|
2017-04-04 13:49:00 +02:00
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// telegram alerting
|
2017-03-22 08:22:09 +01:00
|
|
|
TelegramChat string `json:"telegram_chat"`
|
|
|
|
TelegramToken string `json:"telegram_token"`
|
Allow concurrency for tasks that does not collide
Two different concurrency modes are implemented, and is enabled by
setting "concurrency_mode" in the config file to either "project" or "node".
When "project" concurrency is enabled, tasks will run in parallel if and
only if they do not share the same project id, with no regard to the
nodes/hosts that are affected.
When "node" concurrency is enabled, a task will run in parallel if and
only if the hosts affected by tasks already running does not intersect
with the hosts that would be affected by the task in question.
If "concurrency_mode" is not specified, no task will start before the
previous one has finished.
The collision check is based on the output from the "--list-hosts"
argument to ansible, which uses the hosts specified in the inventory.
Thus, if two different hostnames are used that points to the same node,
such as "127.0.0.1" and "localhost", there will be no collision and two
tasks may connect to the same node concurrently. If this behaviour is
not desired, one should make sure to not include aliases for their hosts
in their inventories when enabling concurrency mode.
To restrict the amount of parallel tasks that runs at the same time, one
can add the "max_parallel_tasks" to the config file. This defaults to a
humble 10 if not specified.
2017-05-29 17:27:56 +02:00
|
|
|
|
|
|
|
// task concurrency
|
|
|
|
ConcurrencyMode string `json:"concurrency_mode"`
|
|
|
|
MaxParallelTasks int `json:"max_parallel_tasks"`
|
2018-03-27 22:12:47 +02:00
|
|
|
|
|
|
|
// configType field ordering with bools at end reduces struct size
|
|
|
|
// (maligned check)
|
|
|
|
|
|
|
|
// feature switches
|
|
|
|
EmailAlert bool `json:"email_alert"`
|
|
|
|
TelegramAlert bool `json:"telegram_alert"`
|
|
|
|
LdapEnable bool `json:"ldap_enable"`
|
|
|
|
LdapNeedTLS bool `json:"ldap_needtls"`
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|
|
|
|
|
2019-07-09 19:49:17 +02:00
|
|
|
//Config exposes the application configuration storage for use in the application
|
|
|
|
var Config *ConfigType
|
2018-03-27 22:12:47 +02:00
|
|
|
|
2021-07-15 22:23:59 +02:00
|
|
|
// ToJSON returns a JSON string of the config
|
|
|
|
func (config *ConfigType) ToJSON() ([]byte, error) {
|
|
|
|
return json.MarshalIndent(&config, " ", "\t")
|
2020-12-03 14:44:30 +01:00
|
|
|
}
|
2016-01-05 00:32:53 +01:00
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// ConfigInit reads in cli flags, and switches actions appropriately on them
|
2018-03-05 18:06:24 +01:00
|
|
|
func ConfigInit() {
|
2016-04-18 02:58:29 +02:00
|
|
|
flag.BoolVar(&InteractiveSetup, "setup", false, "perform interactive setup")
|
2016-01-05 00:32:53 +01:00
|
|
|
flag.BoolVar(&Migration, "migrate", false, "execute migrations")
|
2016-04-26 20:18:28 +02:00
|
|
|
flag.BoolVar(&Upgrade, "upgrade", false, "upgrade semaphore")
|
2021-07-15 22:23:59 +02:00
|
|
|
configPath := flag.String("config", "", "config path")
|
2016-01-05 00:32:53 +01:00
|
|
|
|
2018-03-05 18:06:24 +01:00
|
|
|
var unhashedPwd string
|
|
|
|
flag.StringVar(&unhashedPwd, "hash", "", "generate hash of given password")
|
2016-03-19 00:23:03 +01:00
|
|
|
|
2016-04-11 12:30:31 +02:00
|
|
|
var printConfig bool
|
|
|
|
flag.BoolVar(&printConfig, "printConfig", false, "print example configuration")
|
|
|
|
|
2018-03-08 00:42:11 +01:00
|
|
|
var printVersion bool
|
|
|
|
flag.BoolVar(&printVersion, "version", false, "print the semaphore version")
|
|
|
|
|
2021-07-13 08:27:22 +02:00
|
|
|
var userAdd bool
|
|
|
|
flag.BoolVar(&userAdd, "useradd", false, "add new user")
|
|
|
|
|
|
|
|
var userAddArgs UserAddArgs
|
|
|
|
|
|
|
|
flag.StringVar(&userAddArgs.Username, "login", "", "new user login")
|
|
|
|
flag.StringVar(&userAddArgs.Password, "password", "", "new user password")
|
|
|
|
flag.StringVar(&userAddArgs.Name, "name", "", "new user name")
|
|
|
|
flag.StringVar(&userAddArgs.Email, "email", "", "new user email")
|
|
|
|
|
2016-01-05 00:32:53 +01:00
|
|
|
flag.Parse()
|
|
|
|
|
2021-07-13 08:27:22 +02:00
|
|
|
if userAdd {
|
|
|
|
if userAddArgs.Username == "" || userAddArgs.Password == "" || userAddArgs.Name == "" || userAddArgs.Email == "" {
|
|
|
|
fmt.Println("Required options:\n -login\n -name\n -email\n -password")
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
UserAdd = &userAddArgs
|
|
|
|
}
|
|
|
|
|
2018-03-05 18:06:24 +01:00
|
|
|
if InteractiveSetup {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-08 00:42:11 +01:00
|
|
|
if printVersion {
|
|
|
|
fmt.Println(Version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2016-04-11 12:30:31 +02:00
|
|
|
if printConfig {
|
2021-07-15 22:23:59 +02:00
|
|
|
config := &ConfigType{
|
2020-11-28 22:49:44 +01:00
|
|
|
MySQL: DbConfig{
|
2016-04-11 12:30:31 +02:00
|
|
|
Hostname: "127.0.0.1:3306",
|
|
|
|
Username: "root",
|
|
|
|
DbName: "semaphore",
|
|
|
|
},
|
2016-04-30 14:28:47 +02:00
|
|
|
Port: ":3000",
|
|
|
|
TmpPath: "/tmp/semaphore",
|
|
|
|
}
|
2021-07-15 22:23:59 +02:00
|
|
|
config.GenerateCookieSecrets()
|
2016-04-30 14:28:47 +02:00
|
|
|
|
2021-07-15 22:23:59 +02:00
|
|
|
bytes, _ := config.ToJSON()
|
|
|
|
fmt.Println(string(bytes))
|
2016-04-11 12:30:31 +02:00
|
|
|
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2018-03-05 18:06:24 +01:00
|
|
|
if len(unhashedPwd) > 0 {
|
|
|
|
password, _ := bcrypt.GenerateFromPassword([]byte(unhashedPwd), 11)
|
2016-03-19 00:23:03 +01:00
|
|
|
fmt.Println("Generated password: ", string(password))
|
|
|
|
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2021-07-15 22:23:59 +02:00
|
|
|
loadConfig(configPath)
|
2018-03-20 01:28:59 +01:00
|
|
|
validateConfig()
|
Allow concurrency for tasks that does not collide
Two different concurrency modes are implemented, and is enabled by
setting "concurrency_mode" in the config file to either "project" or "node".
When "project" concurrency is enabled, tasks will run in parallel if and
only if they do not share the same project id, with no regard to the
nodes/hosts that are affected.
When "node" concurrency is enabled, a task will run in parallel if and
only if the hosts affected by tasks already running does not intersect
with the hosts that would be affected by the task in question.
If "concurrency_mode" is not specified, no task will start before the
previous one has finished.
The collision check is based on the output from the "--list-hosts"
argument to ansible, which uses the hosts specified in the inventory.
Thus, if two different hostnames are used that points to the same node,
such as "127.0.0.1" and "localhost", there will be no collision and two
tasks may connect to the same node concurrently. If this behaviour is
not desired, one should make sure to not include aliases for their hosts
in their inventories when enabling concurrency mode.
To restrict the amount of parallel tasks that runs at the same time, one
can add the "max_parallel_tasks" to the config file. This defaults to a
humble 10 if not specified.
2017-05-29 17:27:56 +02:00
|
|
|
|
2016-04-30 14:28:47 +02:00
|
|
|
var encryption []byte
|
|
|
|
|
|
|
|
hash, _ := base64.StdEncoding.DecodeString(Config.CookieHash)
|
|
|
|
if len(Config.CookieEncryption) > 0 {
|
|
|
|
encryption, _ = base64.StdEncoding.DecodeString(Config.CookieEncryption)
|
|
|
|
}
|
|
|
|
|
|
|
|
Cookie = securecookie.New(hash, encryption)
|
2017-05-20 16:14:36 +02:00
|
|
|
WebHostURL, _ = url.Parse(Config.WebHost)
|
2017-05-20 16:25:41 +02:00
|
|
|
if len(WebHostURL.String()) == 0 {
|
|
|
|
WebHostURL = nil
|
|
|
|
}
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|
|
|
|
|
2021-07-15 22:23:59 +02:00
|
|
|
func loadConfig(configPath *string) {
|
|
|
|
//If the configPath option has been set try to load and decode it
|
|
|
|
var usedPath string
|
|
|
|
if configPath != nil && len(*configPath) > 0 {
|
|
|
|
path := *configPath
|
|
|
|
file, err := os.Open(path)
|
2018-03-26 13:58:06 +02:00
|
|
|
exitOnConfigError(err)
|
2018-03-15 00:13:45 +01:00
|
|
|
decodeConfig(file)
|
2021-07-15 22:23:59 +02:00
|
|
|
usedPath = path
|
2018-03-15 00:13:45 +01:00
|
|
|
} else {
|
2021-07-15 22:23:59 +02:00
|
|
|
// if no configPath look in the cwd
|
2018-03-15 00:13:45 +01:00
|
|
|
cwd, err := os.Getwd()
|
2018-03-26 13:58:06 +02:00
|
|
|
exitOnConfigError(err)
|
2021-07-15 22:23:59 +02:00
|
|
|
defaultPath := path.Join(cwd, "config.json")
|
|
|
|
file, err := os.Open(defaultPath)
|
2018-03-26 13:58:06 +02:00
|
|
|
exitOnConfigError(err)
|
|
|
|
decodeConfig(file)
|
2021-07-15 22:23:59 +02:00
|
|
|
usedPath = defaultPath
|
2018-03-15 00:13:45 +01:00
|
|
|
}
|
2021-07-15 22:23:59 +02:00
|
|
|
|
|
|
|
fmt.Println("Using config file: " + usedPath)
|
2018-03-15 00:13:45 +01:00
|
|
|
}
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
func validateConfig() {
|
2018-03-20 01:28:59 +01:00
|
|
|
|
|
|
|
validatePort()
|
|
|
|
|
|
|
|
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"
|
|
|
|
}
|
2018-03-27 22:12:47 +02:00
|
|
|
if !strings.HasPrefix(Config.Port, ":") {
|
|
|
|
Config.Port = ":" + Config.Port
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
func exitOnConfigError(err error) {
|
2018-03-26 13:58:06 +02:00
|
|
|
if err != nil {
|
2021-07-13 08:27:22 +02:00
|
|
|
fmt.Println("Cannot Find configuration! Use -config parameter to point to a JSON file generated by -setup.\n\n Hint: have you run `-setup` ?")
|
2018-03-26 13:58:06 +02:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
func decodeConfig(file io.Reader) {
|
2018-03-15 00:13:45 +01:00
|
|
|
if err := json.NewDecoder(file).Decode(&Config); err != nil {
|
|
|
|
fmt.Println("Could not decode configuration!")
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-28 22:49:44 +01:00
|
|
|
func (d DbDriver) String() string {
|
2020-12-04 09:39:56 +01:00
|
|
|
return [...]string{"mysql"}[d]
|
2020-11-28 22:49:44 +01:00
|
|
|
}
|
|
|
|
|
2021-07-13 08:27:22 +02:00
|
|
|
func (d *DbConfig) IsPresent() bool {
|
2020-11-28 22:49:44 +01:00
|
|
|
return d.Hostname != ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DbConfig) HasSupportMultipleDatabases() bool {
|
2020-12-04 09:39:56 +01:00
|
|
|
return true
|
2020-11-28 22:49:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString string, err error) {
|
|
|
|
switch d.Dialect {
|
2021-05-13 21:45:54 +02:00
|
|
|
case DbDriverBolt:
|
|
|
|
connectionString = d.Hostname
|
2020-11-28 22:49:44 +01:00
|
|
|
case DbDriverMySQL:
|
|
|
|
if includeDbName {
|
|
|
|
connectionString = fmt.Sprintf(
|
|
|
|
"%s:%s@tcp(%s)/%s?parseTime=true&interpolateParams=true",
|
|
|
|
d.Username,
|
|
|
|
d.Password,
|
|
|
|
d.Hostname,
|
|
|
|
d.DbName)
|
|
|
|
} else {
|
|
|
|
connectionString = fmt.Sprintf(
|
|
|
|
"%s:%s@tcp(%s)?parseTime=true&interpolateParams=true",
|
|
|
|
d.Username,
|
|
|
|
d.Password,
|
|
|
|
d.Hostname)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
err = fmt.Errorf("unsupported database driver: %s", d.Dialect)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (conf *ConfigType) GetDBConfig() (dbConfig DbConfig, err error) {
|
|
|
|
switch {
|
2021-07-13 08:27:22 +02:00
|
|
|
case conf.MySQL.IsPresent():
|
2020-11-28 22:49:44 +01:00
|
|
|
dbConfig = conf.MySQL
|
|
|
|
dbConfig.Dialect = DbDriverMySQL
|
2021-07-13 08:27:22 +02:00
|
|
|
case conf.BoltDb.IsPresent():
|
2021-05-13 21:45:54 +02:00
|
|
|
dbConfig = conf.BoltDb
|
|
|
|
dbConfig.Dialect = DbDriverBolt
|
2020-11-28 22:49:44 +01:00
|
|
|
default:
|
|
|
|
err = errors.New("database configuration not found")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-07-09 19:49:17 +02:00
|
|
|
//GenerateCookieSecrets generates cookie secret during setup
|
|
|
|
func (conf *ConfigType) GenerateCookieSecrets() {
|
2016-04-30 14:28:47 +02:00
|
|
|
hash := securecookie.GenerateRandomKey(32)
|
|
|
|
encryption := securecookie.GenerateRandomKey(32)
|
2016-01-05 00:32:53 +01:00
|
|
|
|
2016-04-30 14:28:47 +02:00
|
|
|
conf.CookieHash = base64.StdEncoding.EncodeToString(hash)
|
|
|
|
conf.CookieEncryption = base64.StdEncoding.EncodeToString(encryption)
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|