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