feat: support LimitRange defaults for current requests/limits - #881
Open
abhyudayareddy wants to merge 1 commit into
Open
feat: support LimitRange defaults for current requests/limits#881abhyudayareddy wants to merge 1 commit into
abhyudayareddy wants to merge 1 commit into
Conversation
abhyudayareddy
requested review from
Azahorscak,
sudermanjr and
transient1
as code owners
August 16, 2026 02:18
Contributor
Author
|
cc @sudermanjr — this one builds on the same area as #875/#878, adding LimitRange support for #315. No rush, just making sure it's on your radar alongside the others. |
…ndsOps#315) Namespaces can define a LimitRange that supplies default cpu/memory requests/limits for containers that don't set their own. Previously the dashboard only looked at each container's own resources.Requests/Limits, so any value filled in by a LimitRange showed up as "Not Set" even though a real Pod created from that workload would have gotten an effective value from Kubernetes. GetSummary() now lists each namespace's LimitRange objects once (cached per namespace, so this doesn't add a per-container API call) and resolves the effective requests/limits per container, mirroring the two-step order Kubernetes itself uses: 1. SetDefaults_Pod: an explicit Limit with no explicit Request causes the Request to be defaulted to that Limit, before any admission plugin runs. 2. LimitRanger admission: any resource whose Limit is still unset gets the LimitRange's Default; any resource whose Request is still unset gets DefaultRequest. Because step 1 already ran, a Limit that only becomes set here (via Default) is never copied into a still-unset Request. A value that's still unset after both steps continues to show as "Not Set" -- this only fills gaps Kubernetes itself would fill, it doesn't fabricate values. Container-typed LimitRangeItems only; Pod/PVC-typed items are ignored since they don't constrain an individual container. Explicit values on a container are never touched. ContainerSummary now also records, per resource name, whether a value came from a LimitRange (RequestsFromLimitRange/LimitsFromLimitRange), and the dashboard shows a small "From LimitRange" badge next to those values so it's clear where they came from, following the same badge pattern used for the "Matches Recommendation" indicator.
abhyudayareddy
force-pushed
the
limitrange-support-315
branch
from
August 28, 2026 01:44
addf315 to
7b1ab52
Compare
Contributor
Author
|
Rebased onto latest |
Contributor
Author
|
cc @sudermanjr — checking back in, this one's been quiet for a couple weeks (LimitRange defaults for current requests/limits, #315). Still rebased clean on latest master with no conflicts. Let me know if there's anything I can do to help move it forward. Thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Closes #315.
When a container doesn't set its own
resources.requests/resources.limits, but the namespace has aLimitRangethat provides defaults, Goldilocks previously showed "Not Set" for those values -- even though a real Pod created from that workload would actually receive an effective value from Kubernetes. This made the "current" side of the dashboard misleading in namespaces that rely onLimitRangefor baseline sizing.What changed
Summarizer.GetSummary()(pkg/summary/summary.go) now lists each namespace'sLimitRangeobjects (via the existingkube.ClientInstance, the same client wrapper already used elsewhere) and resolves each container's effective requests/limits before building itsContainerSummary, instead of using the container's raw spec directly.Kubernetes semantics implemented, and why
This intentionally reproduces two separate, ordered steps that real Kubernetes performs, because the order changes the outcome for a case that's easy to get backwards:
SetDefaults_Pod, pkg/apis/core/v1/defaults.go): if a container explicitly sets a Limit for a resource but leaves the Request unset, the Request is defaulted to that explicit Limit. This is decode-time type defaulting that runs before any admission plugin (including LimitRanger) ever sees the object, and it only ever looks at what the container itself already specifies.plugin/pkg/admission/limitranger/admission.go,mergeContainerResources): for any resource whose Limit is still unset after step 1, apply the LimitRange'sDefault. For any resource whose Request is still unset after step 1, applyDefaultRequest.The consequence, verified against the k8s LimitRange memory walkthrough and the actual
SetDefaults_Pod/mergeContainerResourcessource: a Limit that only becomes set via step 2'sDefaultis never copied into a still-unset Request. Only an explicit Limit gets copied to Request (step 1);DefaultRequestis the only thing that can fill a Request left unset after step 1. It's tempting to assume "Limit ends up set -> Request defaults to it" applies regardless of how the Limit got set, but Kubernetes' ordering means that only holds for an explicit Limit. This is covered by a dedicated test case (see below).Other rules implemented:
Container-typedLimitRangeItems apply to a container's own requests/limits;Pod/PersistentVolumeClaim-typed items are ignored.LimitRangeItem) are resolved independently.Design decision on multiple LimitRanges (the one place where "correct" isn't fully pinned down): the Kubernetes docs state plainly that "If two or more LimitRange objects exist in the namespace, it is not deterministic which default value will be applied." For display purposes, this PR approximates LimitRanger's actual "only fill what's still missing" mechanics applied across items in List order: the first applicable item supplies a given resource, later ones are ignored for that resource once it's filled. This is a best-effort, deterministic approximation for the dashboard, not a guarantee of matching live-pod behavior in a namespace with conflicting multiple LimitRanges -- called out in code comments in
pkg/summary/limitrange.go.Namespaces without a LimitRange (the common case)
LimitRanges(namespace).List(...)is called once per distinct namespace per summary generation (cached on theSummarizerfor its lifetime), not once per container/workload/VPA, so this doesn't introduce an N+1. An empty list is a clean no-op -- verified requests/limits are byte-for-byte unchanged and the two existing full-summary regression tests (Test_Summarizer,Test_Summarizer_Daemonset) still pass unmodified against namespaces with noLimitRange.Dashboard
ContainerSummarygainedRequestsFromLimitRange/LimitsFromLimitRange(map[corev1.ResourceName]bool), populated only when a value came from a LimitRange (nil otherwise, so existing JSON/API consumers see no change for the common no-LimitRangecase).pkg/dashboard/templates/container.gohtmlshows a small "From LimitRange" badge next to any such value, following the same visual pattern as the existing "Matches Recommendation" badge (pkg/dashboard/templates/namespace.gohtml) -- same badge CSS structure, just a different color (--color-warning) to keep it visually distinct.Validation performed
go build ./...-- cleango vet ./...-- cleangofmt -lon all changed/added.gofiles -- no outputgolangci-lint run ./...(v2.12.0, locally installed; this repo has no checked-in.golangci.ymlor lint CI workflow to pin a version against) --0 issuesgo test ./...-- all packages pass, including the two new test files:pkg/summary/limitrange_test.go: table-driven unit tests for the resolution function covering no-LimitRange no-op, only-Default, only-DefaultRequest, both set, explicit-values-untouched, cpu/memory resolved independently, Pod-typed items ignored, multiple LimitRange objects (first-applicable-wins), and the explicit-limit-copies-to-request-ignoring-DefaultRequest edge case described above. Also covers the namespace-level LimitRange listing/caching helper (empty namespace, filtering, and that repeated namespace lookups don't re-hit the fake API).pkg/summary/summary_limitrange_test.go: a fullGetSummary()integration test (fake clientsets, following this package's existingkube.GetMockClient()/GetMockVPAClient()/GetMockDynamicClient()conventions) with a container that sets no resources at all, verifying the end-to-end wiring.dashboard.Dashboard()HTTP handler against fixture data (fake clientsets, aLimitRangewithDefaultmemory +DefaultRequestcpu, a container with no explicit resources) and inspected the output HTML. Confirmed: CPU Request =100m(badge shown), CPU Limit =Not Set(no badge, noDefaultconfigured), Memory Request =Not Set(no badge -- noDefaultRequestand nothing explicit to copy), Memory Limit =512Mi(badge shown). This throwaway test file was not committed.Notes
The issue's maintainer said he wasn't sure how hard this would be to implement correctly -- the main risk area was exactly the ordering edge case above (explicit-limit-only vs. Default-only vs. both), which I verified against the actual
SetDefaults_Pod/LimitRanger source and the official walkthrough rather than assuming, since getting it backwards would make the feature actively misleading. The multi-LimitRange tie-break is the one place I made a judgment call rather than following a single documented "correct" behavior, since Kubernetes itself documents that case as non-deterministic.