From 489074da01779e8a5b8da379d4db11a0e68d66a2 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Thu, 2 Jul 2026 13:49:32 +0200 Subject: [PATCH] started implementing auto config loader --- pkg/cfg/automate.go | 174 ++++++++++++++++++++++++++++++++++++++++++++ pkg/cfg/cluster.go | 46 +++++++----- pkg/cfg/config.go | 15 +++- 3 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 pkg/cfg/automate.go diff --git a/pkg/cfg/automate.go b/pkg/cfg/automate.go new file mode 100644 index 0000000..f77b6bf --- /dev/null +++ b/pkg/cfg/automate.go @@ -0,0 +1,174 @@ +/* +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 ( + "bytes" + "context" + "errors" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +type Automator struct { + IsReachable bool + Error error + Env []string + Cluster *Cluster + + // all bash commands, .name must succeed first + Name string `yaml:"name"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Uri string `yaml:"uri"` +} + +type AutomatorRunner struct { + Output string + Error error +} + +func NewAutoEnv() []string { + return []string{ + "PATH=" + os.Getenv("PATH"), + "HOME=" + os.Getenv("HOME"), + "SHELL=" + os.Getenv("SHELL"), + "KUBECONFIG=" + os.Getenv("KUBECONFIG"), + } +} + +func NewAutomator() Automator { + autocfg := filepath.Join([]string{os.Getenv("HOME"), ".config", "esctl", "automate.yaml"}...) + + auto := Automator{Env: NewAutoEnv()} + + if !fileExists(autocfg) { + return auto + } + + data, err := os.ReadFile(autocfg) + if err != nil { + auto.Error = fmt.Errorf("failed to read config file: %w", err) + return auto + } + + err = yaml.Unmarshal(data, &auto) + if err != nil { + auto.Error = fmt.Errorf("failed to unmarshal config file: %w", err) + return auto + } + + hasname := execute(auto.Name, auto.Env) + if hasname.Error != nil { + auto.Error = hasname.Error + return auto + } + auto.Name = hasname.Output + + hasuser := execute(auto.User, auto.Env) + if hasuser.Error != nil { + auto.Error = hasuser.Error + return auto + } + auto.User = hasuser.Output + + haspass := execute(auto.Pass, auto.Env) + if haspass.Error != nil { + auto.Error = haspass.Error + return auto + } + auto.Pass = haspass.Output + + hasuri := execute(auto.Uri, auto.Env) + if hasuri.Error != nil { + auto.Error = hasuri.Error + return auto + } + auto.Uri = hasuri.Output + + auto.IsReachable = true + + auto.Cluster = &Cluster{ + Name: auto.Name, + Uri: auto.Uri, + User: auto.User, + Pass: auto.Pass, + } + + return auto +} + +// FIXME: execute auto.Exec, if defined, in a go routine and let it run forever, might be a tunnel +func (auto *Automator) Exec() {} + +// FIXME: cache automator results, only check auto.Name and if it matches the cache use those vars, but run auto.Exec anyway +func (auto *Automator) Cache() {} + +func execute(code string, env []string) *AutomatorRunner { + timeoutCtx, cancel := context.WithTimeout(context.Background(), + time.Duration(10)*time.Second) + defer cancel() + + var cmd *exec.Cmd + + // pipe code into bash + cmd = exec.CommandContext(timeoutCtx, "bash") + cmd.Stdin = strings.NewReader(code) + + cmd.Env = env + errbuf := &bytes.Buffer{} + cmd.Stderr = errbuf + + done := make(chan bool) + out := AutomatorRunner{} + + go func() { + output, err := cmd.Output() + + out.Output = strings.TrimSpace(string(output)) + + switch { + case err != nil: + out.Error = err + case errbuf.Len() > 0: + out.Error = fmt.Errorf(errbuf.String()) + case timeoutCtx.Err() == context.DeadlineExceeded: + out.Error = errors.New("timed out") + } + + slog.Debug("executed automator", + "code", code, + "output", out.Output, + "error", out.Error) + + done <- true + }() + + for { + select { + case <-done: + return &out + } + } +} diff --git a/pkg/cfg/cluster.go b/pkg/cfg/cluster.go index addf92b..b1b641f 100644 --- a/pkg/cfg/cluster.go +++ b/pkg/cfg/cluster.go @@ -223,27 +223,35 @@ func (cluster *Cluster) IsReachable() (bool, error) { return false, nil } -func (conf *Config) SetupES() error { +func (conf *Config) SetupElasticClient(name string, cluster *Cluster) error { + cluster.Name = name + cluster.DebugHTTP = conf.DebugHTTP + + opts := cluster.getDefaultOptions() + + switch { + case cluster.Pass != "" && cluster.User != "": + opts = append(opts, elasticsearch.WithBasicAuth(cluster.User, cluster.Pass)) + case cluster.Token != "": + opts = append(opts, elasticsearch.WithAPIKey(cluster.Token)) + } + + es, err := elasticsearch.NewTyped(opts...) + + if err != nil { + return fmt.Errorf("failed to setup elasticsearch connection: %w", err) + } + + cluster.SetClient(es) + + return nil +} + +func (conf *Config) SetupElasticClients() error { for name, cluster := range conf.Clusters { - cluster.Name = name - cluster.DebugHTTP = conf.DebugHTTP - - opts := cluster.getDefaultOptions() - - switch { - case cluster.Pass != "" && cluster.User != "": - opts = append(opts, elasticsearch.WithBasicAuth(cluster.User, cluster.Pass)) - case cluster.Token != "": - opts = append(opts, elasticsearch.WithAPIKey(cluster.Token)) + if err := conf.SetupElasticClient(name, cluster); err != nil { + return err } - - es, err := elasticsearch.NewTyped(opts...) - - if err != nil { - return fmt.Errorf("failed to setup elasticsearch connection: %w", err) - } - - cluster.SetClient(es) } return nil diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index ac61757..9ee56d7 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -28,7 +28,7 @@ import ( ) const ( - Version string = `v0.0.24` + Version string = `v0.0.25` ) var ( @@ -133,7 +133,7 @@ func (conf *Config) Init() error { } } - if err := conf.SetupES(); err != nil { + if err := conf.SetupElasticClients(); err != nil { return err } @@ -153,6 +153,9 @@ func (conf *Config) Init() error { conf.DefaultCluster.Default = true } } else { + // load auto config, if any + auto := NewAutomator() + // we need to determine ourselfes if len(conf.Clusters) == 1 { // ok, just one cluster configured, use this, of course @@ -161,6 +164,14 @@ func (conf *Config) Init() error { conf.CurrentCluster = name conf.DefaultCluster.Default = true } + } else if auto.Error == nil && auto.IsReachable { + // use auto conf + if err := conf.SetupElasticClient(auto.Name, auto.Cluster); err != nil { + return err + } + + conf.DefaultCluster = auto.Cluster + conf.CurrentCluster = auto.Name } else { // multiple ones exists, look if one is set as default for name, cluster := range conf.Clusters {