sorcery-go/pkg/cast/query.go

130 lines
4.9 KiB
Go
Executable File

// Interactive Configuration Engine (ICE).
//
// ICE preserves the original Source Mage feel: every spell's CONFIGURE
// script can ask "Support OpenSSL? (y/n)" and the answer is recorded in
// the Tablet (BoltDB). Next time you cast the same spell, the previous
// answers are reused so the build is reproducible.
//
// The CLI version uses a simple fmt.Scanln prompt. The TUI version wraps
// the same Query() with a bubbletea menu. The WebUI version replaces it
// with a modal popup. All three write through the same Tablet API so the
// result is identical regardless of which interface the admin used.
//
// RunICE parses the CONFIGURE script looking for `config_query` directives
// (the standard SMGL idiom), then asks the user about each one. The
// answers are returned as a map and also persisted in the Tablet.
package cast
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
"dcos.net/sorcery-go/pkg/eventbus"
"dcos.net/sorcery-go/pkg/grimoire"
"dcos.net/sorcery-go/pkg/state"
)
// configQueryRe matches lines like:
//
// config_query WGET_SSL "Enable SSL support?" y
// config_query WGET_IPV6 "Enable IPv6?" n
//
// Captures: varname, prompt, default ("y", "n", or empty).
var configQueryRe = regexp.MustCompile(
`^config_query\s+(\S+)\s+"([^"]+)"\s*([yn]?)`)
// QueryResult is one y/n answer.
type QueryResult struct {
Option string
Description string
Value bool
}
// RunICE parses the spell's CONFIGURE script, finds every config_query
// directive, and asks the user about each one (unless the Tablet already
// has an answer and reconfigure is false). Returns a map[option]value.
func RunICE(stateMgr *state.Manager, spell *grimoire.Spell, configurePath string, reconfigure bool, bus *eventbus.Bus, taskID string) (map[string]bool, error) {
out := make(map[string]bool)
f, err := os.Open(configurePath)
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
m := configQueryRe.FindStringSubmatch(line)
if m == nil {
continue
}
varName := m[1]
description := m[2]
defaultVal := m[3] == "y"
val := Query(stateMgr, spell.Name, varName, description, defaultVal, reconfigure)
out[varName] = val
if bus != nil {
bus.Log(taskID, fmt.Sprintf(" ICE: %s = %v", varName, val))
}
}
return out, scanner.Err()
}
// Query is the core ICE primitive. It checks the Tablet first; if no
// answer is recorded, it asks the user via stdin and saves the answer.
//
// If `reconfigure` is true, the user is asked even when the Tablet has
// an existing answer — this mirrors `sorcery cast -r` from the original.
//
// Non-interactive contexts (HPC grid, --default flag) should call
// QueryNonInteractive instead.
// yesResponses maps affirmative user inputs to true.
var yesResponses = map[string]bool{
"y": true, "yes": true, "1": true, "true": true,
}
func Query(stateMgr *state.Manager, spell, option, description string, defaultVal bool, reconfigure bool) bool {
if !reconfigure {
if val, ok := stateMgr.GetTablet(spell, option); ok {
return val
}
}
prompt := fmt.Sprintf("? [%s] %s? (y/n) [default: %v]: ", spell, description, defaultVal)
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(strings.ToLower(line))
if val, ok := yesResponses[line]; ok {
_ = stateMgr.SaveTablet(spell, option, val)
return val
}
_ = stateMgr.SaveTablet(spell, option, defaultVal)
return defaultVal
}
// QueryNonInteractive is the HPC-friendly path. It accepts defaults without
// prompting — used by `sorcery cast -d <spell>` and by the WebUI "Use Defaults"
// button.
func QueryNonInteractive(stateMgr *state.Manager, spell, option string, defaultVal bool) bool {
_ = stateMgr.SaveTablet(spell, option, defaultVal)
return defaultVal
}
// QueryMatrix runs the same query across all maintained arches simultaneously.
// Used by `sorcery cast -m <spell>` so an admin can build x86_64 and aarch64
// variants of the same spell with identical y/n answers in one shot.
func QueryMatrix(stateMgr *state.Manager, spell, option, description string, defaultVal bool, arches []string) map[string]bool {
out := make(map[string]bool, len(arches))
val := Query(stateMgr, spell, option, description, defaultVal, false)
for _, a := range arches {
_ = stateMgr.SaveTablet(spell, option+"@"+a, val)
out[a] = val
}
return out
}