feat(overrides): add CEL valueProgram support for dynamic override values#595
feat(overrides): add CEL valueProgram support for dynamic override values#595JinghanFu wants to merge 0 commit into
Conversation
107ff51 to
0b8abe2
Compare
| Value any `json:"value"` | ||
| Condition string `json:"condition"` | ||
| Path string `json:"path"` | ||
| Value any `json:"value,omitempty"` |
There was a problem hiding this comment.
is adding omitempty intentinoal?
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create program: %w", err) | ||
| } | ||
|
|
There was a problem hiding this comment.
nit: If value and valueProgram are mutually exclusive, can we add a logging here for telemetry?
There was a problem hiding this comment.
Hi, Ruinan. May I know whether you mean checking the cases for mutually exclusive and then return nil, Errorf("error message") here (consistent with all other checks in this function) or printing this out by fmt.Printf("error message")? Thank you!
There was a problem hiding this comment.
Yeah sorry maybe not here, but it would be better if we can add a logging in telemetry saying that we are currently using valueProgram instead of Value.
Not a blocker, if it's too complicated we don't need this.
There was a problem hiding this comment.
Thank you, Ruinan! I put the logging in telemetry in mutation.go and they look like:
if o.ValueProgram != nil { ... logger.Info("override using valueProgram (resolved CEL value expression)", "path", o.Path.String()) } else { logger.Info("override using static default value", "path", o.Path.String()) }
| logger.Info("CEL value expression evaluated to null, skipping mutation", "path", o.Path.String()) | ||
| return StatusInactive, nil | ||
| } | ||
| logger.Info("resolved CEL value expression", "path", o.Path.String(), "resolvedValue", resolvedValue) |
There was a problem hiding this comment.
would logging resolvedValue be too sensitive to log in telemetry.
|
Could we also add a e2e test like in this PR? https://github.com/Azure/eno/pull/590/changes#diff-ff578928676707b352eb1dd2ee6a304cf2887ec8969d83e872ba5b4243b1f930. |
|
For smoke test failures, I have a fix in this PR: #526. I'm currently working on getting it to merge. |
ruinan-liu
left a comment
There was a problem hiding this comment.
Thanks Jinghan for the Pr. I've added some minor comments. Please let me know if you have any question.
7b81d01 to
d239a3d
Compare
| Path string `json:"path"` | ||
| Value any `json:"value,omitempty"` | ||
| Condition string `json:"condition"` | ||
| ValueProgram string `json:"valueProgram,omitempty"` |
There was a problem hiding this comment.
nit: Would be Expression or ValueExpression better?
There was a problem hiding this comment.
I initially named it ValueExpression in override.go to reflect its string type, and ValueProgram in mutation.go to reflect its compiled cel.Program type. However, since the other three fields (Path, Condition, Value) use identical names across both files, I was concerned that having different names for this new field might introduce unnecessary confusion. So I opted to align them both as ValueProgram for consistency.
But if the naming discrepancy wouldn't cause confusion, I'm happy to rename it back to ValueExpression in override.go to more accurately reflect its string type!
| [ | ||
| { | ||
| "path": "self.spec.resourcePolicy.containerPolicies[0].maxAllowed.memory", | ||
| "valueProgram": "self.spec.resourcePolicy.containerPolicies[0].maxAllowed.memory", |
| StatusMissingParent Status = "MissingParent" | ||
| StatusIndexOutOfRange Status = "IndexOutOfRange" | ||
| StatusPathTypeMismatch Status = "PathTypeMismatch" | ||
| StatusInvalidValueProgram Status = "InvalidValueProgram" |
| val, err := enocel.Eval(ctx, o.ValueProgram, comp, current, o.Path) | ||
| if err != nil { | ||
| logger.Error(err, "failed to evaluate value expression", "path", o.Path.String()) | ||
| return StatusInvalidValueProgram, nil |
| return StatusInvalidValueProgram, nil | ||
| } | ||
| resolvedValue = val.Value() | ||
| if resolvedValue == nil || resolvedValue == structpb.NullValue_NULL_VALUE { |
There was a problem hiding this comment.
What if the user want to unset a value?
| cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w= | ||
| cloud.google.com/go/workflows v1.13.0/go.mod h1:StCuY3jhBj1HYMjCPqZs7J0deQLHPhF6hDtzWJaVF+Y= | ||
| codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= | ||
| codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= |
There was a problem hiding this comment.
Where are we using those changes?
|
|
||
| if resolvedValue == structpb.NullValue_NULL_VALUE { | ||
| logger.Info("value expression evaluated to null (explicit unset)", "path", o.Path.String()) | ||
| return StatusActive, nil |
There was a problem hiding this comment.
nit: Why are we returning StatusActive here?
| if resolvedValue == structpb.NullValue_NULL_VALUE { | ||
| logger.Info("value expression evaluated to null (explicit unset)", "path", o.Path.String()) | ||
| return StatusActive, nil | ||
| } |
There was a problem hiding this comment.
Where do we apply the value as "null"?
There was a problem hiding this comment.
Did we verify that it works in e2e? Shall we add an e2e test case as well?
ee62c03 to
5fe02c7
Compare
4f23833 to
26cfb86
Compare
Summary
Adds a
valueProgramfield to overrides, allowing CEL expressions to dynamically compute override values from the current (live) resource state. Previously, overrides could only set static values. WithvalueProgram, the override value is evaluated at apply time against the actual cluster resource, enabling scenarios like preserving VPA-adjusted resource quantities.Motivation
When external actors (e.g., VPA) modify resource fields, Eno's static overrides couldn't reference the current cluster state to decide what value to use.
valueProgramsolves this by letting a CEL expression read from the live resource and return a computed value.Changes
internal/resource/mutation/mutation.goValueProgram cel.Programfield toOpstructvalueProgramJSON field tojsonOpfor deserializationUnmarshalJSON: parsesvalueProgramstring into a compiledcel.Programviaenocel.Parse()Apply: ifValueProgramis set, evaluates it against the current resource state to resolve the value dynamically; skips mutation if current is nil or CEL evaluates to nullinternal/resource/mutation/mutation_test.govalueProgramevaluation, null handling, and missing current resource scenariospkg/function/overrides/override.goValueProgram stringfield toOverridestructvalidate(): ifValueProgramis non-empty, parses it as CEL to ensure validitypkg/function/overrides/override_test.goValueProgramvalidation andOverride.Test()behaviorinternal/controllers/reconciliation/overrides_test.govalueProgramthrough the fullSnapshotWithOverrides→Op.Applypathdocs/overrides.mdvalueProgramfield with usage examplesTesting
go test ./internal/resource/mutation/... ./pkg/function/overrides/... ./internal/cel/...)