130 lines
4.9 KiB
Go
Executable File
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
|
|
}
|