/* Copyright © 2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package cfg import ( "errors" "fmt" "os" "path/filepath" "reflect" "github.com/alecthomas/repr" "gopkg.in/yaml.v3" ) const ( Version string = `v0.0.26` ) var ( // initialized during build, see Makefile:buildlocal APIVERSION, GOVERSION, BUILD, COMMIT, BRANCH string ) type Config struct { ConfigFile string // -c CurrentCluster string // -C Debug bool // -d Output string // -o Clusters map[string]*Cluster DefaultCluster *Cluster HaveJQ bool // determined at runtime by ourselfes ProfileFile string // for internal use (golang profiling) AlignInts bool // -I Index string // index: -i Failed, Partials bool // index: flags Shards, Replicas int // index create+allocation: -s -r Wait bool // index create: -w Policy string // index create: -p Primary bool // index allocation: -p Searchable bool // index fields: -s Aggretable bool // index fields: -a Priority int // index template create: -p Settings []string // index template create: -s Patterns []string // index template create: -i Components []string // index template create: -c Meta []string // index template create: -M Aliases []string // index template create: -A Stream bool // index template create: -S AutoCreate bool // index template create: -a Mode string // index template create: -a Retention string // index template create: -r Rollover bool // index template create: -R From, To, MaxItems int // search: flags Filter []string // search: -F Path string // search+doc sh: -p Subhelp bool // search+doc sh: -H Tail bool // search: -f [tail] Or bool // search: -O Range string // search: -r TimestampFormat string // search: --timestamp-format Explain bool // search: -e Validate bool // search: --validate SortBy string // sort: -k Ascending bool // sort: -a All bool // doc + node ls: -a Exclude string // cluster compare: -e (regexp) Verbose bool // cluster status: -v Persistent, Transient, Default bool // cluster settings set: -p -t -D Force bool // ccr follower renew: -f DebugHTTP bool // root: --debug-http DebugGoRoutines bool // root: --debug-goroutines Separator string // role diff: -s NotDeployed bool // role diff: -n Undefined bool // role diff: -u Diff bool // role diff: -D Hidden bool // ds ls: -H // rollover MaxAge string MaxDocs, MaxShardSize, MaxShardDocs int // roll over DryRun bool // rollover: -n Tag string // api ls: -t HumanCat bool // api repl: -H Ilm Ilm // ilm create FromNode, ToNode string // cluster reroute move: -f + -t AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a Pager string // api repl: -p || PAGER } func NewConfig() *Config { return new(Config{Clusters: map[string]*Cluster{}}) } func getDefaultPath() string { return filepath.Join([]string{os.Getenv("HOME"), ".config", "esctl", "config.yaml"}...) } func (conf *Config) Init() error { DefaultConfig := getDefaultPath() if conf.ConfigFile == "" && fileExists(DefaultConfig) { conf.ConfigFile = DefaultConfig } switch { case conf.ConfigFile != "": if err := conf.LoadConfig(); err != nil { return err } default: if err := conf.LoadEnv(); err != nil { return err } } if err := conf.SetupES(); err != nil { return err } if err := conf.determineDefaultCluster(); err != nil { return err } conf.HaveJQ = isJQinstalled() conf.PrintDebug() return nil } // we are using reflect to clone a config obj w/o the ES stuff for // shorter repr.Println() output (the ES structure is just too large) func (conf *Config) Clone() Config { clone := Config{} ref := reflect.ValueOf(*conf) typeOfS := ref.Type() for idx := range ref.NumField() { field := typeOfS.Field(idx).Name if field == "Clusters" || field == "DefaultCluster" || !ref.Field(idx).CanInterface() { continue } reflect.ValueOf(&clone).Elem().FieldByName(field).Set(reflect.ValueOf(ref.Field(idx).Interface())) } return clone } func (conf *Config) PrintDebug() { if !conf.Debug { return } clone := conf.Clone() fmt.Println("config:") repr.Println(clone) } func (conf *Config) LoadEnv() error { cluster := Cluster{ Uri: os.Getenv("ES_URI"), User: os.Getenv("ES_USER"), Pass: os.Getenv("ES_PASS"), Token: os.Getenv("ES_TOKEN"), } switch { case cluster.Uri == "": return errors.New("ES_URI unset") case cluster.User == "" || cluster.Token == "": return errors.New("ES_USER and ES_TOKEN unset") } conf.Clusters["default"] = &cluster conf.DefaultCluster = &cluster return nil } func (conf *Config) LoadConfig() error { if conf.ConfigFile == "" { return nil } data, err := os.ReadFile(conf.ConfigFile) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } newconf := new(Config{}) err = yaml.Unmarshal(data, newconf) if err != nil { return fmt.Errorf("failed to unmarshal config file: %w", err) } if len(newconf.Clusters) > 0 { conf.Clusters = newconf.Clusters for _, cluster := range conf.Clusters { if cluster.Default { conf.DefaultCluster = cluster break } } if conf.DefaultCluster == nil { conf.DefaultCluster = &Cluster{} } } return nil } func (conf *Config) determineDefaultCluster() error { if conf.CurrentCluster != "" { // -C specified, set current cluster explicitly, no matter what the config says current, exists := conf.Clusters[conf.CurrentCluster] if !exists { return fmt.Errorf("no cluster with alias %s configured", conf.CurrentCluster) } else { conf.DefaultCluster = current // disable all others for _, cluster := range conf.Clusters { cluster.Default = false } conf.DefaultCluster.Default = true } } else { // we need to determine ourselfes if len(conf.Clusters) == 1 { // ok, just one cluster configured, use this, of course for name, cluster := range conf.Clusters { conf.DefaultCluster = cluster conf.CurrentCluster = name conf.DefaultCluster.Default = true } } else { // multiple ones exists, look if one is set as default for name, cluster := range conf.Clusters { if cluster.Default { conf.DefaultCluster = cluster conf.CurrentCluster = name } } } } return nil } func fileExists(filename string) bool { info, err := os.Stat(filename) if err != nil { // return false on any error return false } return !info.IsDir() }