/* 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 ( "crypto/tls" "errors" "fmt" "net/http" "os" "github.com/alecthomas/repr" "github.com/elastic/elastic-transport-go/v8/elastictransport" "github.com/elastic/go-elasticsearch/v9" "gopkg.in/yaml.v3" ) const ( Version string = `v0.0.4` ) type Cluster struct { Uri, User, Pass string ES *elasticsearch.TypedClient } type Config struct { ConfigFile string // -c CurrentCluster string // -C Debug bool // -d Clusters map[string]*Cluster DefaultCluster *Cluster Index string // index: -i Failed, Partials bool // index: flags Shards, Replicas int // index create: -s -r Wait bool // index create: -w From, To, MaxItems int // search: flags Filter []string // search: -F Exclude string // cluster compare: -e (regexp) All bool // cluster status: -a } func NewConfig() *Config { return &Config{Clusters: map[string]*Cluster{}} } func (conf *Config) Init() error { DefaultConfig := os.Getenv("HOME") + "/.config/esctl/config.yaml" switch { case fileExists(DefaultConfig): conf.ConfigFile = DefaultConfig fallthrough case conf.ConfigFile != "": if err := conf.LoadConfig(); err != nil { return err } default: if err := conf.LoadEnv(); err != nil { return err } } if conf.CurrentCluster != "" { current, exists := conf.Clusters[conf.CurrentCluster] if !exists { return fmt.Errorf("no cluster with alias %s configured", conf.CurrentCluster) } else { conf.DefaultCluster = current } } if conf.Debug { repr.Println(conf) } conf.SetupES() return nil } func (conf *Config) LoadEnv() error { cluster := Cluster{ Uri: os.Getenv("ES_URI"), User: os.Getenv("ES_USER"), Pass: os.Getenv("ES_PASS"), } switch { case cluster.Uri == "": return errors.New("ES_URI unset") case cluster.User == "": return errors.New("ES_USER unset") case cluster.Pass == "": return errors.New("ES_PASS 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 := &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 _, exists := conf.Clusters["default"] if !exists { // no "default", just use the first we stumble upon for _, cluster := range conf.Clusters { conf.DefaultCluster = cluster break } } else { conf.DefaultCluster = newconf.Clusters["default"] } } return nil } func (conf *Config) SetupES() error { for _, cluster := range conf.Clusters { es, err := elasticsearch.NewTyped( elasticsearch.WithAddresses(cluster.Uri), elasticsearch.WithBasicAuth(cluster.User, cluster.Pass), elasticsearch.WithTransportOptions( elastictransport.WithTransport( &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, ), ), ) if err != nil { return fmt.Errorf("failed to setup elasticsearch connection: %w", err) } cluster.ES = es } 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() }