-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathintent.go
More file actions
264 lines (224 loc) · 7.35 KB
/
intent.go
File metadata and controls
264 lines (224 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
package seiconfig
import (
"fmt"
"slices"
)
// ConfigIntent declares the desired configuration state for a Sei node.
// It is the portable contract between the controller, sidecar, and CLI.
// sei-config owns the full resolution pipeline: intent -> validated SeiConfig.
//
// The controller builds an intent from the CRD spec, the sidecar resolves it.
// The controller never calls DefaultForMode, ApplyOverrides, or Validate
// directly — it just constructs an intent and sends it through.
type ConfigIntent struct {
// Mode is the node's operating role (validator, full, seed, archive).
Mode NodeMode `json:"mode"`
// Overrides is a flat map of dotted TOML key paths to string values.
// These are applied on top of mode defaults.
Overrides map[string]string `json:"overrides,omitempty"`
// TargetVersion is the desired config schema version.
// When zero, uses CurrentVersion (the latest known by this library).
// Set explicitly when deploying a custom binary that expects a specific
// config version.
TargetVersion int `json:"targetVersion,omitempty"`
// Incremental means "read existing on-disk config and patch it" rather
// than "generate from mode defaults." Used for day-2 changes.
Incremental bool `json:"incremental,omitempty"`
}
// ConfigResult is the output of intent resolution. It contains the resolved
// config, diagnostics, and a validity flag.
type ConfigResult struct {
// Config is the fully resolved SeiConfig. Nil when Valid is false.
Config *SeiConfig `json:"config,omitempty"`
// Version is the config schema version of the resolved config.
Version int `json:"version"`
// Mode is the mode of the resolved config.
Mode NodeMode `json:"mode"`
// Diagnostics contains all validation findings (errors, warnings, info).
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
// Valid is true when no error-level diagnostics exist.
Valid bool `json:"valid"`
}
func (r *ConfigResult) addError(field, msg string) {
r.Diagnostics = append(r.Diagnostics, Diagnostic{SeverityError, field, msg})
r.Valid = false
}
func (r *ConfigResult) addWarning(field, msg string) {
r.Diagnostics = append(r.Diagnostics, Diagnostic{SeverityWarning, field, msg})
}
// ValidateIntent checks whether a ConfigIntent is well-formed without
// producing a resolved config. This enables dry-run validation by the
// controller before submitting a task to the sidecar.
//
// Checks performed:
// - Mode is valid
// - TargetVersion is within the supported range
// - All override keys exist in the Registry
// - Version-required fields for the mode are satisfied
func ValidateIntent(intent ConfigIntent) *ConfigResult {
result := &ConfigResult{
Version: resolveTargetVersion(intent.TargetVersion),
Mode: intent.Mode,
Valid: true,
}
if !intent.Incremental {
validateIntentMode(result, intent)
}
validateIntentVersion(result, intent)
registry := BuildRegistry()
registry.EnrichAll(DefaultEnrichments())
validateIntentOverrideKeys(result, intent, registry)
validateIntentRequiredFields(result, intent, registry)
return result
}
// ResolveIntent produces a fully resolved, validated SeiConfig from an intent.
// This is the primary entry point for non-incremental (bootstrap) config
// generation. The full pipeline is:
//
// 1. Resolve target version
// 2. Generate mode defaults
// 3. Apply overrides
// 4. Validate the result
// 5. Return ConfigResult
func ResolveIntent(intent ConfigIntent) (*ConfigResult, error) {
result := &ConfigResult{
Version: resolveTargetVersion(intent.TargetVersion),
Mode: intent.Mode,
Valid: true,
}
if intent.Mode == "" {
return nil, fmt.Errorf("mode is required for non-incremental config resolution")
}
if !intent.Mode.IsValid() {
return nil, fmt.Errorf("invalid mode %q", intent.Mode)
}
cfg := DefaultForMode(intent.Mode)
cfg.Version = result.Version
if err := ApplyOverrides(cfg, intent.Overrides); err != nil {
return nil, fmt.Errorf("applying overrides: %w", err)
}
vr := ValidateWithOpts(cfg, ValidateOpts{MaxVersion: result.Version})
result.Diagnostics = vr.Diagnostics
result.Valid = !vr.HasErrors()
if result.Valid {
result.Config = cfg
}
return result, nil
}
// ResolveIncrementalIntent resolves an incremental intent against an existing
// on-disk config. Used for day-2 patches where the base config already exists.
func ResolveIncrementalIntent(intent ConfigIntent, current *SeiConfig) (*ConfigResult, error) {
if current == nil {
return nil, fmt.Errorf("current config is required for incremental resolution")
}
result := &ConfigResult{
Version: resolveTargetVersion(intent.TargetVersion),
Valid: true,
}
copied := *current
cfg := &copied
if intent.Mode != "" {
cfg.Mode = intent.Mode
}
result.Mode = cfg.Mode
if err := ApplyOverrides(cfg, intent.Overrides); err != nil {
return nil, fmt.Errorf("applying incremental overrides: %w", err)
}
vr := ValidateWithOpts(cfg, ValidateOpts{MaxVersion: result.Version})
result.Diagnostics = vr.Diagnostics
result.Valid = !vr.HasErrors()
if result.Valid {
result.Config = cfg
result.Version = cfg.Version
}
return result, nil
}
func resolveTargetVersion(requested int) int {
if requested > 0 {
return requested
}
return CurrentVersion
}
func validateIntentMode(result *ConfigResult, intent ConfigIntent) {
if intent.Mode == "" {
result.addError("mode", "mode is required for non-incremental config generation")
return
}
if !intent.Mode.IsValid() {
result.addError("mode", fmt.Sprintf(
"unknown mode %q; valid modes: validator, full, seed, archive", intent.Mode))
}
}
func validateIntentVersion(result *ConfigResult, intent ConfigIntent) {
tv := resolveTargetVersion(intent.TargetVersion)
if tv < 1 {
result.addError("targetVersion", "target version must be >= 1")
}
if tv > CurrentVersion {
result.addError("targetVersion", fmt.Sprintf(
"target version %d exceeds maximum supported version %d",
tv, CurrentVersion))
}
}
func validateIntentOverrideKeys(result *ConfigResult, intent ConfigIntent, registry *Registry) {
if len(intent.Overrides) == 0 {
return
}
for key := range intent.Overrides {
if registry.Field(key) == nil {
result.addError("overrides."+key, fmt.Sprintf("unknown config field %q", key))
}
}
}
func validateIntentRequiredFields(result *ConfigResult, intent ConfigIntent, registry *Registry) {
if intent.Incremental {
return
}
tv := resolveTargetVersion(intent.TargetVersion)
for _, field := range registry.Fields() {
if field.SinceVersion <= 0 || field.SinceVersion > tv {
continue
}
if len(field.RequiredForModes) == 0 {
continue
}
if !slices.Contains(field.RequiredForModes, intent.Mode) {
continue
}
// Field is required for this mode+version. Check if it's in overrides
// or has a non-zero default.
if _, ok := intent.Overrides[field.Key]; ok {
continue
}
defaults := registry.DefaultsByMode(intent.Mode)
if v, ok := defaults[field.Key]; ok && !isZeroValue(v) {
continue
}
result.addError(field.Key, fmt.Sprintf(
"field %q is required for mode %q in config version %d",
field.Key, intent.Mode, field.SinceVersion))
}
}
func isZeroValue(v any) bool {
if v == nil {
return true
}
switch val := v.(type) {
case string:
return val == ""
case int:
return val == 0
case int64:
return val == 0
case uint:
return val == 0
case uint64:
return val == 0
case float64:
return val == 0
case bool:
return !val
default:
return false
}
}