d2b09d854f
Implement a new error handling library based on pkg/errors. It provides stack saving on wrapping and exports some function to add stack saving also to external errors. It also implements custom zerolog error formatting without adding too much verbosity by just printing the chain error file:line without a full stack trace of every error. * Add a --detailed-errors options to print error with they full chain * Wrap all error returns. Use errors.WithStack to wrap without adding a new messsage and error.Wrap[f] to add a message. * Add golangci-lint wrapcheck to check that external packages errors are wrapped. This won't check that internal packages error are wrapped. But we want also to ensure this case so we'll have to find something else to check also these.
422 lines
11 KiB
Go
422 lines
11 KiB
Go
// Copyright 2019 Sorint.lab
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package config
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"time"
|
|
|
|
"agola.io/agola/internal/errors"
|
|
"agola.io/agola/internal/util"
|
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
)
|
|
|
|
const (
|
|
maxIDLength = 20
|
|
)
|
|
|
|
type Config struct {
|
|
// ID defines the agola installation id. It's used inside the
|
|
// various services to uniquely distinguish it from other installations
|
|
// Defaults to "agola"
|
|
ID string `yaml:"id"`
|
|
|
|
Gateway Gateway `yaml:"gateway"`
|
|
Scheduler Scheduler `yaml:"scheduler"`
|
|
Notification Notification `yaml:"notification"`
|
|
Runservice Runservice `yaml:"runservice"`
|
|
Executor Executor `yaml:"executor"`
|
|
Configstore Configstore `yaml:"configstore"`
|
|
Gitserver Gitserver `yaml:"gitserver"`
|
|
}
|
|
|
|
type Gateway struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
// APIExposedURL is the gateway API exposed url i.e. https://myagola.example.com
|
|
APIExposedURL string `yaml:"apiExposedURL"`
|
|
|
|
// WebExposedURL is the web interface exposed url i.e. https://myagola.example.com
|
|
// This is used for generating the redirect_url in oauth2 redirects
|
|
WebExposedURL string `yaml:"webExposedURL"`
|
|
|
|
RunserviceURL string `yaml:"runserviceURL"`
|
|
ConfigstoreURL string `yaml:"configstoreURL"`
|
|
GitserverURL string `yaml:"gitserverURL"`
|
|
|
|
Web Web `yaml:"web"`
|
|
Etcd Etcd `yaml:"etcd"`
|
|
ObjectStorage ObjectStorage `yaml:"objectStorage"`
|
|
|
|
TokenSigning TokenSigning `yaml:"tokenSigning"`
|
|
|
|
AdminToken string `yaml:"adminToken"`
|
|
}
|
|
|
|
type Scheduler struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
RunserviceURL string `yaml:"runserviceURL"`
|
|
}
|
|
|
|
type Notification struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
// WebExposedURL is the web interface exposed url i.e. https://myagola.example.com
|
|
// This is used for generating the redirect_url in oauth2 redirects
|
|
WebExposedURL string `yaml:"webExposedURL"`
|
|
|
|
RunserviceURL string `yaml:"runserviceURL"`
|
|
ConfigstoreURL string `yaml:"configstoreURL"`
|
|
|
|
Etcd Etcd `yaml:"etcd"`
|
|
}
|
|
|
|
type Runservice struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
DataDir string `yaml:"dataDir"`
|
|
Web Web `yaml:"web"`
|
|
Etcd Etcd `yaml:"etcd"`
|
|
ObjectStorage ObjectStorage `yaml:"objectStorage"`
|
|
|
|
RunCacheExpireInterval time.Duration `yaml:"runCacheExpireInterval"`
|
|
RunWorkspaceExpireInterval time.Duration `yaml:"runWorkspaceExpireInterval"`
|
|
}
|
|
|
|
type Executor struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
DataDir string `yaml:"dataDir"`
|
|
|
|
RunserviceURL string `yaml:"runserviceURL"`
|
|
ToolboxPath string `yaml:"toolboxPath"`
|
|
|
|
Web Web `yaml:"web"`
|
|
|
|
Driver Driver `yaml:"driver"`
|
|
|
|
InitImage InitImage `yaml:"initImage"`
|
|
|
|
Labels map[string]string `yaml:"labels"`
|
|
// ActiveTasksLimit is the max number of concurrent active tasks
|
|
ActiveTasksLimit int `yaml:"activeTasksLimit"`
|
|
|
|
AllowPrivilegedContainers bool `yaml:"allowPrivilegedContainers"`
|
|
}
|
|
|
|
type InitImage struct {
|
|
Image string `yaml:"image"`
|
|
|
|
Auth *DockerRegistryAuth `yaml:"auth"`
|
|
}
|
|
|
|
type DockerRegistryAuthType string
|
|
|
|
const (
|
|
DockerRegistryAuthTypeBasic DockerRegistryAuthType = "basic"
|
|
DockerRegistryAuthTypeEncodedAuth DockerRegistryAuthType = "encodedauth"
|
|
)
|
|
|
|
type DockerRegistryAuth struct {
|
|
Type DockerRegistryAuthType `json:"type"`
|
|
|
|
// basic auth
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
|
|
// encoded auth string
|
|
Auth string `json:"auth"`
|
|
|
|
// future auths like aws ecr auth
|
|
}
|
|
|
|
type Configstore struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
DataDir string `yaml:"dataDir"`
|
|
|
|
Web Web `yaml:"web"`
|
|
Etcd Etcd `yaml:"etcd"`
|
|
ObjectStorage ObjectStorage `yaml:"objectStorage"`
|
|
}
|
|
|
|
type Gitserver struct {
|
|
Debug bool `yaml:"debug"`
|
|
|
|
DataDir string `yaml:"dataDir"`
|
|
|
|
Web Web `yaml:"web"`
|
|
Etcd Etcd `yaml:"etcd"`
|
|
ObjectStorage ObjectStorage `yaml:"objectStorage"`
|
|
|
|
RepositoryCleanupInterval time.Duration `yaml:"repositoryCleanupInterval"`
|
|
RepositoryRefsExpireInterval time.Duration `yaml:"repositoryRefsExpireInterval"`
|
|
}
|
|
|
|
type Web struct {
|
|
// http listen addess
|
|
ListenAddress string `yaml:"listenAddress"`
|
|
|
|
// use TLS (https)
|
|
TLS bool `yaml:"tls"`
|
|
// TLSCert is the path to the pem formatted server certificate. If the
|
|
// certificate is signed by a certificate authority, the certFile should be
|
|
// the concatenation of the server's certificate, any intermediates, and the
|
|
// CA's certificate.
|
|
TLSCertFile string `yaml:"tlsCertFile"`
|
|
// Server cert private key
|
|
// TODO(sgotti) support encrypted private keys (add a private key password config entry)
|
|
TLSKeyFile string `yaml:"tlsKeyFile"`
|
|
|
|
// CORS allowed origins
|
|
AllowedOrigins []string `yaml:"allowedOrigins"`
|
|
}
|
|
|
|
type ObjectStorageType string
|
|
|
|
const (
|
|
ObjectStorageTypePosix ObjectStorageType = "posix"
|
|
ObjectStorageTypeS3 ObjectStorageType = "s3"
|
|
)
|
|
|
|
type ObjectStorage struct {
|
|
Type ObjectStorageType `yaml:"type"`
|
|
|
|
// Posix
|
|
Path string `yaml:"path"`
|
|
|
|
// S3
|
|
Endpoint string `yaml:"endpoint"`
|
|
Bucket string `yaml:"bucket"`
|
|
Location string `yaml:"location"`
|
|
AccessKey string `yaml:"accessKey"`
|
|
SecretAccessKey string `yaml:"secretAccessKey"`
|
|
DisableTLS bool `yaml:"disableTLS"`
|
|
}
|
|
|
|
type Etcd struct {
|
|
Endpoints string `yaml:"endpoints"`
|
|
|
|
// TODO(sgotti) support encrypted private keys (add a private key password config entry)
|
|
TLSCertFile string `yaml:"tlsCertFile"`
|
|
TLSKeyFile string `yaml:"tlsKeyFile"`
|
|
TLSCAFile string `yaml:"tlsCAFile"`
|
|
TLSSkipVerify bool `yaml:"tlsSkipVerify"`
|
|
}
|
|
|
|
type DriverType string
|
|
|
|
const (
|
|
DriverTypeDocker DriverType = "docker"
|
|
DriverTypeK8s DriverType = "kubernetes"
|
|
)
|
|
|
|
type Driver struct {
|
|
Type DriverType `yaml:"type"`
|
|
|
|
// docker fields
|
|
|
|
// k8s fields
|
|
|
|
}
|
|
|
|
type TokenSigning struct {
|
|
// token duration (defaults to 12 hours)
|
|
Duration time.Duration `yaml:"duration"`
|
|
// signing method: "hmac" or "rsa"
|
|
Method string `yaml:"method"`
|
|
// signing key. Used only with HMAC signing method
|
|
Key string `yaml:"key"`
|
|
// path to a file containing a pem encoded private key. Used only with RSA signing method
|
|
PrivateKeyPath string `yaml:"privateKeyPath"`
|
|
// path to a file containing a pem encoded public key. Used only with RSA signing method
|
|
PublicKeyPath string `yaml:"publicKeyPath"`
|
|
}
|
|
|
|
var defaultConfig = Config{
|
|
ID: "agola",
|
|
Gateway: Gateway{
|
|
TokenSigning: TokenSigning{
|
|
Duration: 12 * time.Hour,
|
|
},
|
|
},
|
|
Runservice: Runservice{
|
|
RunCacheExpireInterval: 7 * 24 * time.Hour,
|
|
RunWorkspaceExpireInterval: 7 * 24 * time.Hour,
|
|
},
|
|
Executor: Executor{
|
|
InitImage: InitImage{
|
|
Image: "busybox:stable",
|
|
},
|
|
ActiveTasksLimit: 2,
|
|
},
|
|
Gitserver: Gitserver{
|
|
RepositoryCleanupInterval: 24 * time.Hour,
|
|
RepositoryRefsExpireInterval: 30 * 24 * time.Hour,
|
|
},
|
|
}
|
|
|
|
func Parse(configFile string, componentsNames []string) (*Config, error) {
|
|
configData, err := ioutil.ReadFile(configFile)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
c := &defaultConfig
|
|
if err := yaml.Unmarshal(configData, &c); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return c, Validate(c, componentsNames)
|
|
}
|
|
|
|
func validateWeb(w *Web) error {
|
|
if w.ListenAddress == "" {
|
|
return errors.Errorf("listen address undefined")
|
|
}
|
|
|
|
if w.TLS {
|
|
if w.TLSKeyFile == "" {
|
|
return errors.Errorf("no tls key file specified")
|
|
}
|
|
if w.TLSCertFile == "" {
|
|
return errors.Errorf("no tls cert file specified")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateInitImage(i *InitImage) error {
|
|
if i.Image == "" {
|
|
return errors.Errorf("image is empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Validate(c *Config, componentsNames []string) error {
|
|
// Global
|
|
if len(c.ID) > maxIDLength {
|
|
return errors.Errorf("id too long")
|
|
}
|
|
if !util.ValidateName(c.ID) {
|
|
return errors.Errorf("invalid id")
|
|
}
|
|
|
|
// Gateway
|
|
if isComponentEnabled(componentsNames, "gateway") {
|
|
if c.Gateway.APIExposedURL == "" {
|
|
return errors.Errorf("gateway apiExposedURL is empty")
|
|
}
|
|
if c.Gateway.WebExposedURL == "" {
|
|
return errors.Errorf("gateway webExposedURL is empty")
|
|
}
|
|
if c.Gateway.ConfigstoreURL == "" {
|
|
return errors.Errorf("gateway configstoreURL is empty")
|
|
}
|
|
if c.Gateway.RunserviceURL == "" {
|
|
return errors.Errorf("gateway runserviceURL is empty")
|
|
}
|
|
if err := validateWeb(&c.Gateway.Web); err != nil {
|
|
return errors.Wrapf(err, "gateway web configuration error")
|
|
}
|
|
}
|
|
|
|
// Configstore
|
|
if isComponentEnabled(componentsNames, "configstore") {
|
|
if c.Configstore.DataDir == "" {
|
|
return errors.Errorf("configstore dataDir is empty")
|
|
}
|
|
if err := validateWeb(&c.Configstore.Web); err != nil {
|
|
return errors.Wrapf(err, "configstore web configuration error")
|
|
}
|
|
}
|
|
|
|
// Runservice
|
|
if isComponentEnabled(componentsNames, "runservice") {
|
|
if c.Runservice.DataDir == "" {
|
|
return errors.Errorf("runservice dataDir is empty")
|
|
}
|
|
if err := validateWeb(&c.Runservice.Web); err != nil {
|
|
return errors.Wrapf(err, "runservice web configuration error")
|
|
}
|
|
}
|
|
|
|
// Executor
|
|
if isComponentEnabled(componentsNames, "executor") {
|
|
if c.Executor.DataDir == "" {
|
|
return errors.Errorf("executor dataDir is empty")
|
|
}
|
|
if c.Executor.ToolboxPath == "" {
|
|
return errors.Errorf("git server toolboxPath is empty")
|
|
}
|
|
if c.Executor.RunserviceURL == "" {
|
|
return errors.Errorf("executor runserviceURL is empty")
|
|
}
|
|
if c.Executor.Driver.Type == "" {
|
|
return errors.Errorf("executor driver type is empty")
|
|
}
|
|
switch c.Executor.Driver.Type {
|
|
case DriverTypeDocker:
|
|
case DriverTypeK8s:
|
|
default:
|
|
return errors.Errorf("executor driver type %q unknown", c.Executor.Driver.Type)
|
|
}
|
|
|
|
if err := validateInitImage(&c.Executor.InitImage); err != nil {
|
|
return errors.Wrapf(err, "executor initImage configuration error")
|
|
}
|
|
}
|
|
|
|
// Scheduler
|
|
if isComponentEnabled(componentsNames, "scheduler") {
|
|
if c.Scheduler.RunserviceURL == "" {
|
|
return errors.Errorf("scheduler runserviceURL is empty")
|
|
}
|
|
}
|
|
|
|
// Notification
|
|
if isComponentEnabled(componentsNames, "notification") {
|
|
if c.Notification.WebExposedURL == "" {
|
|
return errors.Errorf("notification webExposedURL is empty")
|
|
}
|
|
if c.Notification.ConfigstoreURL == "" {
|
|
return errors.Errorf("notification configstoreURL is empty")
|
|
}
|
|
if c.Notification.RunserviceURL == "" {
|
|
return errors.Errorf("notification runserviceURL is empty")
|
|
}
|
|
}
|
|
|
|
// Git server
|
|
if isComponentEnabled(componentsNames, "gitserver") {
|
|
if c.Gitserver.DataDir == "" {
|
|
return errors.Errorf("git server dataDir is empty")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isComponentEnabled(componentsNames []string, name string) bool {
|
|
if util.StringInSlice(componentsNames, "all-base") && name != "executor" {
|
|
return true
|
|
}
|
|
return util.StringInSlice(componentsNames, name)
|
|
}
|