config/config.go
Rama main · réplica de lectura del repositorio original.
package config
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"github.com/coroot/coroot/cloud"
"github.com/coroot/coroot/db"
"github.com/coroot/coroot/rbac"
"github.com/coroot/coroot/timeseries"
"github.com/coroot/coroot/utils"
"gopkg.in/yaml.v3"
)
type Config struct {
ListenAddress string `yaml:"listen_address"`
HTTPSListenAddress string `yaml:"https_listen_address"`
HTTPDisabled bool `yaml:"http_disabled"`
UrlBasePath string `yaml:"url_base_path"`
DataDir string `yaml:"data_dir"`
DefaultTimeRange timeseries.Duration `yaml:"defaultTimeRange"`
GRPC GRPC `yaml:"grpc"`
TLS *TLS `yaml:"tls"`
Cache Cache `yaml:"cache"`
Traces Traces `yaml:"traces"`
Logs Logs `yaml:"logs"`
Profiles Profiles `yaml:"profiles"`
Metrics Metrics `yaml:"metrics"`
Postgres *Postgres `yaml:"postgres"`
GlobalPrometheus *Prometheus `yaml:"global_prometheus"`
GlobalClickhouse *Clickhouse `yaml:"global_clickhouse"`
Auth Auth `yaml:"auth"`
Projects []Project `yaml:"projects"`
DoNotCheckForDeployments bool `yaml:"do_not_check_for_deployments"`
DoNotCheckForUpdates bool `yaml:"do_not_check_for_updates"`
DisableUsageStatistics bool `yaml:"disable_usage_statistics"`
DisableBuiltinAlerts bool `yaml:"disableBuiltinAlerts"`
DeveloperMode bool `yaml:"developer_mode"`
ClickHouseSpaceManager ClickHouseSpaceManager `yaml:"clickhouse_space_manager"`
CorootCloud *cloud.Settings `yaml:"corootCloud"`
BootstrapClickhouse *Clickhouse `yaml:"-"`
BootstrapPrometheus *Prometheus `yaml:"-"`
}
type GRPC struct {
Disabled bool `yaml:"disabled"`
ListenAddress string `yaml:"listenAddress"`
}
type TLS struct {
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
}
func (c *TLS) Validate() error {
if c == nil {
return nil
}
if c.CertFile == "" {
return errors.New("certFile is required")
}
if c.KeyFile == "" {
return errors.New("keyFile is required")
}
if _, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile); err != nil {
return fmt.Errorf("invalid certificate: %w", err)
}
return nil
}
type ClickHouseSpaceManager struct {
Enabled bool `yaml:"enabled"`
UsageThresholdPercent int `yaml:"usage_threshold_percent"`
MinPartitions int `yaml:"min_partitions"`
}
type Cache struct {
TTL timeseries.Duration `yaml:"ttl"`
GCInterval timeseries.Duration `yaml:"gc_interval"`
}
type Traces struct {
TTL timeseries.Duration `yaml:"ttl"`
}
type Logs struct {
TTL timeseries.Duration `yaml:"ttl"`
}
type Profiles struct {
TTL timeseries.Duration `yaml:"ttl"`
}
type Metrics struct {
TTL timeseries.Duration `yaml:"ttl"`
}
type Postgres struct {
ConnectionString string `yaml:"connection_string"`
}
type Clickhouse struct {
Address string `yaml:"address"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
TlsEnable bool `yaml:"tls_enable"`
TlsSkipVerify bool `yaml:"tls_skip_verify"`
TlsCAFile string `yaml:"tls_ca_file"`
}
func (c *Clickhouse) Validate() error {
if c == nil {
return nil
}
if c.Address == "" {
return errors.New("address is required")
}
host, port, err := net.SplitHostPort(c.Address)
if host == "" || port == "" {
return fmt.Errorf("invalid address: %s", c.Address)
}
if err != nil {
return fmt.Errorf("invalid address: %w", err)
}
return nil
}
type Prometheus struct {
Url string `yaml:"url"`
RefreshInterval timeseries.Duration `yaml:"refresh_interval"`
TlsSkipVerify bool `yaml:"tls_skip_verify"`
User string `yaml:"user"`
Password string `yaml:"password"`
ExtraSelector string `yaml:"extra_selector"`
CustomHeaders map[string]string `yaml:"custom_headers"`
RemoteWriteUrl string `yaml:"remote_write_url"`
UseClickHouse bool `yaml:"use_clickhouse"`
}
func validateUrl(urlString string) error {
u, err := url.Parse(urlString)
if err != nil {
return fmt.Errorf("invalid url: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("invalid url '%s': missing protocol scheme (http:// or https://)", urlString)
}
return nil
}
func (p *Prometheus) Validate() error {
if p == nil {
return nil
}
if p.RefreshInterval <= 0 {
return fmt.Errorf("invalid refresh-interval: %d", p.RefreshInterval)
}
if p.UseClickHouse {
return nil
}
if p.Url == "" {
return errors.New("url is required")
}
if err := validateUrl(p.Url); err != nil {
return err
}
if p.RemoteWriteUrl != "" {
if err := validateUrl(p.RemoteWriteUrl); err != nil {
return err
}
}
if !IsPrometheusSelectorValid(p.ExtraSelector) {
return fmt.Errorf("invalid extra_selector: %s", p.ExtraSelector)
}
return nil
}
type Auth struct {
AnonymousRole string `yaml:"anonymous_role"`
BootstrapAdminPassword string `yaml:"bootstrap_admin_password"`
ServiceAccounts []ServiceAccount `yaml:"serviceAccounts"`
}
type ServiceAccount struct {
Name string `yaml:"name"`
Role rbac.RoleName `yaml:"role"`
ApiKeys []db.ApiKey `yaml:"apiKeys"`
}
func (sa *ServiceAccount) Validate() error {
if sa.Name == "" {
return errors.New("name is required")
}
if sa.Role == "" {
return errors.New("role is required")
}
if len(sa.ApiKeys) == 0 {
return errors.New("no api keys defined")
}
descriptions := map[string]bool{}
for i, k := range sa.ApiKeys {
if err := k.Validate(); err != nil {
return fmt.Errorf("invalid api key #%d: %w", i, err)
}
if k.Description == "" {
return fmt.Errorf("invalid api key #%d: description is required", i)
}
if descriptions[k.Description] {
return fmt.Errorf("invalid api key #%d: duplicate description '%s'", i, k.Description)
}
descriptions[k.Description] = true
}
return nil
}
func NewConfig() *Config {
cfg := &Config{
ListenAddress: ":8080",
UrlBasePath: "/",
DataDir: "./data",
DefaultTimeRange: timeseries.Hour,
Cache: Cache{
TTL: 30 * timeseries.Day,
GCInterval: 10 * timeseries.Minute,
},
Traces: Traces{
TTL: 7 * timeseries.Day,
},
Logs: Logs{
TTL: 7 * timeseries.Day,
},
Profiles: Profiles{
TTL: 7 * timeseries.Day,
},
Metrics: Metrics{
TTL: 7 * timeseries.Day,
},
Auth: Auth{
BootstrapAdminPassword: db.AdminUserDefaultPassword,
},
ClickHouseSpaceManager: ClickHouseSpaceManager{
Enabled: true,
UsageThresholdPercent: 70,
MinPartitions: 1,
},
}
if !cfg.GRPC.Disabled && cfg.GRPC.ListenAddress == "" {
cfg.GRPC.ListenAddress = ":4317"
}
return cfg
}
func Load() (*Config, error) {
cfg := NewConfig()
data, err := ReadFromFile()
if err != nil {
return nil, err
}
if len(data) > 0 {
if err = yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
}
cfg.ApplyFlags()
if err = cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}
func ReadFromFile() ([]byte, error) {
if *configFile == "" {
return nil, nil
}
f, err := os.Open(*configFile)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
data = []byte(os.ExpandEnv(string(data)))
return data, nil
}
func (cfg *Config) Validate() error {
var err error
cfg.UrlBasePath, err = url.JoinPath("/", cfg.UrlBasePath, "/")
if err != nil {
return fmt.Errorf("invalid url_base_path: %s", cfg.UrlBasePath)
}
if cfg.DefaultTimeRange < timeseries.Minute || cfg.DefaultTimeRange%timeseries.Minute != 0 {
return fmt.Errorf("invalid defaultTimeRange: %s (must be a whole number of minutes, e.g. 30m, 1h, 3h)", cfg.DefaultTimeRange)
}
for name, d := range map[string]timeseries.Duration{
"cache.ttl": cfg.Cache.TTL,
"cache.gc_interval": cfg.Cache.GCInterval,
"traces.ttl": cfg.Traces.TTL,
"logs.ttl": cfg.Logs.TTL,
"profiles.ttl": cfg.Profiles.TTL,
"metrics.ttl": cfg.Metrics.TTL,
} {
if d <= 0 {
return fmt.Errorf("invalid %s: %s", name, d)
}
}
if cfg.HTTPDisabled && cfg.HTTPSListenAddress == "" {
return errors.New("at least one of HTTP or HTTPS listener must be enabled")
}
if cfg.HTTPSListenAddress != "" && cfg.TLS == nil {
return errors.New("TLS certificate and key are required for HTTPS")
}
if cfg.TLS != nil {
if err = cfg.TLS.Validate(); err != nil {
return fmt.Errorf("invalid tls settings: %w", err)
}
}
if cfg.CorootCloud != nil {
if err = cfg.CorootCloud.Validate(); err != nil {
return fmt.Errorf("invalid corootCloud settings: %w", err)
}
}
for i, p := range cfg.Projects {
if err = p.Validate(); err != nil {
return fmt.Errorf("invalid project #%d: %w", i, err)
}
}
for i, sa := range cfg.Auth.ServiceAccounts {
if err = sa.Validate(); err != nil {
return fmt.Errorf("invalid service account #%d: %w", i, err)
}
}
if err = cfg.GlobalClickhouse.Validate(); err != nil {
return fmt.Errorf("invalid global_clickhouse: %w", err)
}
if cfg.GlobalClickhouse != nil {
cfg.BootstrapClickhouse = nil
}
if err = cfg.BootstrapClickhouse.Validate(); err != nil {
return fmt.Errorf("invalid bootstrap_clickhouse: %w", err)
}
if err = cfg.GlobalPrometheus.Validate(); err != nil {
return fmt.Errorf("invalid global_prometheus: %w", err)
}
if cfg.GlobalPrometheus != nil {
cfg.BootstrapPrometheus = nil
}
if err = cfg.BootstrapPrometheus.Validate(); err != nil {
return fmt.Errorf("invalid bootstrap_prometheus: %w", err)
}
if cfg.ClickHouseSpaceManager.UsageThresholdPercent < 0 || cfg.ClickHouseSpaceManager.UsageThresholdPercent > 100 {
return fmt.Errorf("invalid usage_threshold_percent: %d", cfg.ClickHouseSpaceManager.UsageThresholdPercent)
}
return nil
}
func (cfg *Config) GetGlobalClickhouse() *db.IntegrationClickhouse {
clickhouse := cfg.GlobalClickhouse
if clickhouse == nil {
return nil
}
c := &db.IntegrationClickhouse{
Global: true,
Protocol: "native",
Addr: clickhouse.Address,
Auth: utils.BasicAuth{
User: clickhouse.User,
Password: clickhouse.Password,
},
Database: "",
InitialDatabase: clickhouse.Database,
TlsEnable: clickhouse.TlsEnable,
TlsSkipVerify: clickhouse.TlsSkipVerify,
TlsCAFile: clickhouse.TlsCAFile,
}
if c.Auth.User == "" {
c.Auth.User = "default"
}
if c.InitialDatabase == "" {
c.InitialDatabase = "default"
}
return c
}
func (cfg *Config) GetBootstrapClickhouse() *db.IntegrationClickhouse {
clickhouse := cfg.BootstrapClickhouse
if clickhouse == nil {
return nil
}
c := &db.IntegrationClickhouse{
Protocol: "native",
Addr: clickhouse.Address,
Auth: utils.BasicAuth{
User: clickhouse.User,
Password: clickhouse.Password,
},
Database: clickhouse.Database,
TlsEnable: clickhouse.TlsEnable,
TlsSkipVerify: clickhouse.TlsSkipVerify,
TlsCAFile: clickhouse.TlsCAFile,
}
if c.Auth.User == "" {
c.Auth.User = "default"
}
if c.Database == "" {
c.Database = "default"
}
return c
}
func (cfg *Config) GetGlobalPrometheus() *db.IntegrationPrometheus {
prometheus := cfg.GlobalPrometheus
if prometheus == nil {
return nil
}
p := &db.IntegrationPrometheus{
Global: true,
Url: prometheus.Url,
RefreshInterval: prometheus.RefreshInterval,
TlsSkipVerify: prometheus.TlsSkipVerify,
ExtraSelector: prometheus.ExtraSelector,
RemoteWriteUrl: prometheus.RemoteWriteUrl,
UseClickHouse: prometheus.UseClickHouse,
}
if prometheus.User != "" && prometheus.Password != "" {
p.BasicAuth = &utils.BasicAuth{
User: prometheus.User,
Password: prometheus.Password,
}
}
for k, v := range prometheus.CustomHeaders {
p.CustomHeaders = append(p.CustomHeaders, utils.Header{Key: k, Value: v})
}
return p
}
func (cfg *Config) GetBootstrapPrometheus() *db.IntegrationPrometheus {
prometheus := cfg.BootstrapPrometheus
if prometheus == nil {
return nil
}
p := &db.IntegrationPrometheus{
Url: prometheus.Url,
RefreshInterval: prometheus.RefreshInterval,
TlsSkipVerify: prometheus.TlsSkipVerify,
ExtraSelector: prometheus.ExtraSelector,
RemoteWriteUrl: prometheus.RemoteWriteUrl,
UseClickHouse: prometheus.UseClickHouse,
}
if prometheus.User != "" && prometheus.Password != "" {
p.BasicAuth = &utils.BasicAuth{
User: prometheus.User,
Password: prometheus.Password,
}
}
for k, v := range prometheus.CustomHeaders {
p.CustomHeaders = append(p.CustomHeaders, utils.Header{Key: k, Value: v})
}
return p
}