Read the config form beside the settings form in a manifest (part of issue 660)
The manifest's hooks for the config form: read into the fields the settings form fills, written back as read, and judged by ConfigProblems; the rules are in config.go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -530,6 +530,11 @@ type Manifest struct {
|
||||
// stays refused by name until a layer sets it. The mesh's layer, then the node's, override it.
|
||||
Settings map[string]SettingDeclaration `json:"settings,omitempty"`
|
||||
|
||||
// forms is which of the two forms its config is written in, `config` or its alias `settings` (config.go,
|
||||
// novox/hq ADR 0348 §1): the config form is read into Settings, a mergeable file's takes-settings and
|
||||
// `${setting:<key>}`, and written back as it was read.
|
||||
forms configForms
|
||||
|
||||
// unknown is the first key this manifest has that this controller does not know, at any depth, when
|
||||
// it was read from the store (novox/hq ADR 0262): a manifest a newer controller registered.
|
||||
// ParseManifest refuses it; the store's catalogue still loads, and the module is left out of every
|
||||
@@ -1321,10 +1326,25 @@ type manifestFields Manifest
|
||||
// strict read there failed the whole catalogue, and with it every plan and every send, the moment a
|
||||
// controller was rolled back. UnknownField says what was set aside.
|
||||
func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
// The config form read into the settings form's fields, so both compose by one path (config.go).
|
||||
raw, forms := toSettingsForm(raw)
|
||||
var keys map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &keys); err != nil {
|
||||
return err
|
||||
}
|
||||
if settings, has := keys[SettingsField]; has && string(settings) != "null" {
|
||||
forms.settings = append(forms.settings, SettingsField)
|
||||
}
|
||||
if config, has := keys[ConfigField]; has {
|
||||
if string(config) != "null" {
|
||||
forms.config = append(forms.config, ConfigField)
|
||||
}
|
||||
if _, both := keys[SettingsField]; !both {
|
||||
keys[SettingsField] = config
|
||||
}
|
||||
// Beside `settings` it is refused by name (MixedFormProblems), never merged into it.
|
||||
delete(keys, ConfigField)
|
||||
}
|
||||
plain := map[string]string{}
|
||||
many := map[string]map[string]string{}
|
||||
if secrets, ok := keys["secrets"]; ok && string(secrets) != "null" {
|
||||
@@ -1453,6 +1473,7 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
}
|
||||
*m = Manifest(fields)
|
||||
m.unknown = unknown
|
||||
m.forms = forms
|
||||
m.OptionalRequires = optionalRequires
|
||||
if len(plain) > 0 {
|
||||
m.Secrets = plain
|
||||
@@ -1470,8 +1491,16 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
}
|
||||
|
||||
// MarshalJSON writes `secrets` and `contributes` back in the shape they were read: single values,
|
||||
// and objects of local names.
|
||||
// and objects of local names; and its config in the form it was read in (config.go).
|
||||
func (m Manifest) MarshalJSON() ([]byte, error) {
|
||||
raw, err := m.marshalFields()
|
||||
if err != nil || !m.DeclaresConfig() {
|
||||
return raw, err
|
||||
}
|
||||
return toConfigForm(raw)
|
||||
}
|
||||
|
||||
func (m Manifest) marshalFields() ([]byte, error) {
|
||||
raw, err := json.Marshal(manifestFields(m))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1783,8 +1812,14 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, StateProblems(m)...)
|
||||
// And what it may call its logs, and how large it may ask them to grow (logs.go, novox/hq ADR 0297).
|
||||
problems = append(problems, LogProblems(m)...)
|
||||
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262).
|
||||
problems = append(problems, SettingProblems(m)...)
|
||||
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
|
||||
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
|
||||
if m.DeclaresConfig() {
|
||||
problems = append(problems, ConfigProblems(m)...)
|
||||
} else {
|
||||
problems = append(problems, SettingProblems(m)...)
|
||||
problems = append(problems, TypedSettingProblems(m)...)
|
||||
}
|
||||
// And what it requires optionally, and the files it delivers only while bound (novox/hq ADR 0330).
|
||||
problems = append(problems, OptionalProblems(m)...)
|
||||
// And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326).
|
||||
@@ -2341,6 +2376,14 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, healthProblems(m)...)
|
||||
// And whether a container is busy (novox/hq ADR 0335), likewise.
|
||||
problems = append(problems, busyProblems(m)...)
|
||||
if m.DeclaresConfig() {
|
||||
// Said in the words it was written in: the rules shared with the settings form say ${setting:<key>}
|
||||
// and takes-settings.
|
||||
for i, p := range problems {
|
||||
problems[i] = inConfigWords(p)
|
||||
}
|
||||
problems = append(problems, MixedFormProblems(m)...)
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
sort.Strings(problems)
|
||||
return Manifest{}, fmt.Errorf("this manifest cannot be used:\n - %s",
|
||||
|
||||
@@ -31,6 +31,28 @@ type SettingDeclaration struct {
|
||||
Default any `json:"default"`
|
||||
// Why this default: one sentence for whoever wonders whether to change it.
|
||||
Why string `json:"why"`
|
||||
|
||||
// The config form's declaration (novox/hq ADR 0348 §4, config.go): none of these is said under
|
||||
// `settings`, and neither Kind nor Why under `config`.
|
||||
|
||||
// Type is number, string or boolean; object or list only when Trusted.
|
||||
Type string `json:"type,omitempty"`
|
||||
// Description says what the value is, including its unit.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Enum, Minimum, Maximum and Pattern are its validation: the values it may take, the bounds of a
|
||||
// number, and the whole of a string matching the pattern.
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
// Trusted is a value that decides what runs or who can reach what (ADR 0348 §8).
|
||||
Trusted bool `json:"trusted,omitempty"`
|
||||
// Predefined is the predefined key the value falls back to, of the same type (ADR 0348 §2, §7).
|
||||
Predefined string `json:"predefined,omitempty"`
|
||||
// Required is the operator's own value: it has no default (ADR 0348 §4).
|
||||
Required bool `json:"required,omitempty"`
|
||||
// Runtime is a value the module's code reads at runtime rather than one placed in a file (ADR 0348 §5).
|
||||
Runtime bool `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
// KindPreference is the kind of a setting that may have a default.
|
||||
@@ -203,6 +225,10 @@ type SettingSource struct {
|
||||
func Effective(m Manifest, layers []Layer) []SettingSource {
|
||||
byKey := map[string]*SettingSource{}
|
||||
for key, d := range m.Settings {
|
||||
if d.Default == nil {
|
||||
// A required config value has no default (ADR 0348 §4); a settings declaration always has one.
|
||||
continue
|
||||
}
|
||||
byKey[key] = &SettingSource{Key: key, Value: d.Default, From: DefaultLayer, FromDefault: true,
|
||||
Default: d.Default, HasDefault: true}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ func TakenSettingsProblems(m Manifest) []string {
|
||||
// Said only when none of the module's mergeable files names its keys: when one does, §2 already
|
||||
// names every file that does not, and a file is not reported twice.
|
||||
func WrittenSettingsProblems(m Manifest) []string {
|
||||
if len(m.Settings) == 0 {
|
||||
if len(m.Settings) == 0 || m.DeclaresConfig() {
|
||||
// A module in the config form is held to this where it is read, in its own words (ConfigProblems).
|
||||
return nil
|
||||
}
|
||||
var silent []string
|
||||
|
||||
Reference in New Issue
Block a user