/* 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 } } }