Skip to content

Conversation

abdelrahman882
Copy link
Contributor

@abdelrahman882 abdelrahman882 commented Sep 11, 2025

What type of PR is this?

/kind feature

What this PR does / why we need it:

Add Capacity Buffer controller loop along with the main needed skeleton for buffers with podTemplateRef.

Special notes for your reviewer:

This PR includes:

  • Filters: Capacity Buffers provisioning strategy and status filtering
  • Translators: podTemplateRef translator that updates buffer status accordingly
  • Updater: updates buffer status via capacity buffer client
  • Controller: Initiates the needed components and contains the reconciliation loop

This PR is not including:

  • Capacity buffers resources limits and scalable objects (those will be introduced as translators)

  • Buffers updating logic (use PodTemplateGeneration for updating buffer status)
    These 2 points will be included in a following PR together

  • Running controller in CA and CA injection will be in separate PR

Proposal document: https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/buffers.md

Does this PR introduce a user-facing change?

no

Add Capacity Buffer controller logic

Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

[AEP]:  https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/buffers.md

@k8s-ci-robot k8s-ci-robot added kind/feature Categorizes issue or PR as related to a new feature. do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/needs-area size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. area/cluster-autoscaler labels Sep 11, 2025
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: abdelrahman882
Once this PR has been reviewed and has the lgtm label, please assign feiskyer for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@abdelrahman882 abdelrahman882 force-pushed the capacity-buffer-controller branch 4 times, most recently from 555355e to 829faae Compare September 11, 2025 12:40
Copy link
Contributor

@BigDarkClown BigDarkClown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of duplicate comments, most important to address:

  • Naming should not include buffer infixes, it is redundant
  • Implementations should be private and not exposed outside of packages.
  • Cleanup methods for filters remove their configs, this is a bug.
  • The usage of filter interface should be consistent, eg. we should use the first return value in both cases. This will make it easier to maintain the implementations without caring about the use cases.

Copy link
Contributor

@jbtk jbtk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the logic and structure. Did not look into tests as I have seen that @BigDarkClown did that already.

select {
case <-stopCh:
return
case <-time.After(c.loopInterval):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we run the loop only if there are any buffers and have a watcher on creation of new so that it does not run without a good reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the reply below that will contradict with the periodic check over updated buffer objects and quota checking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that we should not run the loop at all if there is no a single buffer object and for starting the loop we should rely on a watch that will inform us that a buffer was created. Once there are any buffers in the cluster we should run periodically and if all get deleted go back to relying on the watch. What would it contradict?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Note that this can be implemented as optimization later)


// Constants to use in Capacity Buffers objects
const (
ActiveProvisioningStrategy = "active-capacity"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in common or should it be in some dedicated filter/translator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provisioning filter is generic, it filters the buffers with strategy defined when constructing, so we pass ActiveProvisioningStrategy to the generic provisioningFilter in the controller.

I would prefer keeping it in common because filter package is not only for strategy filtering so filter.ActiveProvisioningStrategy wouldn't give much more meaning imo and also in case it would be used by other filters/translators.

But It's not strong opinion, I would change it if you insist

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that we would keep the logic for a single buffer type in a single place and therefore keep the name together with translation logic and only make the buffer controller rely on the list of supported types. They would reuse the translation logic so that it is not implemented multiple times.

This is simple refactoring though so let's focus on having something working end to end first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the plan was to go with "buffer.x-k8s.io/active-capacity" rather than just active-capacity as per discussion from https://docs.google.com/document/d/1bcct-luMPP51YAeUhVuFV7MIXud5wqHsVBDah9WuKeo/edit?disco=AAABj1R4vNs

(this will require update to validations)

nodeBufferListener v1.CapacityBufferLister,
kubeClient kube_client.Clientset,
) BufferController {
return &bufferController{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we would split it more by buffer prov strategy or so in case different strategies need different handling, but for now I would say that it may be better to leave it as is and make sure that when adding second one we will introduce a structure that makes sense at that time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this structure it would be very hard to implement for example if translation of pods to pod template differs depending on the provisioning strategy. But as mentioned - not worth creating the whole structure up front. The logic seems well split to be reused and this is more important now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding was that the controller will be operating on only one provisioningStrategy - So just to double check that I understood correctly, we need to have the provisioningStrategies mapped to set of translators and depending on the defined strategy we route the buffer to its corresponding translator, is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether we need this structure right away but I was assuming that we have a single controller to support all the provisioning strategies that have built in support. (If the user wants to have some custom ones they are free to implement their own controller and handle their translation and execution).

return errors
}

func (b *PodTemplateBufferTranslator) translate(buffer *api_v1.CapacityBuffer) (*api_v1.LocalObjectRef, int32, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When do you plant to take the buffer.spec limit into account? Also the error is incorrect if you take into account the limits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limits will be applied by adding another translator for the limits, which will be executed last and will enforce the limits on the number of pods overriding replicas wrote by previous translators (podTemplateRef or scalableRef).

For your question when, It will be in separate PR right after merging this one, I have it already implemented but wrapping up its unit tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the error will be incorrect as we were planning to support an option: do not provide number of replicas and we will count how many replicas fit into the limit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather have a single place where we calculate the whole translation but provide libraries that handle limits or quotas so that you do not have to reimplement if there are many buffers that need the same logic. Chaining translators is like chaining processors - powerful but later to make any changes you need to understand the whole chain.

Copy link
Contributor Author

@abdelrahman882 abdelrahman882 Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's better to keep it chained in this way, I mean to make a change you don't need to understand the whole chain at all but rather understand what you want you translator to do only.

One note, I will need to double check the API then because we have Xor=replicas,percentage which I am not sure if it will block having both set to nil

Okay so, my suggestion is to keep it as chained translators, and in the following PR -with introducing limits- I will be double checking the API validation to accept both replicas and percentage as nil along side with fixing this translator here to not return error, does that make sense?

Also I would appreciate if you took a look on the CRD validation and please let me know if you notice something odd.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this in a follow up, but I do not like the idea of chaining in general.

If we provide libraries you will still have to understand only the part that you are modifying, but if you want to read what happens with a single buffer you will just read through a single place that has library calls rather than go through list of translators that are applied one on top of each other making modifying them more complex and error handling much harder. I know that we do a lot of this with processors in our code, but this is because these are extension points. Here I do not see a reason to do it as a list of translators.

@abdelrahman882 abdelrahman882 force-pushed the capacity-buffer-controller branch from 829faae to 312308a Compare September 11, 2025 16:44
return &bufferController{
buffersLister: nodeBufferListener,
strategyFilter: filters.NewStrategyFilter([]string{common.ActiveProvisioningStrategy}),
statusFilter: filters.NewBuffersStatusFilter(map[string]string{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that filtering on status isn't likely a good idea:

  • for pod template we will need to store the generation id. If the pod is update we have to process it once again (as there are limits)
  • for quotas we will need to process all the buffers over and over again to make sure that they fit into quotas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it,

  • For generation id: will update the status filter to include generation so we process those with higher generation id
  • For Quotas: will create another translator that applies on all buffers -including filtered out buffers- that updates status replicas

As mentioned Would add in following PR, this one is big already.

One note here we will need to run the loop periodically and not only if there are any buffers with a watcher on creation -from your previous comment- so we can check quotas and updates, does that make sense to you?

@abdelrahman882 abdelrahman882 force-pushed the capacity-buffer-controller branch 2 times, most recently from 3524194 to 5c1e317 Compare September 11, 2025 19:27
@abdelrahman882 abdelrahman882 force-pushed the capacity-buffer-controller branch from 5c1e317 to 1a7d949 Compare September 11, 2025 19:51
Copy link
Contributor

@BigDarkClown BigDarkClown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One micro nit. Overall LGTM, waiting for LGTM from @jbtk to add approval.

/lgtm

}

func (f *strategyFilter) isAllowedProvisioningStrategy(buffer *v1.CapacityBuffer) bool {
provisioingStrategy := ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo provisioning

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Sep 12, 2025
@jackfrancis
Copy link
Contributor

/release-note-none

@k8s-ci-robot k8s-ci-robot added release-note-none Denotes a PR that doesn't merit a release note. and removed do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. labels Sep 12, 2025
@jackfrancis
Copy link
Contributor

/release-note-edit

Add Capacity Buffer controller logic

@k8s-ci-robot
Copy link
Contributor

@jackfrancis: /release-note-edit must be used with a single release note block.

In response to this:

/release-note-edit

Add Capacity Buffer controller logic

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@jackfrancis
Copy link
Contributor

/release-note-edit

Add Capacity Buffer controller logic

@k8s-ci-robot k8s-ci-robot added release-note Denotes a PR that will be considered when it comes time to generate release notes. and removed release-note-none Denotes a PR that doesn't merit a release note. labels Sep 12, 2025
Copy link
Contributor

@jbtk jbtk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re validations (since the PR is already merged and we are discussing here some of the things):

  • xor on percentage and replicas is wrong - you can define neither (the replicas will be counted based on number of pod templates fitting the limits), you can also define both (this will mean that we will use number of replicas as minimum)
  • maximum for percentage - you could keep it higher than 100, for example 200 would mean that we keep twice the number of replicas compared to the current size of a workload. I guess this is not a common thing to do but I do not see why we should introduce this limitation

) BufferController {
return &bufferController{
buffersLister: nodeBufferListener,
strategyFilter: filters.NewStrategyFilter([]string{common.ActiveProvisioningStrategy, ""}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a comment for why the empty is accepted.

return errors
}

func (b *PodTemplateBufferTranslator) translate(buffer *api_v1.CapacityBuffer) (*api_v1.LocalObjectRef, int32, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this in a follow up, but I do not like the idea of chaining in general.

If we provide libraries you will still have to understand only the part that you are modifying, but if you want to read what happens with a single buffer you will just read through a single place that has library calls rather than go through list of translators that are applied one on top of each other making modifying them more complex and error handling much harder. I know that we do a lot of this with processors in our code, but this is because these are extension points. Here I do not see a reason to do it as a list of translators.


// Constants to use in Capacity Buffers objects
const (
ActiveProvisioningStrategy = "active-capacity"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the plan was to go with "buffer.x-k8s.io/active-capacity" rather than just active-capacity as per discussion from https://docs.google.com/document/d/1bcct-luMPP51YAeUhVuFV7MIXud5wqHsVBDah9WuKeo/edit?disco=AAABj1R4vNs

(this will require update to validations)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/cluster-autoscaler cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/feature Categorizes issue or PR as related to a new feature. lgtm "Looks good to me", indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants