Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: aliok The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
9d5a39c to
c414f65
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Legacy KPA settings can be lost during migration, and some invalid KEDA bounds bypass deployer-level validation.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 3
gauron99
left a comment
There was a problem hiding this comment.
Few migration simplifications and some leftover code to remove and we should be good to go!
| // stored scale under deploy.options.scale). The 0.34.0 migration writes to | ||
| // it and the 0.38.0 migration moves it to Function.Scale. Hidden from the | ||
| // JSON schema so new files never use this path. | ||
| Scale *ScaleOptions `yaml:"scale,omitempty" jsonschema:"-"` |
There was a problem hiding this comment.
This keeps a field in the live Function only so migrations can use it. Migrations should read old shapes from their own snippet of the raw YAML, not from the current struct. migrateGitToSource does that for build.git, which is no longer on BuildSpec. migrateScaleToTopLevel already decodes its own oldScale from disk, so it doesn't need this field.
Keeping it has a real cost: a current-spec file that still has deploy.options.scale is accepted silently. The 0.38.0 migration does not run, no deployer reads it, Validate() no longer checks it, and Write() keeps it. The unknown-field warning in Migrate() never fires either, because the key still maps to a field.
Repro: specVersion: 0.38.0 with deploy.options.scale: {min: -5, max: 3} gives f.Scale == nil, Validate() == nil, and the block is written back unchanged.
Anyone copying an older example gets no scaling and no error. Please drop Options.Scale, together with the in-memory fallback in the migration that reads it. Then the strict unmarshal warns on the stale key.
There was a problem hiding this comment.
Done in eabeb16d. Dropped Options.Scale entirely and made migrateScaleToTopLevel self-contained: it now reads deploy.options.scale from its own disk snippet (like migrateGitToSource does for build.git), falling back to the pre-0.34 top-level options.scale — which is still on disk when the migration runs, since each migration re-reads the original file and migrateToSpecsStructure no longer stages it in-memory.
Reading that block with the old shape (rather than the current ScaleOptions) also recovers the pre-0.34 flat metric/target/utilization fields, so it incidentally fixes the pre-0.34 half of the Copilot drop finding too.
Also removed the in-memory fallback branch and the now-unnecessary in-memory clear. As you noted, the real win: an at-spec func.yaml with a stale deploy.options.scale now surfaces via the unknown-fields warning instead of being silently accepted. Added TestUnknownFieldsWarnOnStaleScale to lock that in.
| } | ||
| } | ||
|
|
||
| func TestMigrateScaleToTopLevel(t *testing.T) { |
There was a problem hiding this comment.
Please drive these through NewFunction(root) on the inline func.yaml instead of calling migrateScaleToTopLevel with a hand-built Function. Hand-built states that can't happen in practice (e.g. empty Deploy.Deployer with a value on disk) are what the write-back at line 536 was written for.
There was a problem hiding this comment.
Done in fa7d20aa. All TestMigrateScaleToTopLevel subtests now go through NewFunction(root) on inline func.yaml (via a small newFn helper) instead of hand-building a Function and calling migrateScaleToTopLevel directly. SpecVersion assertions became LastSpecVersion(), and the pre-0.34 lift case uses specVersion: 0.33.0 so it actually flows through migrateToSpecsStructure too.
Dropped the "empty Root" subtest — it was the only hand-built state (no backing file), un-drivable through NewFunction, and it only exercised the write-back that's now gone (see the other thread).
| // from values specified in function configuration options | ||
| func setServiceOptions(template *servingv1.RevisionTemplateSpec, options fn.Options) error { | ||
| // from values specified in function configuration options and scale config. | ||
| func setServiceOptions(template *servingv1.RevisionTemplateSpec, scale *fn.ScaleOptions, options fn.Options) error { |
There was a problem hiding this comment.
This is the only consumer of scale.kpa, and the mapping to annotations has no unit test. The only coverage is TestInt_Deploy_WithOptions, which needs a cluster. A small unit test calling setServiceOptions with KPA set, KPA nil, and scale nil, asserting which annotations are added and removed, would cover it.
There was a problem hiding this comment.
Done in 75a2e2c4. Added TestSetServiceOptions_KPAAnnotations in pkg/knative/deployer_scale_test.go, a pure unit test (no cluster) covering the three shapes:
- KPA set →
min/maxplusmetric/target/utilizationannotations are added with the expected values. - KPA nil (min/max still set) → the three kpa annotations are removed,
min/maxkept. scalenil → the whole block is skipped, so pre-existing autoscaling annotations are left untouched.
TestInt_Deploy_WithOptions still covers the cluster path.
| // needs no handling here: the in-memory value already reflects whatever | ||
| // was set via the Go field name, so there's nothing on disk to migrate | ||
| // from and nothing to fall back to. | ||
| if disk.Deploy.Deployer != "" { |
There was a problem hiding this comment.
This write-back is a no-op on every real path. NewFunction unmarshals the same bytes into f, and deploy.deployer/deploy.expose keep their yaml tags, so the values are already there. hasInitializedFunction calls Migrate with Root == "", so nothing is read from disk at all. expose has nothing to do with scale either.
It looks carried over from #4051, together with the #4054 NOTE around line 448. Please drop the block, the NOTE, and the subtests that only cover it:
- "old deploy.deployer/deploy.expose keys populate the observed-state fields"
- "migration never touches f.Deployer intent"
- "no-op when neither old deployer/expose key is present"
- "empty Root does not touch the in-memory deployer/expose value"
This holds on its own. If the migration becomes a plain move (see the comment on line 503), the rest of the deployer handling here goes as well.
There was a problem hiding this comment.
Done in fa7d20aa. Removed the write-back block and the deploy.deployer/deploy.expose disk read that fed it — you're right it was a no-op on every real path: NewFunction unmarshals those keys onto f (via their yaml tags) before migrations run, and hasInitializedFunction migrates with Root == "" so nothing is read from disk at all. Either way the values are already on f. expose was unrelated to scale, and the #4054 NOTE went with it.
One thing kept: the kpa-gating still needs the observed deployer for pre-#3953 files (deployer recorded only under deploy.deployer, so f.Deployer is empty) — otherwise the legacy flat fields would be lifted into a scale.kpa that raw/keda rejects. But per the same reasoning, it now reads f.Deploy.Deployer directly (already populated by the primary unmarshal) instead of re-reading disk, so the redundant read is gone with no behavior change.
There was a problem hiding this comment.
Update: the one piece I said I kept here — the deployer-gating reading f.Deploy.Deployer — has now been removed too (919a65e1). Adopting your other comment fully (plain move + scale.kpa ignored-with-warning) means there's no reason to gate the lift on deployer, so the migration no longer inspects the deployer at all. The flat fields are lifted into scale.kpa for every deployer; raw/keda just warn-and-ignore at deploy time.
| intentKnative := f.Deployer == "" || f.Deployer == "knative" | ||
| observedKnative := observedDeployer == "" || observedDeployer == "knative" | ||
| validKPADeployer := intentKnative && observedKnative | ||
| if hasFlat && newScale.KPA == nil && validKPADeployer { |
There was a problem hiding this comment.
The flat metric/target/utilization were only ever read by the knative deployer (setServiceOptions). raw reads only min and keda only min/max; both ignore the rest. So the migration can stay a plain move: lift the whole old block to scale (min/max as is, the three flat fields into scale.kpa) for every deployer, without looking at the deployer at all.
To keep the behavior raw/keda users have today, scale.kpa on raw/keda should be ignored with a warning rather than rejected, the same way warnExposeIgnore in cmd/deploy.go handles expose on knative. That means the scale.kpa requires deployer: knative rule in ValidateScale becomes a warning.
This removes from the migration: validKPADeployer and the intent/observed deployer logic, the deploy.deployer/deploy.expose read from disk, oldScale.KPA (no released func ever wrote deploy.options.scale.kpa), and the subtests about the deployer check. It also fixes the current silent drop of the flat fields from func.yaml for raw/keda.
There was a problem hiding this comment.
Done in 919a65e1 — adopted this fully. migrateScaleToTopLevel is now a plain move: min/max as-is and the flat metric/target/utilization into scale.kpa, for every deployer, with no deployer inspection at all. That drops validKPADeployer and the intent/observed logic, the oldScale.KPA field (no released func ever wrote deploy.options.scale.kpa), and it fixes the silent drop of those flat fields for raw/keda.
scale.kpa requires deployer: knative in ValidateScale is gone; scale.kpa on raw/keda is now ignored-with-warning at deploy time via warnScaleKpaIgnore in cmd/deploy.go, mirroring warnExposeIgnore. The kpa values are still validated regardless of deployer, so a bad metric/target/utilization is still caught.
Two follow-on effects, noted on their own threads: the deployer-gating I'd kept is now removed entirely, and the f.Deployer = deployer recovery in the tekton provider is removed — its only purpose was to make that rule hard-fail. The migration subtests that asserted "no scale.kpa for non-knative" are inverted accordingly.
c414f65 to
d9c74ea
Compare


This is Part 1 of 2 splitting #4051 into reviewable pieces. Together with the follow-up Part 2, this PR supersedes #4051.
scalefield and its migration.scale.kedablock: scaler types and triggers).Part 1 contains no KEDA scale-config surface — it's the foundational schema change that Part 2 builds on.
What changes
Autoscaling configuration moves from the nested
deploy.options.scaleto a top-levelscalefield:scale.min/scale.max— deployer-agnostic replica bounds (0 … 2147483647).scale.kpa—metric/target/utilization, consumed only by theknativedeployer (and the unset default, which behaves as knative). On the raw and keda deployers it is ignored with a warning at deploy time rather than rejected.deploy.optionsnow exposes onlyresources.Details
0.38.0(migrateScaleToTopLevel) lifts the old flatdeploy.options.scale(min/max/metric/target/utilization) to the new top-levelscale, folding metric/target/utilization intoscale.kpa. It is a deployer-agnostic plain move: the flat fields were only ever read by the knative deployer, so they are lifted for every deployer and simply ignored (with a warning) where unused, rather than being silently dropped for raw/keda.deploy.options.scale(and the pre-0.34 top-leveloptions.scale) directly from the raw YAML on disk.deploy.optionsno longer carries ascalefield at all, so a staledeploy.options.scaleleft in a current-spec file surfaces as an unknown key rather than being silently accepted.f.Scale.schema/func_yaml-schema.jsonand updateddocs/reference/func_yaml.md.Breaking changes
Options.Scaleremoved from thefunctionspackage. func.yaml files are migrated automatically, so end users are unaffected, but Go/library consumers referencingf.Deploy.Options.Scalewill no longer compile — readf.Scaleinstead.Behavior notes for reviewers
scaleis unset differ per deployer (unchanged in spirit, just relocated): raw → 1 replica; keda → min 1 / max 10; knative → Knative's own autoscaling defaults.scaleis validated against the effective deployer — the intent (f.Deployer) when set, otherwise the last-deployed deployer (f.Deploy.Deployer) — so the deployer-specific rules (e.g. keda'smax >= 1) apply consistently across the CLI, the tekton pipeline path, and library callers.replicaBoundsre-checks at deploy time as a backstop.deployers.ValidateSwitchgate (func deletefirst). Part 1 doesn't change that; the analogous scaler/trigger switch gate lands in Part 2.