2016-01-05 00:32:53 +01:00
|
|
|
package util
|
|
|
|
|
|
|
|
import (
|
2021-12-18 14:16:34 +01:00
|
|
|
"context"
|
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
|
|
|
"fmt"
|
2021-08-27 21:14:20 +02:00
|
|
|
"io"
|
2021-07-13 08:27:22 +02:00
|
|
|
"net/url"
|
2016-03-16 22:49:43 +01:00
|
|
|
"os"
|
2021-12-18 14:16:34 +01:00
|
|
|
"os/exec"
|
2016-04-24 20:11:43 +02:00
|
|
|
"path"
|
2021-12-18 14:16:34 +01:00
|
|
|
"path/filepath"
|
2023-08-05 15:56:39 +02:00
|
|
|
"reflect"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
2023-08-06 11:01:24 +02:00
|
|
|
"strings"
|
2019-07-09 19:49:17 +02:00
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
"github.com/google/go-github/github"
|
2016-04-30 14:28:47 +02:00
|
|
|
"github.com/gorilla/securecookie"
|
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
|
|
|
// 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
|
|
|
const (
|
2023-09-14 19:55:09 +02:00
|
|
|
DbDriverMySQL = "mysql"
|
|
|
|
DbDriverBolt = "bolt"
|
|
|
|
DbDriverPostgres = "postgres"
|
2020-11-28 22:49:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type DbConfig struct {
|
2023-09-14 19:55:09 +02:00
|
|
|
Dialect string `json:"-"`
|
2021-08-28 14:04:56 +02:00
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
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"`
|
2021-10-14 21:14:21 +02:00
|
|
|
Options map[string]string `json:"options"`
|
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"`
|
|
|
|
}
|
|
|
|
|
2023-04-17 22:57:50 +02:00
|
|
|
type oidcEndpoint struct {
|
|
|
|
IssuerURL string `json:"issuer"`
|
|
|
|
AuthURL string `json:"auth"`
|
|
|
|
TokenURL string `json:"token"`
|
|
|
|
UserInfoURL string `json:"userinfo"`
|
|
|
|
JWKSURL string `json:"jwks"`
|
|
|
|
Algorithms []string `json:"algorithms"`
|
|
|
|
}
|
|
|
|
|
2023-04-16 23:57:56 +02:00
|
|
|
type oidcProvider struct {
|
2023-04-17 22:57:50 +02:00
|
|
|
ClientID string `json:"client_id"`
|
|
|
|
ClientSecret string `json:"client_secret"`
|
|
|
|
RedirectURL string `json:"redirect_url"`
|
|
|
|
Scopes []string `json:"scopes"`
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
AutoDiscovery string `json:"provider_url"`
|
|
|
|
Endpoint oidcEndpoint `json:"endpoint"`
|
|
|
|
UsernameClaim string `json:"username_claim"`
|
|
|
|
NameClaim string `json:"name_claim"`
|
|
|
|
EmailClaim string `json:"email_claim"`
|
2023-04-16 23:57:56 +02:00
|
|
|
}
|
|
|
|
|
2023-07-23 16:18:02 +02:00
|
|
|
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.
|
2023-09-14 13:27:41 +02:00
|
|
|
GoGitClientId = "go_git"
|
2023-07-23 16:18:02 +02:00
|
|
|
// CmdGitClientId is external Git client.
|
|
|
|
// Default Git client. It is use external Git binary to clone repositories.
|
2023-09-14 13:27:41 +02:00
|
|
|
CmdGitClientId = "cmd_git"
|
2023-07-23 16:18:02 +02:00
|
|
|
)
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
// // 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-\\.]*)$
|
|
|
|
//
|
|
|
|
// */
|
|
|
|
|
2023-08-29 00:51:04 +02:00
|
|
|
type RunnerSettings struct {
|
2023-09-14 19:23:00 +02:00
|
|
|
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"`
|
2023-09-12 19:40:22 +02:00
|
|
|
// OneOff indicates than runner runs only one job and exit
|
2023-09-14 19:23:00 +02:00
|
|
|
OneOff bool `json:"one_off" env:"SEMAPHORE_RUNNER_ONE_OFF"`
|
2023-08-29 00:51:04 +02:00
|
|
|
}
|
|
|
|
|
2022-11-19 21:20:00 +01:00
|
|
|
// ConfigType mapping between Config and the json file that sets it
|
2019-07-09 19:49:17 +02:00
|
|
|
type ConfigType struct {
|
2021-09-22 05:43:19 +02:00
|
|
|
MySQL DbConfig `json:"mysql"`
|
|
|
|
BoltDb DbConfig `json:"bolt"`
|
2021-08-30 17:11:08 +02:00
|
|
|
Postgres DbConfig `json:"postgres"`
|
2020-11-28 22:49:44 +01:00
|
|
|
|
2023-09-14 19:55:09 +02:00
|
|
|
Dialect string `json:"dialect" rule:"^mysql|bolt|postgres$" env:"SEMAPHORE_DB_DIALECT"`
|
2021-08-28 14:04:56 +02: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
|
2023-09-14 18:56:28 +02:00
|
|
|
Port string `json:"port" default:":3000" rule:"^:([0-9]{1,5})$" env:"SEMAPHORE_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
|
2023-09-14 19:23:00 +02:00
|
|
|
Interface string `json:"interface" env:"SEMAPHORE_INTERFACE"`
|
2018-05-14 21:37:07 +02:00
|
|
|
|
2018-03-15 00:13:45 +01:00
|
|
|
// semaphore stores ephemeral projects here
|
2023-09-14 19:23:00 +02:00
|
|
|
TmpPath string `json:"tmp_path" default:"/tmp/semaphore" env:"SEMAPHORE_TMP_PATH"`
|
2016-04-30 14:28:47 +02:00
|
|
|
|
2023-07-23 16:18:02 +02:00
|
|
|
// SshConfigPath is a path to the custom SSH config file.
|
|
|
|
// Default path is ~/.ssh/config.
|
2023-09-14 19:23:00 +02:00
|
|
|
SshConfigPath string `json:"ssh_config_path" env:"SEMAPHORE_TMP_PATH"`
|
2023-07-23 16:18:02 +02:00
|
|
|
|
2023-09-14 13:27:41 +02:00
|
|
|
GitClientId string `json:"git_client" rule:"^go_git|cmd_git$" env:"SEMAPHORE_GIT_CLIENT" default:"cmd_git"`
|
2023-07-24 16:04:03 +02:00
|
|
|
|
|
|
|
// web host
|
2023-09-14 19:23:00 +02:00
|
|
|
WebHost string `json:"web_host" env:"SEMAPHORE_WEB_ROOT"`
|
2023-07-24 16:04:03 +02:00
|
|
|
|
2016-04-30 14:28:47 +02:00
|
|
|
// cookie hashing & encryption
|
2023-09-14 19:27:11 +02:00
|
|
|
CookieHash string `json:"cookie_hash" env:"SEMAPHORE_COOKIE_HASH"`
|
|
|
|
CookieEncryption string `json:"cookie_encryption" env:"SEMAPHORE_COOKIE_ENCRYPTION"`
|
2022-01-26 08:14:56 +01:00
|
|
|
// AccessKeyEncryption is BASE64 encoded byte array used
|
|
|
|
// for encrypting and decrypting access keys stored in database.
|
2023-09-14 19:27:11 +02:00
|
|
|
AccessKeyEncryption string `json:"access_key_encryption" env:"SEMAPHORE_ACCESS_KEY_ENCRYPTION"`
|
2017-02-22 09:46:42 +01:00
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// email alerting
|
2023-09-14 19:23:00 +02:00
|
|
|
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"`
|
2017-03-27 06:53:00 +02:00
|
|
|
|
2017-04-18 16:36:09 +02:00
|
|
|
// ldap settings
|
2023-09-14 19:23:00 +02:00
|
|
|
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"`
|
2017-04-04 14:27:06 +02:00
|
|
|
LdapMappings ldapMappings `json:"ldap_mappings"`
|
2023-09-14 18:56:28 +02:00
|
|
|
LdapNeedTLS bool `json:"ldap_needtls" env:"SEMAPHORE_LDAP_NEEDTLS"`
|
2017-04-04 13:49:00 +02:00
|
|
|
|
2023-07-24 16:04:03 +02:00
|
|
|
// telegram and slack alerting
|
2023-09-14 19:23:00 +02:00
|
|
|
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"`
|
2022-04-11 10:29:48 +02:00
|
|
|
|
2023-07-24 16:04:03 +02:00
|
|
|
// oidc settings
|
|
|
|
OidcProviders map[string]oidcProvider `json:"oidc_providers"`
|
|
|
|
|
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
|
2023-09-14 19:04:17 +02:00
|
|
|
MaxParallelTasks int `json:"max_parallel_tasks" rule:"^[0-9]{1,10}$" env:"SEMAPHORE_MAX_PARALLEL_TASKS"`
|
2018-03-27 22:12:47 +02:00
|
|
|
|
2023-09-14 19:23:00 +02:00
|
|
|
RunnerRegistrationToken string `json:"runner_registration_token" env:"SEMAPHORE_RUNNER_REGISTRATION_TOKEN"`
|
2023-08-27 18:02:51 +02:00
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// feature switches
|
2023-09-14 19:23:00 +02:00
|
|
|
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"`
|
2023-08-29 00:51:04 +02:00
|
|
|
|
2023-09-14 19:23:00 +02:00
|
|
|
UseRemoteRunner bool `json:"use_remote_runner" env:"SEMAPHORE_USE_REMOTE_RUNNER"`
|
2023-09-10 23:18:25 +02:00
|
|
|
|
2023-08-29 00:51:04 +02:00
|
|
|
Runner RunnerSettings `json:"runner"`
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|
|
|
|
|
2022-11-19 21:20:00 +01:00
|
|
|
// Config exposes the application configuration storage for use in the application
|
2019-07-09 19:49:17 +02:00
|
|
|
var Config *ConfigType
|
2018-03-27 22:12:47 +02:00
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
// ToJSON returns a JSON string of the config
|
|
|
|
func (conf *ConfigType) ToJSON() ([]byte, error) {
|
|
|
|
return json.MarshalIndent(&conf, " ", "\t")
|
2022-01-26 08:14:56 +01:00
|
|
|
}
|
|
|
|
|
2018-03-27 22:12:47 +02:00
|
|
|
// ConfigInit reads in cli flags, and switches actions appropriately on them
|
2021-08-25 22:12:19 +02:00
|
|
|
func ConfigInit(configPath string) {
|
2023-08-05 15:56:39 +02:00
|
|
|
fmt.Println("Loading config")
|
|
|
|
loadConfigFile(configPath)
|
|
|
|
loadConfigEnvironment()
|
2023-09-15 01:57:25 +02:00
|
|
|
loadConfigDefaults()
|
2023-08-05 15:56:39 +02:00
|
|
|
|
|
|
|
fmt.Println("Validating config")
|
2023-09-09 17:01:36 +02: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
|
|
|
}
|
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
func loadConfigFile(configPath string) {
|
2021-08-27 21:14:20 +02:00
|
|
|
if configPath == "" {
|
|
|
|
configPath = os.Getenv("SEMAPHORE_CONFIG_PATH")
|
|
|
|
}
|
|
|
|
|
2021-07-15 22:23:59 +02:00
|
|
|
//If the configPath option has been set try to load and decode it
|
2021-08-28 13:44:41 +02:00
|
|
|
//var usedPath string
|
2021-08-27 21:14:20 +02:00
|
|
|
|
|
|
|
if configPath == "" {
|
2018-03-15 00:13:45 +01:00
|
|
|
cwd, err := os.Getwd()
|
2023-08-05 15:56:39 +02:00
|
|
|
exitOnConfigFileError(err)
|
2022-01-23 21:13:29 +01:00
|
|
|
paths := []string{
|
|
|
|
path.Join(cwd, "config.json"),
|
|
|
|
"/usr/local/etc/semaphore/config.json",
|
|
|
|
}
|
|
|
|
for _, p := range paths {
|
|
|
|
_, err = os.Stat(p)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var file *os.File
|
|
|
|
file, err = os.Open(p)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
decodeConfig(file)
|
|
|
|
break
|
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
exitOnConfigFileError(err)
|
2021-08-27 21:14:20 +02:00
|
|
|
} else {
|
2022-01-23 21:13:29 +01:00
|
|
|
p := configPath
|
|
|
|
file, err := os.Open(p)
|
2023-08-05 15:56:39 +02:00
|
|
|
exitOnConfigFileError(err)
|
2021-08-27 21:14:20 +02:00
|
|
|
decodeConfig(file)
|
2018-03-15 00:13:45 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
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)
|
|
|
|
|
2023-09-15 01:57:25 +02:00
|
|
|
if !fieldValue.IsZero() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
if fieldType.Type.Kind() == reflect.Struct {
|
|
|
|
err := loadDefaultsToObject(fieldValue.Addr())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2018-03-20 01:28:59 +01:00
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
defaultVar := fieldType.Tag.Get("default")
|
|
|
|
if defaultVar == "" {
|
|
|
|
continue
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
2023-09-09 17:01:36 +02:00
|
|
|
|
|
|
|
setConfigValue(fieldValue, defaultVar)
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadConfigDefaults() {
|
|
|
|
|
|
|
|
err := loadDefaultsToObject(Config)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func castStringToInt(value string) int {
|
2018-03-20 01:28:59 +01:00
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
valueInt, err := strconv.Atoi(value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
return valueInt
|
|
|
|
|
|
|
|
}
|
2018-03-20 01:28:59 +01:00
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
func castStringToBool(value string) bool {
|
|
|
|
|
|
|
|
var valueBool bool
|
|
|
|
if value == "1" || strings.ToLower(value) == "true" {
|
|
|
|
valueBool = true
|
|
|
|
} else {
|
|
|
|
valueBool = false
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
return valueBool
|
|
|
|
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
func setConfigValue(attribute reflect.Value, value interface{}) {
|
2018-03-20 01:28:59 +01:00
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
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))
|
2023-08-06 11:01:24 +02:00
|
|
|
} else {
|
2023-09-09 17:01:36 +02:00
|
|
|
panic(fmt.Errorf("got non-existent config attribute"))
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
|
|
|
|
return fmt.Sprintf("%v", attribute)
|
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
func validate(value interface{}) error {
|
|
|
|
var t = reflect.TypeOf(value)
|
|
|
|
var v = reflect.ValueOf(value)
|
2023-08-05 15:56:39 +02:00
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
t = t.Elem()
|
|
|
|
v = reflect.Indirect(v)
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
fieldType := t.Field(i)
|
|
|
|
fieldValue := v.Field(i)
|
|
|
|
|
|
|
|
rule := fieldType.Tag.Get("rule")
|
|
|
|
if rule == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-09-14 18:56:28 +02:00
|
|
|
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)
|
2023-08-05 15:56:39 +02:00
|
|
|
if !match {
|
2023-09-09 17:01:36 +02:00
|
|
|
return fmt.Errorf(
|
|
|
|
"value of field '%v' is not valid! (Must match regex: '%v')",
|
|
|
|
fieldType.Name, rule,
|
|
|
|
)
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
return nil
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
func validateConfig() {
|
2023-08-05 15:56:39 +02:00
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
err := validate(Config)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
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 {
|
2023-09-14 19:04:17 +02:00
|
|
|
err := loadEnvironmentToObject(fieldValue.Addr().Interface())
|
2023-09-09 17:01:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
continue
|
2023-09-09 17:01:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
envVar := fieldType.Tag.Get("env")
|
|
|
|
if envVar == "" {
|
2023-08-05 15:56:39 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
envValue, exists := os.LookupEnv(envVar)
|
2023-09-09 17:01:36 +02:00
|
|
|
|
|
|
|
if !exists {
|
|
|
|
continue
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
2023-09-09 17:01:36 +02:00
|
|
|
|
|
|
|
setConfigValue(fieldValue, envValue)
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
|
2023-09-09 17:01:36 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadConfigEnvironment() {
|
|
|
|
err := loadEnvironmentToObject(Config)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2023-08-05 15:56:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func exitOnConfigError(msg string) {
|
|
|
|
fmt.Println(msg)
|
|
|
|
os.Exit(1)
|
2018-03-20 01:28:59 +01:00
|
|
|
}
|
|
|
|
|
2023-08-05 15:56:39 +02:00
|
|
|
func exitOnConfigFileError(err error) {
|
2018-03-26 13:58:06 +02:00
|
|
|
if err != nil {
|
2023-08-05 15:56:39 +02:00
|
|
|
exitOnConfigError("Cannot Find configuration! Use --config parameter to point to a JSON file generated by `semaphore setup`.")
|
2018-03-26 13:58:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 14:18:40 +02:00
|
|
|
func mapToQueryString(m map[string]string) (str string) {
|
|
|
|
for option, value := range m {
|
|
|
|
if str != "" {
|
|
|
|
str += "&"
|
|
|
|
}
|
|
|
|
str += option + "=" + value
|
|
|
|
}
|
|
|
|
if str != "" {
|
|
|
|
str = "?" + str
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-18 14:16:34 +01:00
|
|
|
// FindSemaphore looks in the PATH for the semaphore variable
|
|
|
|
// if not found it will attempt to find the absolute path of the first
|
|
|
|
// os argument, the semaphore command, and return it
|
|
|
|
func FindSemaphore() string {
|
|
|
|
cmdPath, _ := exec.LookPath("semaphore") //nolint: gas
|
|
|
|
|
|
|
|
if len(cmdPath) == 0 {
|
|
|
|
cmdPath, _ = filepath.Abs(os.Args[0]) // nolint: gas
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmdPath
|
|
|
|
}
|
|
|
|
|
|
|
|
func AnsibleVersion() string {
|
|
|
|
bytes, err := exec.Command("ansible", "--version").Output()
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(bytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckUpdate uses the GitHub client to check for new tags in the semaphore repo
|
|
|
|
func CheckUpdate() (updateAvailable *github.RepositoryRelease, err error) {
|
|
|
|
// fetch releases
|
|
|
|
gh := github.NewClient(nil)
|
|
|
|
releases, _, err := gh.Repositories.ListReleases(context.TODO(), "ansible-semaphore", "semaphore", nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
updateAvailable = nil
|
|
|
|
if (*releases[0].TagName)[1:] != Version {
|
|
|
|
updateAvailable = releases[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-13 08:27:22 +02:00
|
|
|
func (d *DbConfig) IsPresent() bool {
|
2023-01-28 00:25:25 +01:00
|
|
|
return d.GetHostname() != ""
|
2020-11-28 22:49:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DbConfig) HasSupportMultipleDatabases() bool {
|
2020-12-04 09:39:56 +01:00
|
|
|
return true
|
2020-11-28 22:49:44 +01:00
|
|
|
}
|
|
|
|
|
2023-01-28 00:25:25 +01:00
|
|
|
func (d *DbConfig) GetDbName() string {
|
2023-01-27 19:54:46 +01:00
|
|
|
dbName := os.Getenv("SEMAPHORE_DB_NAME")
|
2023-01-28 00:25:25 +01:00
|
|
|
if dbName != "" {
|
|
|
|
return dbName
|
2023-01-27 19:54:46 +01:00
|
|
|
}
|
2023-01-28 00:25:25 +01:00
|
|
|
return d.DbName
|
|
|
|
}
|
2023-01-27 19:54:46 +01:00
|
|
|
|
2023-01-28 00:25:25 +01:00
|
|
|
func (d *DbConfig) GetUsername() string {
|
|
|
|
username := os.Getenv("SEMAPHORE_DB_USER")
|
|
|
|
if username != "" {
|
|
|
|
return username
|
2023-01-27 19:54:46 +01:00
|
|
|
}
|
2023-01-28 00:25:25 +01:00
|
|
|
return d.Username
|
|
|
|
}
|
2023-01-27 19:54:46 +01:00
|
|
|
|
2023-01-28 00:25:25 +01:00
|
|
|
func (d *DbConfig) GetPassword() string {
|
|
|
|
password := os.Getenv("SEMAPHORE_DB_PASS")
|
|
|
|
if password != "" {
|
|
|
|
return password
|
2023-01-27 19:54:46 +01:00
|
|
|
}
|
2023-01-28 00:25:25 +01:00
|
|
|
return d.Password
|
|
|
|
}
|
2023-01-27 19:54:46 +01:00
|
|
|
|
2023-01-28 00:25:25 +01:00
|
|
|
func (d *DbConfig) GetHostname() string {
|
|
|
|
hostname := os.Getenv("SEMAPHORE_DB_HOST")
|
|
|
|
if hostname != "" {
|
|
|
|
return hostname
|
2023-01-27 19:54:46 +01:00
|
|
|
}
|
2023-01-28 00:25:25 +01:00
|
|
|
return d.Hostname
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString string, err error) {
|
|
|
|
dbName := d.GetDbName()
|
|
|
|
dbUser := d.GetUsername()
|
|
|
|
dbPass := d.GetPassword()
|
|
|
|
dbHost := d.GetHostname()
|
2023-01-27 19:54:46 +01:00
|
|
|
|
2020-11-28 22:49:44 +01:00
|
|
|
switch d.Dialect {
|
2021-05-13 21:45:54 +02:00
|
|
|
case DbDriverBolt:
|
2023-01-27 19:54:46 +01:00
|
|
|
connectionString = dbHost
|
2020-11-28 22:49:44 +01:00
|
|
|
case DbDriverMySQL:
|
|
|
|
if includeDbName {
|
|
|
|
connectionString = fmt.Sprintf(
|
2021-09-22 14:18:40 +02:00
|
|
|
"%s:%s@tcp(%s)/%s",
|
2023-01-27 19:54:46 +01:00
|
|
|
dbUser,
|
|
|
|
dbPass,
|
|
|
|
dbHost,
|
|
|
|
dbName)
|
2020-11-28 22:49:44 +01:00
|
|
|
} else {
|
|
|
|
connectionString = fmt.Sprintf(
|
2021-09-22 14:18:40 +02:00
|
|
|
"%s:%s@tcp(%s)/",
|
2023-01-27 19:54:46 +01:00
|
|
|
dbUser,
|
|
|
|
dbPass,
|
|
|
|
dbHost)
|
2020-11-28 22:49:44 +01:00
|
|
|
}
|
2021-10-14 21:14:21 +02:00
|
|
|
options := map[string]string{
|
|
|
|
"parseTime": "true",
|
2021-09-22 14:18:40 +02:00
|
|
|
"interpolateParams": "true",
|
|
|
|
}
|
|
|
|
for v, k := range d.Options {
|
|
|
|
options[v] = k
|
|
|
|
}
|
|
|
|
connectionString += mapToQueryString(options)
|
2021-08-24 17:20:34 +02:00
|
|
|
case DbDriverPostgres:
|
|
|
|
if includeDbName {
|
|
|
|
connectionString = fmt.Sprintf(
|
|
|
|
"postgres://%s:%s@%s/%s",
|
2023-01-27 19:54:46 +01:00
|
|
|
dbUser,
|
|
|
|
url.QueryEscape(dbPass),
|
|
|
|
dbHost,
|
|
|
|
dbName)
|
2021-08-24 17:20:34 +02:00
|
|
|
} else {
|
|
|
|
connectionString = fmt.Sprintf(
|
2021-08-30 17:11:08 +02:00
|
|
|
"postgres://%s:%s@%s",
|
2023-01-27 19:54:46 +01:00
|
|
|
dbUser,
|
|
|
|
url.QueryEscape(dbPass),
|
|
|
|
dbHost)
|
2021-08-24 17:20:34 +02:00
|
|
|
}
|
2021-09-22 14:18:40 +02:00
|
|
|
connectionString += mapToQueryString(d.Options)
|
2020-11-28 22:49:44 +01:00
|
|
|
default:
|
|
|
|
err = fmt.Errorf("unsupported database driver: %s", d.Dialect)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-19 21:20:00 +01:00
|
|
|
func (conf *ConfigType) PrintDbInfo() {
|
|
|
|
dialect, err := conf.GetDialect()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
switch dialect {
|
|
|
|
case DbDriverMySQL:
|
2023-01-28 00:25:25 +01:00
|
|
|
fmt.Printf("MySQL %v@%v %v\n", conf.MySQL.GetUsername(), conf.MySQL.GetHostname(), conf.MySQL.GetDbName())
|
2022-11-19 21:20:00 +01:00
|
|
|
case DbDriverBolt:
|
2023-01-28 00:25:25 +01:00
|
|
|
fmt.Printf("BoltDB %v\n", conf.BoltDb.GetHostname())
|
2022-11-19 21:20:00 +01:00
|
|
|
case DbDriverPostgres:
|
2023-01-28 00:25:25 +01:00
|
|
|
fmt.Printf("Postgres %v@%v %v\n", conf.Postgres.GetUsername(), conf.Postgres.GetHostname(), conf.Postgres.GetDbName())
|
2022-11-19 21:20:00 +01:00
|
|
|
default:
|
|
|
|
panic(fmt.Errorf("database configuration not found"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 19:55:09 +02:00
|
|
|
func (conf *ConfigType) GetDialect() (dialect string, err error) {
|
2021-08-28 14:04:56 +02:00
|
|
|
if conf.Dialect == "" {
|
|
|
|
switch {
|
|
|
|
case conf.MySQL.IsPresent():
|
|
|
|
dialect = DbDriverMySQL
|
|
|
|
case conf.BoltDb.IsPresent():
|
|
|
|
dialect = DbDriverBolt
|
|
|
|
case conf.Postgres.IsPresent():
|
|
|
|
dialect = DbDriverPostgres
|
|
|
|
default:
|
|
|
|
err = errors.New("database configuration not found")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dialect = conf.Dialect
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-28 22:49:44 +01:00
|
|
|
func (conf *ConfigType) GetDBConfig() (dbConfig DbConfig, err error) {
|
2023-09-14 19:55:09 +02:00
|
|
|
var dialect string
|
2021-08-28 14:04:56 +02:00
|
|
|
dialect, err = conf.GetDialect()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch dialect {
|
|
|
|
case DbDriverBolt:
|
2021-05-13 21:45:54 +02:00
|
|
|
dbConfig = conf.BoltDb
|
2021-08-28 14:04:56 +02:00
|
|
|
case DbDriverPostgres:
|
2021-08-24 17:20:34 +02:00
|
|
|
dbConfig = conf.Postgres
|
2021-08-28 14:04:56 +02:00
|
|
|
case DbDriverMySQL:
|
|
|
|
dbConfig = conf.MySQL
|
2020-11-28 22:49:44 +01:00
|
|
|
default:
|
|
|
|
err = errors.New("database configuration not found")
|
|
|
|
}
|
2021-08-28 18:24:54 +02:00
|
|
|
|
|
|
|
dbConfig.Dialect = dialect
|
|
|
|
|
2020-11-28 22:49:44 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-19 21:20:00 +01:00
|
|
|
// GenerateSecrets generates cookie secret during setup
|
2021-08-31 01:02:41 +02:00
|
|
|
func (conf *ConfigType) GenerateSecrets() {
|
2016-04-30 14:28:47 +02:00
|
|
|
hash := securecookie.GenerateRandomKey(32)
|
|
|
|
encryption := securecookie.GenerateRandomKey(32)
|
2021-08-31 01:02:41 +02:00
|
|
|
accessKeyEncryption := 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)
|
2021-08-31 01:02:41 +02:00
|
|
|
conf.AccessKeyEncryption = base64.StdEncoding.EncodeToString(accessKeyEncryption)
|
2016-01-05 00:32:53 +01:00
|
|
|
}
|