agola/internal/services/config/config.go
Simone Gotti d2b09d854f *: use new errors handling library
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.
2022-02-28 12:49:13 +01:00

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)
}