Ctrl K
ComunidadGuías…

config/config.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}