From 996c06459c1b23a763483078465b5919faabdbee Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Thu, 26 Mar 2026 16:09:58 +0300 Subject: [PATCH 01/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/modules/basic.go | 6 ++ pkg/module_manager/models/modules/global.go | 34 ++++++++++- .../models/modules/values_storage.go | 8 +++ pkg/module_manager/module_manager.go | 19 ++++++- pkg/module_manager/module_manager_hooks.go | 1 + pkg/utils/values_patch.go | 27 +++++++++ pkg/values/validation/schema/override.go | 56 +++++++++++++++++++ pkg/values/validation/schemas.go | 19 +++++++ 8 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 pkg/values/validation/schema/override.go diff --git a/pkg/module_manager/models/modules/basic.go b/pkg/module_manager/models/modules/basic.go index 5cbbfcf07..23db4e44c 100644 --- a/pkg/module_manager/models/modules/basic.go +++ b/pkg/module_manager/models/modules/basic.go @@ -30,6 +30,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" + "github.com/flant/addon-operator/pkg/values/validation/schema" "github.com/flant/addon-operator/sdk" shapp "github.com/flant/shell-operator/pkg/app" "github.com/flant/shell-operator/pkg/executor" @@ -1344,6 +1345,11 @@ func (bm *BasicModule) GetSchemaStorage() *validation.SchemaStorage { return bm.valuesStorage.schemaStorage } +// OverrideSchemaDefaults overrides values schema openAPI spec defaults +func (bm *BasicModule) OverrideSchemaDefaults(override ...schema.DefaultOverride) { + bm.valuesStorage.OverrideDefaults(override...) +} + // ApplyNewSchemaStorage updates schema storage of the basic module func (bm *BasicModule) ApplyNewSchemaStorage(schema *validation.SchemaStorage) error { return bm.valuesStorage.applyNewSchemaStorage(schema) diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index e1e7dfbb4..62241beb5 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -37,8 +37,9 @@ type GlobalModule struct { valuesStorage *ValuesStorage - enabledByHookC chan *EnabledPatchReport - hasReadiness bool + enabledByHookC chan *EnabledPatchReport + overrideByHookC chan *OverridePatchReport + hasReadiness bool // dependency dc *hooks.HookExecutionDependencyContainer @@ -53,6 +54,11 @@ func (gm *GlobalModule) EnabledReportChannel() chan *EnabledPatchReport { return gm.enabledByHookC } +// OverrideReportChannel returns channel with dynamic openAPI override by global hooks +func (gm *GlobalModule) OverrideReportChannel() chan *OverridePatchReport { + return gm.overrideByHookC +} + // NewGlobalModule build ephemeral global container for global hooks and values func NewGlobalModule(hooksDir string, staticValues utils.Values, dc *hooks.HookExecutionDependencyContainer, configBytes, valuesBytes []byte, keepTemporaryHookFiles bool, opts ...ModuleOption, @@ -69,6 +75,7 @@ func NewGlobalModule(hooksDir string, staticValues utils.Values, dc *hooks.HookE valuesStorage: valuesStorage, dc: dc, enabledByHookC: make(chan *EnabledPatchReport, 10), + overrideByHookC: make(chan *OverridePatchReport, 10), keepTemporaryHookFiles: keepTemporaryHookFiles, } @@ -306,6 +313,8 @@ func (gm *GlobalModule) executeHook(ctx context.Context, h *hooks.GlobalHook, bi if err != nil { return fmt.Errorf("apply enabled patches from global values patch: %v", err) } + + gm.applyDefaultsOverride(*valuesPatch) } return nil @@ -335,6 +344,27 @@ func (gm *GlobalModule) applyEnabledPatches(valuesPatch utils.ValuesPatch) error return err } +type OverridePatchReport struct { + Override utils.DefaultsOverride + Done chan struct{} +} + +func (gm *GlobalModule) applyDefaultsOverride(valuesPatch utils.ValuesPatch) { + override := utils.DefaultsOverrideFromValuesPatch(valuesPatch) + if len(override.Override) == 0 { + return + } + + report := &OverridePatchReport{ + Override: override, + Done: make(chan struct{}), + } + + <-report.Done + + return +} + func (gm *GlobalModule) GetValues(withPrefix bool) utils.Values { return gm.valuesStorage.GetValues(withPrefix) } diff --git a/pkg/module_manager/models/modules/values_storage.go b/pkg/module_manager/models/modules/values_storage.go index 0219dc2fa..678613252 100644 --- a/pkg/module_manager/models/modules/values_storage.go +++ b/pkg/module_manager/models/modules/values_storage.go @@ -6,6 +6,7 @@ import ( "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" + "github.com/flant/addon-operator/pkg/values/validation/schema" ) /* @@ -268,3 +269,10 @@ func (vs *ValuesStorage) getValuesPatches() []utils.ValuesPatch { func (vs *ValuesStorage) GetSchemaStorage() *validation.SchemaStorage { return vs.schemaStorage } + +func (vs *ValuesStorage) OverrideDefaults(override ...schema.DefaultOverride) { + vs.lock.Lock() + defer vs.lock.Unlock() + + vs.schemaStorage.OverrideDefaults(validation.ValuesSchema, override...) +} diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index f294dbb2c..32b8d16ee 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -39,6 +39,7 @@ import ( static_extender "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders/static" "github.com/flant/addon-operator/pkg/task" "github.com/flant/addon-operator/pkg/utils" + "github.com/flant/addon-operator/pkg/values/validation/schema" . "github.com/flant/shell-operator/pkg/hook/binding_context" "github.com/flant/shell-operator/pkg/hook/controller" . "github.com/flant/shell-operator/pkg/hook/types" @@ -980,7 +981,7 @@ func (mm *ModuleManager) HandleScheduleEvent( } func (mm *ModuleManager) CreateTasksByBinding(binding BindingType, createTasksFunc func(gh *hooks.GlobalHook, m *modules.BasicModule, mh *hooks.ModuleHook) []sh_task.Task) []sh_task.Task { - var allTasks []sh_task.Task //nolint: prealloc + var allTasks []sh_task.Task // nolint: prealloc // Process global hooks allTasks = append(allTasks, mm.createTasksFromGlobalHooks(binding, createTasksFunc)...) @@ -1028,6 +1029,22 @@ func (mm *ModuleManager) createTasksFromModuleHooks(binding BindingType, createT return tasks } +func (mm *ModuleManager) runDynamicDefaultsOverrideLoop() { + for report := range mm.global.OverrideReportChannel() { + mm.applyDefaultsOverride(report.Override.Name, report.Override.Override) + report.Done <- struct{}{} + } +} + +func (mm *ModuleManager) applyDefaultsOverride(name string, override []schema.DefaultOverride) { + basic := mm.GetModule(name) + if basic == nil { + return + } + + basic.OverrideSchemaDefaults(override...) +} + func (mm *ModuleManager) runDynamicEnabledLoop(extender *dynamic_extender.Extender) { for report := range mm.global.EnabledReportChannel() { err := mm.applyEnabledPatch(report.Patch, extender) diff --git a/pkg/module_manager/module_manager_hooks.go b/pkg/module_manager/module_manager_hooks.go index 93dfc0510..49cf2b0ce 100644 --- a/pkg/module_manager/module_manager_hooks.go +++ b/pkg/module_manager/module_manager_hooks.go @@ -106,6 +106,7 @@ func (mm *ModuleManager) registerGlobalModule(globalValues utils.Values, configB } // catch dynamin Enabled patches from global hooks go mm.runDynamicEnabledLoop(dynamicExtender) + go mm.runDynamicDefaultsOverrideLoop() return mm.registerGlobalHooks(gm) } diff --git a/pkg/utils/values_patch.go b/pkg/utils/values_patch.go index 2027fc005..172b86cd5 100644 --- a/pkg/utils/values_patch.go +++ b/pkg/utils/values_patch.go @@ -12,6 +12,8 @@ import ( sdkutils "github.com/deckhouse/module-sdk/pkg/utils" lazynode "github.com/deckhouse/module-sdk/pkg/utils/lazy-node" "github.com/deckhouse/module-sdk/pkg/utils/patch" + + "github.com/flant/addon-operator/pkg/values/validation/schema" ) type ValuesPatchType string @@ -21,6 +23,11 @@ const ( MemoryValuesPatch ValuesPatchType = "MEMORY_VALUES_PATCH" ) +type DefaultsOverride struct { + Name string + Override []schema.DefaultOverride +} + type ValuesPatch struct { Operations []*sdkutils.ValuesPatchOperation } @@ -376,6 +383,26 @@ func EnabledFromValuesPatch(valuesPatch ValuesPatch) ValuesPatch { return newValuesPatch } +func DefaultsOverrideFromValuesPatch(valuesPatch ValuesPatch) DefaultsOverride { + override := DefaultsOverride{} + for _, op := range valuesPatch.Operations { + pathParts := strings.Split(op.Path, "/") + if len(pathParts) > 2 { + if pathParts[1] == "override" { + override.Name = pathParts[2] + + var overrides []schema.DefaultOverride + if err := json.Unmarshal(op.Value, &overrides); err != nil { + continue + } + override.Override = append(override.Override, overrides...) + } + } + } + + return override +} + // Error messages to distinguish non-typed errors from the 'json-patch' library. const ( NonExistentPathErrorMsg = "error in remove for path:" diff --git a/pkg/values/validation/schema/override.go b/pkg/values/validation/schema/override.go new file mode 100644 index 000000000..4ea2e9875 --- /dev/null +++ b/pkg/values/validation/schema/override.go @@ -0,0 +1,56 @@ +package schema + +import ( + "strings" + + "github.com/go-openapi/spec" +) + +// DefaultOverride defines a single default value override for a schema property. +// Path uses "/" as a separator (JSON Pointer style, without leading slash). +type DefaultOverride struct { + Path string `json:"path" yaml:"path"` + Value any `json:"value" yaml:"value"` +} + +// DefaultsTransformer is a SchemaTransformer that overrides default values +// on schema properties identified by "/"-separated paths. +type DefaultsTransformer struct { + Overrides []DefaultOverride +} + +func (t *DefaultsTransformer) Transform(s *spec.Schema) *spec.Schema { + if s == nil || len(t.Overrides) == 0 { + return s + } + + for _, override := range t.Overrides { + segments := strings.Split(override.Path, "/") + setDefault(s, segments, override.Value) + } + + return s +} + +// setDefault walks the schema tree through Properties and sets +// the Default field on the leaf property. +func setDefault(s *spec.Schema, segments []string, value any) { + if len(segments) == 0 || s == nil { + return + } + + propName := segments[0] + prop, exists := s.Properties[propName] + if !exists { + return + } + + if len(segments) == 1 { + prop.Default = value + s.Properties[propName] = prop + return + } + + setDefault(&prop, segments[1:], value) + s.Properties[propName] = prop +} diff --git a/pkg/values/validation/schemas.go b/pkg/values/validation/schemas.go index 88835e7ed..b9a02fa31 100644 --- a/pkg/values/validation/schemas.go +++ b/pkg/values/validation/schemas.go @@ -305,3 +305,22 @@ func PrepareSchemas(configBytes, valuesBytes []byte) (map[SchemaType]*spec.Schem return res, nil } + +// OverrideDefaults overrides default values on stored schema at runtime. +func (st *SchemaStorage) OverrideDefaults(schemaType SchemaType, overrides ...schema.DefaultOverride) { + if len(overrides) == 0 { + return + } + + scheme := st.Schemas[schemaType] + if scheme == nil { + return + } + + if len(scheme.Properties) == 0 { + scheme.Properties = make(map[string]spec.Schema) + } + + defaults := &schema.DefaultsTransformer{Overrides: overrides} + defaults.Transform(scheme) +} From 92630fa37eda2c4784d84bdb5863e8e9da5781ad Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Thu, 26 Mar 2026 16:32:25 +0300 Subject: [PATCH 02/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/module_manager.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index 32b8d16ee..8a4a1a4a0 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -39,7 +39,6 @@ import ( static_extender "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders/static" "github.com/flant/addon-operator/pkg/task" "github.com/flant/addon-operator/pkg/utils" - "github.com/flant/addon-operator/pkg/values/validation/schema" . "github.com/flant/shell-operator/pkg/hook/binding_context" "github.com/flant/shell-operator/pkg/hook/controller" . "github.com/flant/shell-operator/pkg/hook/types" @@ -1031,18 +1030,14 @@ func (mm *ModuleManager) createTasksFromModuleHooks(binding BindingType, createT func (mm *ModuleManager) runDynamicDefaultsOverrideLoop() { for report := range mm.global.OverrideReportChannel() { - mm.applyDefaultsOverride(report.Override.Name, report.Override.Override) - report.Done <- struct{}{} - } -} + basic := mm.GetModule(report.Override.Name) + if basic == nil { + return + } -func (mm *ModuleManager) applyDefaultsOverride(name string, override []schema.DefaultOverride) { - basic := mm.GetModule(name) - if basic == nil { - return + basic.OverrideSchemaDefaults(report.Override.Override...) + report.Done <- struct{}{} } - - basic.OverrideSchemaDefaults(override...) } func (mm *ModuleManager) runDynamicEnabledLoop(extender *dynamic_extender.Extender) { From 83ea4ee07d154eed92c3b685e1b7eb95e6e0f749 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Thu, 26 Mar 2026 16:33:34 +0300 Subject: [PATCH 03/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/module_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index 8a4a1a4a0..5ffb69ef3 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -980,7 +980,7 @@ func (mm *ModuleManager) HandleScheduleEvent( } func (mm *ModuleManager) CreateTasksByBinding(binding BindingType, createTasksFunc func(gh *hooks.GlobalHook, m *modules.BasicModule, mh *hooks.ModuleHook) []sh_task.Task) []sh_task.Task { - var allTasks []sh_task.Task // nolint: prealloc + var allTasks []sh_task.Task //nolint: prealloc // Process global hooks allTasks = append(allTasks, mm.createTasksFromGlobalHooks(binding, createTasksFunc)...) From a556804a708ba0d18106911d525f508f665edca4 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Thu, 26 Mar 2026 16:34:29 +0300 Subject: [PATCH 04/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/modules/global.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index 62241beb5..172f2baf9 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -361,8 +361,6 @@ func (gm *GlobalModule) applyDefaultsOverride(valuesPatch utils.ValuesPatch) { } <-report.Done - - return } func (gm *GlobalModule) GetValues(withPrefix bool) utils.Values { From 594db3aa9984820ae024f88f215b629c5ec80eb3 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sat, 28 Mar 2026 17:43:48 +0300 Subject: [PATCH 05/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- .../models/modules/values_storage.go | 25 +- pkg/utils/values_patch.go | 27 -- .../validation/defaultsoverride/policy.go | 203 ++++++++++++++ .../defaultsoverride/policy_test.go | 251 ++++++++++++++++++ pkg/values/validation/schema/override.go | 56 ---- pkg/values/validation/schemas.go | 19 -- 6 files changed, 470 insertions(+), 111 deletions(-) create mode 100644 pkg/values/validation/defaultsoverride/policy.go create mode 100644 pkg/values/validation/defaultsoverride/policy_test.go delete mode 100644 pkg/values/validation/schema/override.go diff --git a/pkg/module_manager/models/modules/values_storage.go b/pkg/module_manager/models/modules/values_storage.go index 678613252..636e8388e 100644 --- a/pkg/module_manager/models/modules/values_storage.go +++ b/pkg/module_manager/models/modules/values_storage.go @@ -6,7 +6,7 @@ import ( "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" - "github.com/flant/addon-operator/pkg/values/validation/schema" + "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" ) /* @@ -28,6 +28,8 @@ type ValuesStorage struct { schemaStorage *validation.SchemaStorage moduleName string + overridePolicy *defaultsoverride.Policy + // we are locking the whole storage on any concurrent operation // because it could be called from concurrent hooks (goroutines) and we will have a deadlock on RW mutex lock sync.Mutex @@ -56,19 +58,20 @@ type Registry struct { // NewValuesStorage build a new storage for module values // // staticValues - values from /modules//values.yaml, which couldn't be reloaded during the runtime -func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, valuesBytes []byte) (*ValuesStorage, error) { +func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, valuesBytes []byte, contracts []defaultsoverride.Contract) (*ValuesStorage, error) { schemaStorage, err := validation.NewSchemaStorage(configBytes, valuesBytes) if err != nil { return nil, fmt.Errorf("new schema storage: %w", err) } vs := &ValuesStorage{ - staticValues: staticValues, - schemaStorage: schemaStorage, - moduleName: moduleName, + staticValues: staticValues, + schemaStorage: schemaStorage, + moduleName: moduleName, + overridePolicy: defaultsoverride.PolicyByContracts(contracts...), } - err = vs.calculateResultValues() - if err != nil { + + if err = vs.calculateResultValues(); err != nil { return nil, fmt.Errorf("critical error occurred with calculating values for %q: %w", moduleName, err) } @@ -270,9 +273,13 @@ func (vs *ValuesStorage) GetSchemaStorage() *validation.SchemaStorage { return vs.schemaStorage } -func (vs *ValuesStorage) OverrideDefaults(override ...schema.DefaultOverride) { +func (vs *ValuesStorage) ApplyDefaultsOverride(override defaultsoverride.Override) { vs.lock.Lock() defer vs.lock.Unlock() - vs.schemaStorage.OverrideDefaults(validation.ValuesSchema, override...) + scheme := vs.schemaStorage.Schemas[validation.ValuesSchema] + vs.overridePolicy.ApplyOverride(scheme, override) + + // error could be only if values patch fails to apply + _ = vs.calculateResultValues() } diff --git a/pkg/utils/values_patch.go b/pkg/utils/values_patch.go index 172b86cd5..2027fc005 100644 --- a/pkg/utils/values_patch.go +++ b/pkg/utils/values_patch.go @@ -12,8 +12,6 @@ import ( sdkutils "github.com/deckhouse/module-sdk/pkg/utils" lazynode "github.com/deckhouse/module-sdk/pkg/utils/lazy-node" "github.com/deckhouse/module-sdk/pkg/utils/patch" - - "github.com/flant/addon-operator/pkg/values/validation/schema" ) type ValuesPatchType string @@ -23,11 +21,6 @@ const ( MemoryValuesPatch ValuesPatchType = "MEMORY_VALUES_PATCH" ) -type DefaultsOverride struct { - Name string - Override []schema.DefaultOverride -} - type ValuesPatch struct { Operations []*sdkutils.ValuesPatchOperation } @@ -383,26 +376,6 @@ func EnabledFromValuesPatch(valuesPatch ValuesPatch) ValuesPatch { return newValuesPatch } -func DefaultsOverrideFromValuesPatch(valuesPatch ValuesPatch) DefaultsOverride { - override := DefaultsOverride{} - for _, op := range valuesPatch.Operations { - pathParts := strings.Split(op.Path, "/") - if len(pathParts) > 2 { - if pathParts[1] == "override" { - override.Name = pathParts[2] - - var overrides []schema.DefaultOverride - if err := json.Unmarshal(op.Value, &overrides); err != nil { - continue - } - override.Override = append(override.Override, overrides...) - } - } - } - - return override -} - // Error messages to distinguish non-typed errors from the 'json-patch' library. const ( NonExistentPathErrorMsg = "error in remove for path:" diff --git a/pkg/values/validation/defaultsoverride/policy.go b/pkg/values/validation/defaultsoverride/policy.go new file mode 100644 index 000000000..1b44cf443 --- /dev/null +++ b/pkg/values/validation/defaultsoverride/policy.go @@ -0,0 +1,203 @@ +// Package defaultsoverride provides mechanisms for overriding default values +// in OpenAPI schemas used by addon-operator modules. +// It reads an override contract from a YAML file, validates patches +// against the contract's allowed fields, and applies them to the schema. +package defaultsoverride + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/go-openapi/spec" + "sigs.k8s.io/yaml" + + "github.com/flant/addon-operator/pkg/utils" +) + +// contractsFile is the default filename for override contracts. +const contractsFile = "override.yaml" + +// Contract defines the rules governing which schema fields may be overridden. +// It is loaded from a YAML file and acts as a guard: only patches targeting +// paths listed in Paths are permitted. +// +// Example YAML (override.yaml): +// +// purpose: "cloud-provider" +// allowedModules: +// - cloud-provider-aws +// - cloud-provider-gcp +// paths: +// - network.podSubnet +// - network.serviceSubnet +type Contract struct { + // Purpose describes the intent of this override contract. + Purpose string `json:"purpose"` + // Allowed lists the module names that may use this contract. + Allowed []string `json:"allowed"` + // Paths maps dot-separated property paths to the list of modules + // that are allowed to override those fields. + Paths []string `json:"paths"` +} + +// Policy is the resolved, flattened representation of all contracts. +// It maps each property path to its access policy, used at runtime +// to decide which patches are permitted. +type Policy struct { + // paths maps dot-separated property paths to their access policies. + paths map[string][]string +} + +// Override holds a collection of patches that override +// default values in a schema, submitted by a specific source module. +// +// Example YAML: +// +// source: cloud-provider-aws +// patches: +// - path: network.podSubnet +// value: "10.244.0.0/16" +// - path: network.serviceSubnet +// value: "10.96.0.0/12" +type Override struct { + // Source is the module name requesting the overrides. + // It is checked against PathPolicy.allowed to determine permission. + Source string `json:"source"` + // Source is the module name that schema should be overridden + Target string `json:"target"` + // Patches is an ordered list of default-value overrides to apply. + Patches []Patch `json:"patches"` +} + +// Patch defines a single default value override for a schema property. +// Path uses "." as a separator (e.g. "spec.replicas"). +type Patch struct { + // Path is the dot-separated property path within the schema. + Path string `json:"path"` + // Value is the new default value to set on the target property. + Value string `json:"value"` +} + +// ParseContractsFromDir reads the contracts file (override.yaml) from the given +// directory and unmarshals it into contracts. +func ParseContractsFromDir(dirPath string) ([]Contract, error) { + raw, err := os.ReadFile(filepath.Join(dirPath, contractsFile)) + if err != nil { + return nil, fmt.Errorf("read contracts file: %w", err) + } + + var c []Contract + if err = yaml.Unmarshal(raw, &c); err != nil { + return nil, fmt.Errorf("unmarshal contracts file: %w", err) + } + + return c, nil +} + +// PolicyByContracts flattens the given contracts into a single Policy. +// Each contract's Allowed modules are associated with every path it declares, +// and multiple contracts contributing the same path merge their allowed lists. +func PolicyByContracts(contracts ...Contract) *Policy { + p := &Policy{ + paths: make(map[string][]string), + } + + for _, c := range contracts { + for _, path := range c.Paths { + p.paths[path] = append(p.paths[path], c.Allowed...) + } + } + + return p +} + +func OverridesByValuesPatch(valuesPatch utils.ValuesPatch) []Override { + var overrides []Override + + for _, op := range valuesPatch.Operations { + pathParts := strings.Split(op.Path, "/") + if len(pathParts) > 2 { + if pathParts[1] == "override" { + var override Override + if err := yaml.Unmarshal(op.Value, &overrides); err != nil { + continue + } + + overrides = append(overrides, override) + } + } + } + + return overrides +} + +// ApplyOverride applies permitted default overrides directly to the schema. +// Each patch is checked against the policy: only patches whose paths exist +// in the policy and whose source module is in the path's allowed list are +// applied. Non-matching patches are silently skipped. +func (p *Policy) ApplyOverride(s *spec.Schema, override Override) { + if p == nil { + return + } + + if len(override.Patches) == 0 || s == nil { + return + } + + if len(s.Properties) == 0 { + s.Properties = make(map[string]spec.Schema) + } + + var allowed Override + for _, patch := range override.Patches { + policy, ok := p.paths[patch.Path] + if !ok { + continue + } + + if slices.Contains(policy, override.Source) { + allowed.Patches = append(allowed.Patches, patch) + } + } + + allowed.transform(s) +} + +// transform walks over every patch and applies it to the schema by +// setting default values on the matching properties. +func (t *Override) transform(s *spec.Schema) { + if s == nil || len(t.Patches) == 0 { + return + } + + for _, override := range t.Patches { + segments := strings.Split(override.Path, ".") + setDefault(s, segments, override.Value) + } +} + +// setDefault walks the schema tree through Properties and sets +// the Default field on the leaf property. +func setDefault(s *spec.Schema, segments []string, value any) { + if len(segments) == 0 || s == nil { + return + } + + propName := segments[0] + prop, exists := s.Properties[propName] + if !exists { + return + } + + if len(segments) == 1 { + prop.Default = value + s.Properties[propName] = prop + return + } + + setDefault(&prop, segments[1:], value) + s.Properties[propName] = prop +} diff --git a/pkg/values/validation/defaultsoverride/policy_test.go b/pkg/values/validation/defaultsoverride/policy_test.go new file mode 100644 index 000000000..5f9426e19 --- /dev/null +++ b/pkg/values/validation/defaultsoverride/policy_test.go @@ -0,0 +1,251 @@ +package defaultsoverride_test + +import ( + "testing" + + "github.com/go-openapi/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" +) + +func TestParsePolicyByContracts(t *testing.T) { + t.Run("single contract", func(t *testing.T) { + c := defaultsoverride.Contract{ + Purpose: "cloud-provider", + Allowed: []string{"cloud-provider-aws"}, + Paths: []string{"network.podSubnet", "network.serviceSubnet"}, + } + + p := defaultsoverride.PolicyByContracts(c) + require.NotNil(t, p) + }) + + t.Run("no contracts produces valid policy", func(t *testing.T) { + p := defaultsoverride.PolicyByContracts() + require.NotNil(t, p) + }) +} + +func TestApplyOverride(t *testing.T) { + t.Run("applies permitted patch", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"my-module"}, + Paths: []string{"replicas"}, + }) + s := schemaWithProps("replicas") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "my-module", + Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "3"}}, + }) + + assert.Equal(t, "3", s.Properties["replicas"].Default) + }) + + t.Run("skips patch for disallowed source", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"allowed-module"}, + Paths: []string{"replicas"}, + }) + s := schemaWithProps("replicas") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "other-module", + Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "3"}}, + }) + + assert.Nil(t, s.Properties["replicas"].Default) + }) + + t.Run("skips patch for path not in policy", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"my-module"}, + Paths: []string{"replicas"}, + }) + s := schemaWithProps("replicas", "image") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "my-module", + Patches: []defaultsoverride.Patch{{Path: "image", Value: "nginx"}}, + }) + + assert.Nil(t, s.Properties["image"].Default) + }) + + t.Run("no-op on empty patches", func(t *testing.T) { + p := buildPolicy(t) + s := &spec.Schema{} + + p.ApplyOverride(s, defaultsoverride.Override{Source: "m"}) + }) + + t.Run("no-op on nil schema", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"m"}, + Paths: []string{"replicas"}, + }) + + p.ApplyOverride(nil, defaultsoverride.Override{ + Source: "m", + Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "1"}}, + }) + }) + + t.Run("mixed allowed and disallowed patches", func(t *testing.T) { + p := buildPolicy(t, + defaultsoverride.Contract{ + Allowed: []string{"my-module"}, + Paths: []string{"replicas"}, + }, + defaultsoverride.Contract{ + Allowed: []string{"other-module"}, + Paths: []string{"image"}, + }, + ) + s := schemaWithProps("replicas", "image") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "my-module", + Patches: []defaultsoverride.Patch{ + {Path: "replicas", Value: "5"}, + {Path: "image", Value: "nginx"}, + }, + }) + + assert.Equal(t, "5", s.Properties["replicas"].Default) + assert.Nil(t, s.Properties["image"].Default) + }) + + t.Run("multiple contracts merge permissions for same path", func(t *testing.T) { + p := buildPolicy(t, + defaultsoverride.Contract{ + Allowed: []string{"module-a"}, + Paths: []string{"replicas"}, + }, + defaultsoverride.Contract{ + Allowed: []string{"module-b"}, + Paths: []string{"replicas"}, + }, + ) + s := schemaWithProps("replicas") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "module-b", + Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "7"}}, + }) + + assert.Equal(t, "7", s.Properties["replicas"].Default) + }) + + t.Run("applies nested property path", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"my-module"}, + Paths: []string{"network.podSubnet"}, + }) + s := &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "network": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "podSubnet": {}, + }, + }, + }, + }, + }, + } + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "my-module", + Patches: []defaultsoverride.Patch{{Path: "network.podSubnet", Value: "10.244.0.0/16"}}, + }) + + assert.Equal(t, "10.244.0.0/16", s.Properties["network"].Properties["podSubnet"].Default) + }) + + t.Run("applies deeply nested property path", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"m"}, + Paths: []string{"a.b.c"}, + }) + s := &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "a": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "b": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "c": {}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "m", + Patches: []defaultsoverride.Patch{{Path: "a.b.c", Value: "deep"}}, + }) + + assert.Equal(t, "deep", s.Properties["a"].Properties["b"].Properties["c"].Default) + }) + + t.Run("skips patch when schema property does not exist", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"m"}, + Paths: []string{"missing"}, + }) + s := schemaWithProps("replicas") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "m", + Patches: []defaultsoverride.Patch{{Path: "missing", Value: "val"}}, + }) + + _, exists := s.Properties["missing"] + assert.False(t, exists) + }) + + t.Run("contract with multiple allowed modules", func(t *testing.T) { + p := buildPolicy(t, defaultsoverride.Contract{ + Allowed: []string{"module-a", "module-b"}, + Paths: []string{"replicas"}, + }) + s := schemaWithProps("replicas") + + p.ApplyOverride(s, defaultsoverride.Override{ + Source: "module-b", + Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "2"}}, + }) + + assert.Equal(t, "2", s.Properties["replicas"].Default) + }) +} + +// buildPolicy is a test helper that creates a Policy from the given contracts. +func buildPolicy(t *testing.T, contracts ...defaultsoverride.Contract) *defaultsoverride.Policy { + t.Helper() + return defaultsoverride.PolicyByContracts(contracts...) +} + +// schemaWithProps creates a schema with the given top-level property names. +func schemaWithProps(names ...string) *spec.Schema { + props := make(map[string]spec.Schema, len(names)) + for _, n := range names { + props[n] = spec.Schema{} + } + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: props, + }, + } +} diff --git a/pkg/values/validation/schema/override.go b/pkg/values/validation/schema/override.go deleted file mode 100644 index 4ea2e9875..000000000 --- a/pkg/values/validation/schema/override.go +++ /dev/null @@ -1,56 +0,0 @@ -package schema - -import ( - "strings" - - "github.com/go-openapi/spec" -) - -// DefaultOverride defines a single default value override for a schema property. -// Path uses "/" as a separator (JSON Pointer style, without leading slash). -type DefaultOverride struct { - Path string `json:"path" yaml:"path"` - Value any `json:"value" yaml:"value"` -} - -// DefaultsTransformer is a SchemaTransformer that overrides default values -// on schema properties identified by "/"-separated paths. -type DefaultsTransformer struct { - Overrides []DefaultOverride -} - -func (t *DefaultsTransformer) Transform(s *spec.Schema) *spec.Schema { - if s == nil || len(t.Overrides) == 0 { - return s - } - - for _, override := range t.Overrides { - segments := strings.Split(override.Path, "/") - setDefault(s, segments, override.Value) - } - - return s -} - -// setDefault walks the schema tree through Properties and sets -// the Default field on the leaf property. -func setDefault(s *spec.Schema, segments []string, value any) { - if len(segments) == 0 || s == nil { - return - } - - propName := segments[0] - prop, exists := s.Properties[propName] - if !exists { - return - } - - if len(segments) == 1 { - prop.Default = value - s.Properties[propName] = prop - return - } - - setDefault(&prop, segments[1:], value) - s.Properties[propName] = prop -} diff --git a/pkg/values/validation/schemas.go b/pkg/values/validation/schemas.go index b9a02fa31..88835e7ed 100644 --- a/pkg/values/validation/schemas.go +++ b/pkg/values/validation/schemas.go @@ -305,22 +305,3 @@ func PrepareSchemas(configBytes, valuesBytes []byte) (map[SchemaType]*spec.Schem return res, nil } - -// OverrideDefaults overrides default values on stored schema at runtime. -func (st *SchemaStorage) OverrideDefaults(schemaType SchemaType, overrides ...schema.DefaultOverride) { - if len(overrides) == 0 { - return - } - - scheme := st.Schemas[schemaType] - if scheme == nil { - return - } - - if len(scheme.Properties) == 0 { - scheme.Properties = make(map[string]spec.Schema) - } - - defaults := &schema.DefaultsTransformer{Overrides: overrides} - defaults.Transform(scheme) -} From a11c147a97414e63e0a6b3a0a3f1531723011fd3 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sat, 28 Mar 2026 18:20:35 +0300 Subject: [PATCH 06/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/modules/basic.go | 12 ++- pkg/module_manager/models/modules/global.go | 9 +- .../models/modules/values_storage.go | 13 +-- pkg/module_manager/module_manager.go | 11 ++- .../validation/defaultsoverride/policy.go | 4 + .../defaultsoverride/policy_test.go | 86 +++++++++++++++++++ 6 files changed, 118 insertions(+), 17 deletions(-) diff --git a/pkg/module_manager/models/modules/basic.go b/pkg/module_manager/models/modules/basic.go index 23db4e44c..b6495dd88 100644 --- a/pkg/module_manager/models/modules/basic.go +++ b/pkg/module_manager/models/modules/basic.go @@ -30,7 +30,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" - "github.com/flant/addon-operator/pkg/values/validation/schema" + "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" "github.com/flant/addon-operator/sdk" shapp "github.com/flant/shell-operator/pkg/app" "github.com/flant/shell-operator/pkg/executor" @@ -118,6 +118,10 @@ func (bm *BasicModule) WithLogger(logger *log.Logger) { bm.logger = logger } +func (bm *BasicModule) SetDefaultsOverrideContracts(contracts []defaultsoverride.Contract) { + bm.valuesStorage.SetDefaultsOverrideContracts(contracts) +} + func (bm *BasicModule) SetCritical(value bool) { bm.critical = value } @@ -1345,9 +1349,9 @@ func (bm *BasicModule) GetSchemaStorage() *validation.SchemaStorage { return bm.valuesStorage.schemaStorage } -// OverrideSchemaDefaults overrides values schema openAPI spec defaults -func (bm *BasicModule) OverrideSchemaDefaults(override ...schema.DefaultOverride) { - bm.valuesStorage.OverrideDefaults(override...) +// ApplyDefaultsOverride overrides values schema openAPI spec defaults +func (bm *BasicModule) ApplyDefaultsOverride(override defaultsoverride.Override) { + bm.valuesStorage.ApplyDefaultsOverride(override) } // ApplyNewSchemaStorage updates schema storage of the basic module diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index 172f2baf9..e64dadb16 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -12,6 +12,7 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" sdkutils "github.com/deckhouse/module-sdk/pkg/utils" + "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" "go.opentelemetry.io/otel" "github.com/flant/addon-operator/pkg" @@ -345,18 +346,18 @@ func (gm *GlobalModule) applyEnabledPatches(valuesPatch utils.ValuesPatch) error } type OverridePatchReport struct { - Override utils.DefaultsOverride + Override []defaultsoverride.Override Done chan struct{} } func (gm *GlobalModule) applyDefaultsOverride(valuesPatch utils.ValuesPatch) { - override := utils.DefaultsOverrideFromValuesPatch(valuesPatch) - if len(override.Override) == 0 { + overrides := defaultsoverride.OverridesByValuesPatch(valuesPatch) + if len(overrides) == 0 { return } report := &OverridePatchReport{ - Override: override, + Override: overrides, Done: make(chan struct{}), } diff --git a/pkg/module_manager/models/modules/values_storage.go b/pkg/module_manager/models/modules/values_storage.go index 636e8388e..c50546b90 100644 --- a/pkg/module_manager/models/modules/values_storage.go +++ b/pkg/module_manager/models/modules/values_storage.go @@ -58,17 +58,16 @@ type Registry struct { // NewValuesStorage build a new storage for module values // // staticValues - values from /modules//values.yaml, which couldn't be reloaded during the runtime -func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, valuesBytes []byte, contracts []defaultsoverride.Contract) (*ValuesStorage, error) { +func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, valuesBytes []byte) (*ValuesStorage, error) { schemaStorage, err := validation.NewSchemaStorage(configBytes, valuesBytes) if err != nil { return nil, fmt.Errorf("new schema storage: %w", err) } vs := &ValuesStorage{ - staticValues: staticValues, - schemaStorage: schemaStorage, - moduleName: moduleName, - overridePolicy: defaultsoverride.PolicyByContracts(contracts...), + staticValues: staticValues, + schemaStorage: schemaStorage, + moduleName: moduleName, } if err = vs.calculateResultValues(); err != nil { @@ -78,6 +77,10 @@ func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, return vs, nil } +func (vs *ValuesStorage) SetDefaultsOverrideContracts(contracts []defaultsoverride.Contract) { + vs.overridePolicy = defaultsoverride.PolicyByContracts(contracts...) +} + func (vs *ValuesStorage) openapiDefaultsTransformer(schemaType validation.SchemaType) transformer { return &applyDefaults{ SchemaType: schemaType, diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index 5ffb69ef3..9b3a364e4 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -1030,12 +1030,15 @@ func (mm *ModuleManager) createTasksFromModuleHooks(binding BindingType, createT func (mm *ModuleManager) runDynamicDefaultsOverrideLoop() { for report := range mm.global.OverrideReportChannel() { - basic := mm.GetModule(report.Override.Name) - if basic == nil { - return + for _, override := range report.Override { + basic := mm.GetModule(override.Target) + if basic == nil { + return + } + + basic.ApplyDefaultsOverride(override) } - basic.OverrideSchemaDefaults(report.Override.Override...) report.Done <- struct{}{} } } diff --git a/pkg/values/validation/defaultsoverride/policy.go b/pkg/values/validation/defaultsoverride/policy.go index 1b44cf443..f10284000 100644 --- a/pkg/values/validation/defaultsoverride/policy.go +++ b/pkg/values/validation/defaultsoverride/policy.go @@ -114,6 +114,10 @@ func PolicyByContracts(contracts ...Contract) *Policy { return p } +// OverridesByValuesPatch extracts Override entries from a ValuesPatch. +// It scans each operation for a JSON-Patch path whose second segment is +// "override" (e.g. "/override/something") and unmarshals the operation's +// value as a list of Override structs. func OverridesByValuesPatch(valuesPatch utils.ValuesPatch) []Override { var overrides []Override diff --git a/pkg/values/validation/defaultsoverride/policy_test.go b/pkg/values/validation/defaultsoverride/policy_test.go index 5f9426e19..04a62f3a5 100644 --- a/pkg/values/validation/defaultsoverride/policy_test.go +++ b/pkg/values/validation/defaultsoverride/policy_test.go @@ -1,12 +1,15 @@ package defaultsoverride_test import ( + "encoding/json" "testing" + sdkutils "github.com/deckhouse/module-sdk/pkg/utils" "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" ) @@ -231,6 +234,89 @@ func TestApplyOverride(t *testing.T) { }) } +func TestOverridesByValuesPatch(t *testing.T) { + t.Run("extracts overrides from operations with /override/ path", func(t *testing.T) { + overrideValue := []defaultsoverride.Override{ + { + Source: "cloud-provider-aws", + Target: "global", + Patches: []defaultsoverride.Patch{ + {Path: "network.podSubnet", Value: "10.244.0.0/16"}, + }, + }, + } + raw, err := json.Marshal(overrideValue) + require.NoError(t, err) + + vp := utils.ValuesPatch{ + Operations: []*sdkutils.ValuesPatchOperation{ + { + Op: "add", + Path: "/override/cloud-provider-aws", + Value: raw, + }, + }, + } + + result := defaultsoverride.OverridesByValuesPatch(vp) + require.NotEmpty(t, result) + assert.Equal(t, "cloud-provider-aws", result[0].Source) + assert.Equal(t, "global", result[0].Target) + assert.Equal(t, "network.podSubnet", result[0].Patches[0].Path) + }) + + t.Run("skips operations without override path segment", func(t *testing.T) { + vp := utils.ValuesPatch{ + Operations: []*sdkutils.ValuesPatchOperation{ + { + Op: "add", + Path: "/global/someKey", + Value: json.RawMessage(`"value"`), + }, + }, + } + + result := defaultsoverride.OverridesByValuesPatch(vp) + assert.Empty(t, result) + }) + + t.Run("skips operations with too short path", func(t *testing.T) { + vp := utils.ValuesPatch{ + Operations: []*sdkutils.ValuesPatchOperation{ + { + Op: "add", + Path: "/override", + Value: json.RawMessage(`[]`), + }, + }, + } + + result := defaultsoverride.OverridesByValuesPatch(vp) + assert.Empty(t, result) + }) + + t.Run("skips operations with invalid yaml value", func(t *testing.T) { + vp := utils.ValuesPatch{ + Operations: []*sdkutils.ValuesPatchOperation{ + { + Op: "add", + Path: "/override/something", + Value: json.RawMessage(`{invalid`), + }, + }, + } + + result := defaultsoverride.OverridesByValuesPatch(vp) + assert.Empty(t, result) + }) + + t.Run("returns empty for empty values patch", func(t *testing.T) { + vp := utils.ValuesPatch{} + result := defaultsoverride.OverridesByValuesPatch(vp) + assert.Empty(t, result) + }) +} + // buildPolicy is a test helper that creates a Policy from the given contracts. func buildPolicy(t *testing.T, contracts ...defaultsoverride.Contract) *defaultsoverride.Policy { t.Helper() From 1fe499255bb4c20c0760e74cfc17d523508ef5d2 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sat, 28 Mar 2026 18:21:22 +0300 Subject: [PATCH 07/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/modules/global.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index e64dadb16..c75decd0b 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -12,7 +12,6 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" sdkutils "github.com/deckhouse/module-sdk/pkg/utils" - "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" "go.opentelemetry.io/otel" "github.com/flant/addon-operator/pkg" @@ -22,6 +21,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" + "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" "github.com/flant/addon-operator/sdk" bindingcontext "github.com/flant/shell-operator/pkg/hook/binding_context" sh_op_types "github.com/flant/shell-operator/pkg/hook/types" From f795a24b40fd9c6363f0956e485effb0fec81cbc Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sun, 29 Mar 2026 13:17:24 +0300 Subject: [PATCH 08/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/modules/basic.go | 6 +- pkg/module_manager/models/modules/global.go | 12 +- .../modules/values_defaulting_transformers.go | 3 +- .../models/modules/values_storage.go | 10 +- .../{defaulting.go => defaults/defaults.go} | 2 +- .../defaults_test.go} | 7 +- .../policy.go => defaults/override.go} | 48 ++++--- .../override_test.go} | 129 ++++++++++-------- 8 files changed, 114 insertions(+), 103 deletions(-) rename pkg/values/validation/{defaulting.go => defaults/defaults.go} (98%) rename pkg/values/validation/{defaulting_test.go => defaults/defaults_test.go} (95%) rename pkg/values/validation/{defaultsoverride/policy.go => defaults/override.go} (79%) rename pkg/values/validation/{defaultsoverride/policy_test.go => defaults/override_test.go} (67%) diff --git a/pkg/module_manager/models/modules/basic.go b/pkg/module_manager/models/modules/basic.go index b6495dd88..fabf47035 100644 --- a/pkg/module_manager/models/modules/basic.go +++ b/pkg/module_manager/models/modules/basic.go @@ -30,7 +30,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" - "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" + "github.com/flant/addon-operator/pkg/values/validation/defaults" "github.com/flant/addon-operator/sdk" shapp "github.com/flant/shell-operator/pkg/app" "github.com/flant/shell-operator/pkg/executor" @@ -118,7 +118,7 @@ func (bm *BasicModule) WithLogger(logger *log.Logger) { bm.logger = logger } -func (bm *BasicModule) SetDefaultsOverrideContracts(contracts []defaultsoverride.Contract) { +func (bm *BasicModule) SetDefaultsOverrideContracts(contracts []defaults.OverrideContract) { bm.valuesStorage.SetDefaultsOverrideContracts(contracts) } @@ -1350,7 +1350,7 @@ func (bm *BasicModule) GetSchemaStorage() *validation.SchemaStorage { } // ApplyDefaultsOverride overrides values schema openAPI spec defaults -func (bm *BasicModule) ApplyDefaultsOverride(override defaultsoverride.Override) { +func (bm *BasicModule) ApplyDefaultsOverride(override defaults.Override) { bm.valuesStorage.ApplyDefaultsOverride(override) } diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index c75decd0b..18957d8fa 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -21,7 +21,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" - "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" + "github.com/flant/addon-operator/pkg/values/validation/defaults" "github.com/flant/addon-operator/sdk" bindingcontext "github.com/flant/shell-operator/pkg/hook/binding_context" sh_op_types "github.com/flant/shell-operator/pkg/hook/types" @@ -346,19 +346,19 @@ func (gm *GlobalModule) applyEnabledPatches(valuesPatch utils.ValuesPatch) error } type OverridePatchReport struct { - Override []defaultsoverride.Override - Done chan struct{} + Overrides []defaults.Override + Done chan struct{} } func (gm *GlobalModule) applyDefaultsOverride(valuesPatch utils.ValuesPatch) { - overrides := defaultsoverride.OverridesByValuesPatch(valuesPatch) + overrides := defaults.GetOverridesByPatch(valuesPatch) if len(overrides) == 0 { return } report := &OverridePatchReport{ - Override: overrides, - Done: make(chan struct{}), + Overrides: overrides, + Done: make(chan struct{}), } <-report.Done diff --git a/pkg/module_manager/models/modules/values_defaulting_transformers.go b/pkg/module_manager/models/modules/values_defaulting_transformers.go index 652084024..7376bb96a 100644 --- a/pkg/module_manager/models/modules/values_defaulting_transformers.go +++ b/pkg/module_manager/models/modules/values_defaulting_transformers.go @@ -5,6 +5,7 @@ import ( "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" + "github.com/flant/addon-operator/pkg/values/validation/defaults" ) type transformer interface { @@ -27,7 +28,7 @@ func (a *applyDefaults) Transform(values utils.Values) utils.Values { } res := values.Copy() - validation.ApplyDefaults(res, s) + defaults.ApplyDefaults(res, s) return res } diff --git a/pkg/module_manager/models/modules/values_storage.go b/pkg/module_manager/models/modules/values_storage.go index c50546b90..6518c41f2 100644 --- a/pkg/module_manager/models/modules/values_storage.go +++ b/pkg/module_manager/models/modules/values_storage.go @@ -6,7 +6,7 @@ import ( "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" - "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" + "github.com/flant/addon-operator/pkg/values/validation/defaults" ) /* @@ -28,7 +28,7 @@ type ValuesStorage struct { schemaStorage *validation.SchemaStorage moduleName string - overridePolicy *defaultsoverride.Policy + overridePolicy *defaults.OverridePolicy // we are locking the whole storage on any concurrent operation // because it could be called from concurrent hooks (goroutines) and we will have a deadlock on RW mutex @@ -77,8 +77,8 @@ func NewValuesStorage(moduleName string, staticValues utils.Values, configBytes, return vs, nil } -func (vs *ValuesStorage) SetDefaultsOverrideContracts(contracts []defaultsoverride.Contract) { - vs.overridePolicy = defaultsoverride.PolicyByContracts(contracts...) +func (vs *ValuesStorage) SetDefaultsOverrideContracts(contracts []defaults.OverrideContract) { + vs.overridePolicy = defaults.BuildOverridePolicy(contracts...) } func (vs *ValuesStorage) openapiDefaultsTransformer(schemaType validation.SchemaType) transformer { @@ -276,7 +276,7 @@ func (vs *ValuesStorage) GetSchemaStorage() *validation.SchemaStorage { return vs.schemaStorage } -func (vs *ValuesStorage) ApplyDefaultsOverride(override defaultsoverride.Override) { +func (vs *ValuesStorage) ApplyDefaultsOverride(override defaults.Override) { vs.lock.Lock() defer vs.lock.Unlock() diff --git a/pkg/values/validation/defaulting.go b/pkg/values/validation/defaults/defaults.go similarity index 98% rename from pkg/values/validation/defaulting.go rename to pkg/values/validation/defaults/defaults.go index 831fb8631..67ae327cd 100644 --- a/pkg/values/validation/defaulting.go +++ b/pkg/values/validation/defaults/defaults.go @@ -1,4 +1,4 @@ -package validation +package defaults import ( "github.com/go-openapi/spec" diff --git a/pkg/values/validation/defaulting_test.go b/pkg/values/validation/defaults/defaults_test.go similarity index 95% rename from pkg/values/validation/defaulting_test.go rename to pkg/values/validation/defaults/defaults_test.go index 741b34289..b1b318b45 100644 --- a/pkg/values/validation/defaulting_test.go +++ b/pkg/values/validation/defaults/defaults_test.go @@ -1,4 +1,4 @@ -package validation_test +package defaults_test import ( "testing" @@ -11,6 +11,7 @@ import ( "github.com/flant/addon-operator/pkg/module_manager/models/modules" "github.com/flant/addon-operator/pkg/utils" "github.com/flant/addon-operator/pkg/values/validation" + "github.com/flant/addon-operator/pkg/values/validation/defaults" ) func Test_ApplyDefaults(t *testing.T) { @@ -72,7 +73,7 @@ properties: s := valueStorage.GetSchemaStorage().Schemas[validation.ConfigValuesSchema] - changed := validation.ApplyDefaults(moduleValues["moduleName"], s) + changed := defaults.ApplyDefaults(moduleValues["moduleName"], s) g.Expect(changed).Should(BeTrue()) g.Expect(moduleValues["moduleName"]).Should(HaveKey("param2")) @@ -162,7 +163,7 @@ properties: s := valueStorage.GetSchemaStorage().Schemas[validation.ConfigValuesSchema] - changed := validation.ApplyDefaults(moduleValues["moduleName"], s) + changed := defaults.ApplyDefaults(moduleValues["moduleName"], s) validator := validate.NewSchemaValidator(s, nil, "", strfmt.Default) diff --git a/pkg/values/validation/defaultsoverride/policy.go b/pkg/values/validation/defaults/override.go similarity index 79% rename from pkg/values/validation/defaultsoverride/policy.go rename to pkg/values/validation/defaults/override.go index f10284000..211312f01 100644 --- a/pkg/values/validation/defaultsoverride/policy.go +++ b/pkg/values/validation/defaults/override.go @@ -1,8 +1,4 @@ -// Package defaultsoverride provides mechanisms for overriding default values -// in OpenAPI schemas used by addon-operator modules. -// It reads an override contract from a YAML file, validates patches -// against the contract's allowed fields, and applies them to the schema. -package defaultsoverride +package defaults import ( "fmt" @@ -20,7 +16,7 @@ import ( // contractsFile is the default filename for override contracts. const contractsFile = "override.yaml" -// Contract defines the rules governing which schema fields may be overridden. +// OverrideContract defines the rules governing which schema fields may be overridden. // It is loaded from a YAML file and acts as a guard: only patches targeting // paths listed in Paths are permitted. // @@ -33,7 +29,7 @@ const contractsFile = "override.yaml" // paths: // - network.podSubnet // - network.serviceSubnet -type Contract struct { +type OverrideContract struct { // Purpose describes the intent of this override contract. Purpose string `json:"purpose"` // Allowed lists the module names that may use this contract. @@ -43,10 +39,10 @@ type Contract struct { Paths []string `json:"paths"` } -// Policy is the resolved, flattened representation of all contracts. +// OverridePolicy is the resolved, flattened representation of all contracts. // It maps each property path to its access policy, used at runtime // to decide which patches are permitted. -type Policy struct { +type OverridePolicy struct { // paths maps dot-separated property paths to their access policies. paths map[string][]string } @@ -63,13 +59,13 @@ type Policy struct { // - path: network.serviceSubnet // value: "10.96.0.0/12" type Override struct { - // Source is the module name requesting the overrides. - // It is checked against PathPolicy.allowed to determine permission. - Source string `json:"source"` // Source is the module name that schema should be overridden - Target string `json:"target"` + Target string // Patches is an ordered list of default-value overrides to apply. Patches []Patch `json:"patches"` + // Source is the module name requesting the overrides. + // It is checked against PathPolicy.allowed to determine permission. + Source string `json:"source"` } // Patch defines a single default value override for a schema property. @@ -81,15 +77,15 @@ type Patch struct { Value string `json:"value"` } -// ParseContractsFromDir reads the contracts file (override.yaml) from the given +// ParseOverrideContractsFromDir reads the contracts file (override.yaml) from the given // directory and unmarshals it into contracts. -func ParseContractsFromDir(dirPath string) ([]Contract, error) { +func ParseOverrideContractsFromDir(dirPath string) ([]OverrideContract, error) { raw, err := os.ReadFile(filepath.Join(dirPath, contractsFile)) if err != nil { return nil, fmt.Errorf("read contracts file: %w", err) } - var c []Contract + var c []OverrideContract if err = yaml.Unmarshal(raw, &c); err != nil { return nil, fmt.Errorf("unmarshal contracts file: %w", err) } @@ -97,11 +93,11 @@ func ParseContractsFromDir(dirPath string) ([]Contract, error) { return c, nil } -// PolicyByContracts flattens the given contracts into a single Policy. +// BuildOverridePolicy flattens the given contracts into a single OverridePolicy. // Each contract's Allowed modules are associated with every path it declares, // and multiple contracts contributing the same path merge their allowed lists. -func PolicyByContracts(contracts ...Contract) *Policy { - p := &Policy{ +func BuildOverridePolicy(contracts ...OverrideContract) *OverridePolicy { + p := &OverridePolicy{ paths: make(map[string][]string), } @@ -114,22 +110,24 @@ func PolicyByContracts(contracts ...Contract) *Policy { return p } -// OverridesByValuesPatch extracts Override entries from a ValuesPatch. +// GetOverridesByPatch extracts Override entries from a ValuesPatch. // It scans each operation for a JSON-Patch path whose second segment is -// "override" (e.g. "/override/something") and unmarshals the operation's +// "override" (e.g. "/override/target") and unmarshals the operation's // value as a list of Override structs. -func OverridesByValuesPatch(valuesPatch utils.ValuesPatch) []Override { +func GetOverridesByPatch(valuesPatch utils.ValuesPatch) []Override { var overrides []Override for _, op := range valuesPatch.Operations { pathParts := strings.Split(op.Path, "/") - if len(pathParts) > 2 { + if len(pathParts) == 3 { if pathParts[1] == "override" { var override Override - if err := yaml.Unmarshal(op.Value, &overrides); err != nil { + if err := yaml.Unmarshal(op.Value, &override.Patches); err != nil { continue } + override.Target = pathParts[2] + overrides = append(overrides, override) } } @@ -142,7 +140,7 @@ func OverridesByValuesPatch(valuesPatch utils.ValuesPatch) []Override { // Each patch is checked against the policy: only patches whose paths exist // in the policy and whose source module is in the path's allowed list are // applied. Non-matching patches are silently skipped. -func (p *Policy) ApplyOverride(s *spec.Schema, override Override) { +func (p *OverridePolicy) ApplyOverride(s *spec.Schema, override Override) { if p == nil { return } diff --git a/pkg/values/validation/defaultsoverride/policy_test.go b/pkg/values/validation/defaults/override_test.go similarity index 67% rename from pkg/values/validation/defaultsoverride/policy_test.go rename to pkg/values/validation/defaults/override_test.go index 04a62f3a5..98855abc7 100644 --- a/pkg/values/validation/defaultsoverride/policy_test.go +++ b/pkg/values/validation/defaults/override_test.go @@ -1,4 +1,4 @@ -package defaultsoverride_test +package defaults_test import ( "encoding/json" @@ -10,68 +10,68 @@ import ( "github.com/stretchr/testify/require" "github.com/flant/addon-operator/pkg/utils" - "github.com/flant/addon-operator/pkg/values/validation/defaultsoverride" + "github.com/flant/addon-operator/pkg/values/validation/defaults" ) func TestParsePolicyByContracts(t *testing.T) { t.Run("single contract", func(t *testing.T) { - c := defaultsoverride.Contract{ + c := defaults.OverrideContract{ Purpose: "cloud-provider", Allowed: []string{"cloud-provider-aws"}, Paths: []string{"network.podSubnet", "network.serviceSubnet"}, } - p := defaultsoverride.PolicyByContracts(c) + p := defaults.BuildOverridePolicy(c) require.NotNil(t, p) }) t.Run("no contracts produces valid policy", func(t *testing.T) { - p := defaultsoverride.PolicyByContracts() + p := defaults.BuildOverridePolicy() require.NotNil(t, p) }) } func TestApplyOverride(t *testing.T) { t.Run("applies permitted patch", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"my-module"}, Paths: []string{"replicas"}, }) s := schemaWithProps("replicas") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "my-module", - Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "3"}}, + Patches: []defaults.Patch{{Path: "replicas", Value: "3"}}, }) assert.Equal(t, "3", s.Properties["replicas"].Default) }) t.Run("skips patch for disallowed source", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"allowed-module"}, Paths: []string{"replicas"}, }) s := schemaWithProps("replicas") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "other-module", - Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "3"}}, + Patches: []defaults.Patch{{Path: "replicas", Value: "3"}}, }) assert.Nil(t, s.Properties["replicas"].Default) }) t.Run("skips patch for path not in policy", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"my-module"}, Paths: []string{"replicas"}, }) s := schemaWithProps("replicas", "image") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "my-module", - Patches: []defaultsoverride.Patch{{Path: "image", Value: "nginx"}}, + Patches: []defaults.Patch{{Path: "image", Value: "nginx"}}, }) assert.Nil(t, s.Properties["image"].Default) @@ -81,37 +81,37 @@ func TestApplyOverride(t *testing.T) { p := buildPolicy(t) s := &spec.Schema{} - p.ApplyOverride(s, defaultsoverride.Override{Source: "m"}) + p.ApplyOverride(s, defaults.Override{Source: "m"}) }) t.Run("no-op on nil schema", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"m"}, Paths: []string{"replicas"}, }) - p.ApplyOverride(nil, defaultsoverride.Override{ + p.ApplyOverride(nil, defaults.Override{ Source: "m", - Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "1"}}, + Patches: []defaults.Patch{{Path: "replicas", Value: "1"}}, }) }) t.Run("mixed allowed and disallowed patches", func(t *testing.T) { p := buildPolicy(t, - defaultsoverride.Contract{ + defaults.OverrideContract{ Allowed: []string{"my-module"}, Paths: []string{"replicas"}, }, - defaultsoverride.Contract{ + defaults.OverrideContract{ Allowed: []string{"other-module"}, Paths: []string{"image"}, }, ) s := schemaWithProps("replicas", "image") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "my-module", - Patches: []defaultsoverride.Patch{ + Patches: []defaults.Patch{ {Path: "replicas", Value: "5"}, {Path: "image", Value: "nginx"}, }, @@ -123,27 +123,27 @@ func TestApplyOverride(t *testing.T) { t.Run("multiple contracts merge permissions for same path", func(t *testing.T) { p := buildPolicy(t, - defaultsoverride.Contract{ + defaults.OverrideContract{ Allowed: []string{"module-a"}, Paths: []string{"replicas"}, }, - defaultsoverride.Contract{ + defaults.OverrideContract{ Allowed: []string{"module-b"}, Paths: []string{"replicas"}, }, ) s := schemaWithProps("replicas") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "module-b", - Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "7"}}, + Patches: []defaults.Patch{{Path: "replicas", Value: "7"}}, }) assert.Equal(t, "7", s.Properties["replicas"].Default) }) t.Run("applies nested property path", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"my-module"}, Paths: []string{"network.podSubnet"}, }) @@ -161,16 +161,16 @@ func TestApplyOverride(t *testing.T) { }, } - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "my-module", - Patches: []defaultsoverride.Patch{{Path: "network.podSubnet", Value: "10.244.0.0/16"}}, + Patches: []defaults.Patch{{Path: "network.podSubnet", Value: "10.244.0.0/16"}}, }) assert.Equal(t, "10.244.0.0/16", s.Properties["network"].Properties["podSubnet"].Default) }) t.Run("applies deeply nested property path", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"m"}, Paths: []string{"a.b.c"}, }) @@ -194,24 +194,24 @@ func TestApplyOverride(t *testing.T) { }, } - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "m", - Patches: []defaultsoverride.Patch{{Path: "a.b.c", Value: "deep"}}, + Patches: []defaults.Patch{{Path: "a.b.c", Value: "deep"}}, }) assert.Equal(t, "deep", s.Properties["a"].Properties["b"].Properties["c"].Default) }) t.Run("skips patch when schema property does not exist", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"m"}, Paths: []string{"missing"}, }) s := schemaWithProps("replicas") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "m", - Patches: []defaultsoverride.Patch{{Path: "missing", Value: "val"}}, + Patches: []defaults.Patch{{Path: "missing", Value: "val"}}, }) _, exists := s.Properties["missing"] @@ -219,15 +219,15 @@ func TestApplyOverride(t *testing.T) { }) t.Run("contract with multiple allowed modules", func(t *testing.T) { - p := buildPolicy(t, defaultsoverride.Contract{ + p := buildPolicy(t, defaults.OverrideContract{ Allowed: []string{"module-a", "module-b"}, Paths: []string{"replicas"}, }) s := schemaWithProps("replicas") - p.ApplyOverride(s, defaultsoverride.Override{ + p.ApplyOverride(s, defaults.Override{ Source: "module-b", - Patches: []defaultsoverride.Patch{{Path: "replicas", Value: "2"}}, + Patches: []defaults.Patch{{Path: "replicas", Value: "2"}}, }) assert.Equal(t, "2", s.Properties["replicas"].Default) @@ -235,34 +235,30 @@ func TestApplyOverride(t *testing.T) { } func TestOverridesByValuesPatch(t *testing.T) { - t.Run("extracts overrides from operations with /override/ path", func(t *testing.T) { - overrideValue := []defaultsoverride.Override{ - { - Source: "cloud-provider-aws", - Target: "global", - Patches: []defaultsoverride.Patch{ - {Path: "network.podSubnet", Value: "10.244.0.0/16"}, - }, - }, + t.Run("extracts overrides from operations with /override/target path", func(t *testing.T) { + patches := []defaults.Patch{ + {Path: "network.podSubnet", Value: "10.244.0.0/16"}, } - raw, err := json.Marshal(overrideValue) + raw, err := json.Marshal(patches) require.NoError(t, err) vp := utils.ValuesPatch{ Operations: []*sdkutils.ValuesPatchOperation{ { Op: "add", - Path: "/override/cloud-provider-aws", + Path: "/override/global", Value: raw, }, }, } - result := defaultsoverride.OverridesByValuesPatch(vp) - require.NotEmpty(t, result) + result := defaults.GetOverridesByPatch(vp) + require.Len(t, result, 1) assert.Equal(t, "cloud-provider-aws", result[0].Source) assert.Equal(t, "global", result[0].Target) + require.Len(t, result[0].Patches, 1) assert.Equal(t, "network.podSubnet", result[0].Patches[0].Path) + assert.Equal(t, "10.244.0.0/16", result[0].Patches[0].Value) }) t.Run("skips operations without override path segment", func(t *testing.T) { @@ -276,7 +272,7 @@ func TestOverridesByValuesPatch(t *testing.T) { }, } - result := defaultsoverride.OverridesByValuesPatch(vp) + result := defaults.GetOverridesByPatch(vp) assert.Empty(t, result) }) @@ -291,7 +287,22 @@ func TestOverridesByValuesPatch(t *testing.T) { }, } - result := defaultsoverride.OverridesByValuesPatch(vp) + result := defaults.GetOverridesByPatch(vp) + assert.Empty(t, result) + }) + + t.Run("skips operations with too long path", func(t *testing.T) { + vp := utils.ValuesPatch{ + Operations: []*sdkutils.ValuesPatchOperation{ + { + Op: "add", + Path: "/override/target/extra", + Value: json.RawMessage(`[]`), + }, + }, + } + + result := defaults.GetOverridesByPatch(vp) assert.Empty(t, result) }) @@ -300,27 +311,27 @@ func TestOverridesByValuesPatch(t *testing.T) { Operations: []*sdkutils.ValuesPatchOperation{ { Op: "add", - Path: "/override/something", + Path: "/override/target", Value: json.RawMessage(`{invalid`), }, }, } - result := defaultsoverride.OverridesByValuesPatch(vp) + result := defaults.GetOverridesByPatch(vp) assert.Empty(t, result) }) t.Run("returns empty for empty values patch", func(t *testing.T) { vp := utils.ValuesPatch{} - result := defaultsoverride.OverridesByValuesPatch(vp) + result := defaults.GetOverridesByPatch(vp) assert.Empty(t, result) }) } -// buildPolicy is a test helper that creates a Policy from the given contracts. -func buildPolicy(t *testing.T, contracts ...defaultsoverride.Contract) *defaultsoverride.Policy { +// buildPolicy is a test helper that creates a OverridePolicy from the given contracts. +func buildPolicy(t *testing.T, contracts ...defaults.OverrideContract) *defaults.OverridePolicy { t.Helper() - return defaultsoverride.PolicyByContracts(contracts...) + return defaults.BuildOverridePolicy(contracts...) } // schemaWithProps creates a schema with the given top-level property names. From 025a67c04dc53c3629543ede7dfe29a5e25db39a Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sun, 29 Mar 2026 13:25:30 +0300 Subject: [PATCH 09/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/models/hooks/dependency.go | 18 +++++++---- pkg/module_manager/models/modules/global.go | 32 ++----------------- pkg/module_manager/module_manager.go | 29 ++++++++--------- pkg/module_manager/module_manager_hooks.go | 10 +++--- 4 files changed, 33 insertions(+), 56 deletions(-) diff --git a/pkg/module_manager/models/hooks/dependency.go b/pkg/module_manager/models/hooks/dependency.go index ae272a83c..41b638fab 100644 --- a/pkg/module_manager/models/hooks/dependency.go +++ b/pkg/module_manager/models/hooks/dependency.go @@ -10,6 +10,7 @@ import ( gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" "github.com/flant/addon-operator/pkg/utils" + "github.com/flant/addon-operator/pkg/values/validation/defaults" bindingcontext "github.com/flant/shell-operator/pkg/hook/binding_context" "github.com/flant/shell-operator/pkg/hook/config" "github.com/flant/shell-operator/pkg/hook/controller" @@ -32,6 +33,10 @@ type kubeObjectPatcher interface { ExecuteOperations([]sdkpkg.PatchCollectorOperation) error } +type defaultsOverrideApplier interface { + ApplyDefaultsOverride(overrides []defaults.Override) +} + type globalValuesGetter interface { GetValues(bool) utils.Values GetConfigValues(bool) utils.Values @@ -39,12 +44,13 @@ type globalValuesGetter interface { // HookExecutionDependencyContainer container for all hook execution dependencies type HookExecutionDependencyContainer struct { - HookMetricsStorage hooksMetricsStorage - KubeConfigManager kubeConfigManager - KubeObjectPatcher kubeObjectPatcher - MetricStorage metricStorage - GlobalValuesGetter globalValuesGetter - EnvironmentManager *environmentmanager.Manager + HookMetricsStorage hooksMetricsStorage + KubeConfigManager kubeConfigManager + KubeObjectPatcher kubeObjectPatcher + MetricStorage metricStorage + GlobalValuesGetter globalValuesGetter + DefaultsOverrideApplier defaultsOverrideApplier + EnvironmentManager *environmentmanager.Manager } type executableHook interface { diff --git a/pkg/module_manager/models/modules/global.go b/pkg/module_manager/models/modules/global.go index 18957d8fa..14b5f2f5d 100644 --- a/pkg/module_manager/models/modules/global.go +++ b/pkg/module_manager/models/modules/global.go @@ -38,9 +38,8 @@ type GlobalModule struct { valuesStorage *ValuesStorage - enabledByHookC chan *EnabledPatchReport - overrideByHookC chan *OverridePatchReport - hasReadiness bool + enabledByHookC chan *EnabledPatchReport + hasReadiness bool // dependency dc *hooks.HookExecutionDependencyContainer @@ -55,11 +54,6 @@ func (gm *GlobalModule) EnabledReportChannel() chan *EnabledPatchReport { return gm.enabledByHookC } -// OverrideReportChannel returns channel with dynamic openAPI override by global hooks -func (gm *GlobalModule) OverrideReportChannel() chan *OverridePatchReport { - return gm.overrideByHookC -} - // NewGlobalModule build ephemeral global container for global hooks and values func NewGlobalModule(hooksDir string, staticValues utils.Values, dc *hooks.HookExecutionDependencyContainer, configBytes, valuesBytes []byte, keepTemporaryHookFiles bool, opts ...ModuleOption, @@ -76,7 +70,6 @@ func NewGlobalModule(hooksDir string, staticValues utils.Values, dc *hooks.HookE valuesStorage: valuesStorage, dc: dc, enabledByHookC: make(chan *EnabledPatchReport, 10), - overrideByHookC: make(chan *OverridePatchReport, 10), keepTemporaryHookFiles: keepTemporaryHookFiles, } @@ -315,7 +308,7 @@ func (gm *GlobalModule) executeHook(ctx context.Context, h *hooks.GlobalHook, bi return fmt.Errorf("apply enabled patches from global values patch: %v", err) } - gm.applyDefaultsOverride(*valuesPatch) + gm.dc.DefaultsOverrideApplier.ApplyDefaultsOverride(defaults.GetOverridesByPatch(*valuesPatch)) } return nil @@ -345,25 +338,6 @@ func (gm *GlobalModule) applyEnabledPatches(valuesPatch utils.ValuesPatch) error return err } -type OverridePatchReport struct { - Overrides []defaults.Override - Done chan struct{} -} - -func (gm *GlobalModule) applyDefaultsOverride(valuesPatch utils.ValuesPatch) { - overrides := defaults.GetOverridesByPatch(valuesPatch) - if len(overrides) == 0 { - return - } - - report := &OverridePatchReport{ - Overrides: overrides, - Done: make(chan struct{}), - } - - <-report.Done -} - func (gm *GlobalModule) GetValues(withPrefix bool) utils.Values { return gm.valuesStorage.GetValues(withPrefix) } diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index 9b3a364e4..b002eec5a 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -13,6 +13,7 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" metricsstorage "github.com/deckhouse/deckhouse/pkg/metrics-storage" + "github.com/flant/addon-operator/pkg/values/validation/defaults" "github.com/hashicorp/go-multierror" "go.opentelemetry.io/otel" @@ -980,7 +981,7 @@ func (mm *ModuleManager) HandleScheduleEvent( } func (mm *ModuleManager) CreateTasksByBinding(binding BindingType, createTasksFunc func(gh *hooks.GlobalHook, m *modules.BasicModule, mh *hooks.ModuleHook) []sh_task.Task) []sh_task.Task { - var allTasks []sh_task.Task //nolint: prealloc + var allTasks []sh_task.Task // nolint: prealloc // Process global hooks allTasks = append(allTasks, mm.createTasksFromGlobalHooks(binding, createTasksFunc)...) @@ -1028,21 +1029,6 @@ func (mm *ModuleManager) createTasksFromModuleHooks(binding BindingType, createT return tasks } -func (mm *ModuleManager) runDynamicDefaultsOverrideLoop() { - for report := range mm.global.OverrideReportChannel() { - for _, override := range report.Override { - basic := mm.GetModule(override.Target) - if basic == nil { - return - } - - basic.ApplyDefaultsOverride(override) - } - - report.Done <- struct{}{} - } -} - func (mm *ModuleManager) runDynamicEnabledLoop(extender *dynamic_extender.Extender) { for report := range mm.global.EnabledReportChannel() { err := mm.applyEnabledPatch(report.Patch, extender) @@ -1540,6 +1526,17 @@ func (mm *ModuleManager) EnvironmentManagerEnabled() bool { return mm.environmentManager != nil } +func (mm *ModuleManager) ApplyDefaultsOverride(overrides []defaults.Override) { + for _, override := range overrides { + basic := mm.GetModule(override.Target) + if basic == nil { + return + } + + basic.ApplyDefaultsOverride(override) + } +} + // queueHasPendingModuleRunTaskWithStartup returns true if queue has pending tasks // with the type "ModuleRun" related to the module "moduleName" and DoModuleStartup is set to true. func queueHasPendingModuleRunTaskWithStartup(q *queue.TaskQueue, moduleName string) bool { diff --git a/pkg/module_manager/module_manager_hooks.go b/pkg/module_manager/module_manager_hooks.go index 49cf2b0ce..c02decdef 100644 --- a/pkg/module_manager/module_manager_hooks.go +++ b/pkg/module_manager/module_manager_hooks.go @@ -85,10 +85,11 @@ func (mm *ModuleManager) loadGlobalValues() (*globalValues, error) { func (mm *ModuleManager) registerGlobalModule(globalValues utils.Values, configBytes, valuesBytes []byte) error { // load and registry global hooks dep := hooks.HookExecutionDependencyContainer{ - HookMetricsStorage: mm.dependencies.HookMetricStorage, - KubeConfigManager: mm.dependencies.KubeConfigManager, - KubeObjectPatcher: mm.dependencies.KubeObjectPatcher, - MetricStorage: mm.dependencies.MetricStorage, + HookMetricsStorage: mm.dependencies.HookMetricStorage, + KubeConfigManager: mm.dependencies.KubeConfigManager, + KubeObjectPatcher: mm.dependencies.KubeObjectPatcher, + MetricStorage: mm.dependencies.MetricStorage, + DefaultsOverrideApplier: mm, } gm, err := modules.NewGlobalModule(mm.GlobalHooksDir, globalValues, &dep, configBytes, valuesBytes, shapp.DebugKeepTmpFiles, modules.WithLogger(mm.logger.Named("global-module"))) @@ -106,7 +107,6 @@ func (mm *ModuleManager) registerGlobalModule(globalValues utils.Values, configB } // catch dynamin Enabled patches from global hooks go mm.runDynamicEnabledLoop(dynamicExtender) - go mm.runDynamicDefaultsOverrideLoop() return mm.registerGlobalHooks(gm) } From 8a3a4e8ee130b9b348714ebe82c6859089c703cd Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sun, 29 Mar 2026 13:26:50 +0300 Subject: [PATCH 10/10] [feature] override schema defaults Signed-off-by: Stepan Paksashvili --- pkg/module_manager/module_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/module_manager/module_manager.go b/pkg/module_manager/module_manager.go index b002eec5a..aad31127d 100644 --- a/pkg/module_manager/module_manager.go +++ b/pkg/module_manager/module_manager.go @@ -981,7 +981,7 @@ func (mm *ModuleManager) HandleScheduleEvent( } func (mm *ModuleManager) CreateTasksByBinding(binding BindingType, createTasksFunc func(gh *hooks.GlobalHook, m *modules.BasicModule, mh *hooks.ModuleHook) []sh_task.Task) []sh_task.Task { - var allTasks []sh_task.Task // nolint: prealloc + var allTasks []sh_task.Task //nolint: prealloc // Process global hooks allTasks = append(allTasks, mm.createTasksFromGlobalHooks(binding, createTasksFunc)...)