-
Notifications
You must be signed in to change notification settings - Fork 39
Automatic resource pruning EP #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d0b6bf6
c74b8d9
6b88da2
f5db387
cf5e9aa
687d618
15d5381
02cd266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
--- | ||
title: automatic-resource-pruning | ||
authors: | ||
- "@ryantking" | ||
reviewers: | ||
- "@jmrodri" | ||
- "@gallettilance" | ||
- "@fgiloux" | ||
- "@joelanford" | ||
approvers: | ||
- "@jmrodri" | ||
creation-date: 2022-01-28 | ||
last-updated: 2022-05-03 | ||
status: implementable | ||
--- | ||
|
||
# Automatic Resource Pruning | ||
|
||
## Summary | ||
|
||
This EP will provide a way for operator authors to limit the number of unbounded resources that may exist on the | ||
cluster. In the context of this EP, we define an "unbounded resource" as any resource that has the following two | ||
properties: | ||
|
||
1. The resource is continuously created as the operator runs. | ||
2. The resource is not removed as the operator runs. | ||
|
||
When those two properties exist for a resource, the total number of that resource will grow unbounded. The functionality | ||
introduced by this EP will make it possible for operator authors to easily control the entire life cycle of a resource | ||
by automatically removing resources that meet specific criteria. These criteria can include properties of individual | ||
resources, the entire set of resources, or a combination of both. | ||
|
||
## Motivation | ||
|
||
Often, operators will create unbounded resources during execution. For example, if we imagine an operator that | ||
implements Kubernetes' builtin `CronJob` functionality, every time the operator reconciles and finds a `CronJobs` to | ||
run, it creates a new `Job` type to represent a single execution of the `CronJob`. Users will often want to have access | ||
to theses unbounded resources in order to view historical data, but want to limit the number that can exist on the | ||
system. Looking again at Kubernetes out-of-the-box functionalities, users can configure the retention policy for | ||
resources such as `ReplicaSets` and `Jobs` to maintain a certain amount of historical data. Operator authors should have | ||
a defined path for implementing the same functionality for their operators. | ||
|
||
### Goals | ||
|
||
- Add a library to [operator-lib](https://github.com/operator-framework/operator-lib) that houses this functionality | ||
with a user-friendly API. | ||
- Add a happy path for what we determine to be common use cases, such as removing operator-owned `Pods` that are both in | ||
a finished state and older than a certain age. | ||
- Provide an easy way for operator authors to plug in custom logic for their custom resource types and use cases. | ||
|
||
### Non-Goals | ||
|
||
- Add auto-pruning to any of the templates or scaffolding functionality. | ||
- Adding auto-pruning support for Helm or Ansible operators. | ||
|
||
## Proposal | ||
|
||
The proposed implementation is adding a package, `prune`, to | ||
[operator-lib](https://github.com/operator-framework/operator-lib) that exposes this functionality. There will be a | ||
primary entry point function that takes in configuration and prunes resources accordingly. The configuration will accept | ||
one or resource types, a pruning strategy, namespaces, label selectors, and other common settings such as a dry run | ||
mode, hooks, and logging configuration. | ||
|
||
Another aspect of the library will be determining when it can and cannot prune a resource. For example, the prune | ||
functionality should not remove a running `Pod` until it has completed, even if it meets the strategy criteria. The | ||
library will expose a way for operator authors to specify what criteria makes a resource of a specific type safe to | ||
prune; this can include checking annotations, a status, or any other data available on the resource. An important | ||
distinction to draw is the difference between a strategy and checking whether it is safe to prune a resource (henceforth | ||
called the "is-pruneable" functionality). Strategies look generically at collections of resources and decide which | ||
resources in the collection to prune, if any. They should only take criteria common to all Kubernetes resources into | ||
account such as the count of resources and creation timestamp. The is-pruneable functionality conversely looks at one | ||
and only one resource at a given point in time to determine whether or not the current prune task can remove a resource | ||
based on its specific data. | ||
|
||
Note that strategies will not be programmatically limited to being resource agnostic, but it will be a defined best | ||
practice to write strategies in such a way. One exception to this recommendation will be when the operator author wants | ||
to prune based on a cumulative value such as the summation of a field across multiple resources. The operator author | ||
must then be sure to add safe guards to the strategy to avoid unexpected behavior if used with an incompatible resource. | ||
|
||
### User Stories | ||
|
||
#### Story 1 | ||
|
||
An an operator author, I want to limit the number of operator-owned `Jobs` in a completed state on the cluster so that | ||
the long term storage cost of my operator has a cap. | ||
|
||
ryantking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#### Story 2 | ||
|
||
As an operator author, I want to limit the number of operator-owned `Pods` in a completed state on a the cluster and | ||
preserve the `Pods` in an error state so that the long term storage cost of my operator has a soft cap and the user can | ||
see status information about failed `Pods` before manually removing. | ||
|
||
#### Story 3 | ||
|
||
As an operator author, I want to limit the number of operator-owned `Pods`with a custom heuristic based on the creation | ||
timestamp so that the long term storage cost of my operator has a cap based on my operator's logic. | ||
|
||
ryantking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#### Story 4 | ||
|
||
As an operator author, I want to prune a custom resource with specific state information when there is a certain number | ||
so that the long term storage cost of my operator has a cap. | ||
|
||
#### Story 5 | ||
|
||
As an operator author, I want to prune both operator-owned `Jobs` and `Pods` of a certain age so that the long term | ||
storage cost of my operator has a cap and there are no orphaned resources. | ||
|
||
#### Story 6 | ||
|
||
ryantking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
As an operator author, I want to prune a custom resource with specific state information with a custom strategy so that | ||
the long term storage cost of my operator has a cap. | ||
|
||
### Implementation Detail | ||
|
||
- A strategy is a function that takes in a collection of resources and returns a collection of resources to remove. | ||
- The identifier for a resource types will be `GroupVersionKind` value. | ||
- An is-pruneable function takes in one instance of a resource and returns an error value that indicates if it is safe | ||
to prune or has other issues. | ||
- The library will provide built-in is-pruneable functions for `Pods` and `Jobs` that can be overwritten. | ||
- A registry will hold a mapping of resource types (`GVKs`) to is-pruneable functions. | ||
- The library will use `client.Object` from controller-runtime to reference Kubernetes objects since it includes both | ||
`metav1.Object` and `runtime.Object`. | ||
- The library will perform all Kubernetes operations with a dynamic client to support custom resources. | ||
|
||
The proposed API is in [Appendix A](#appendix-a). | ||
|
||
### Risks and Mitigations | ||
|
||
The primary risk associated with this EP is exposing too many knobs and features in the day 1 implementation. We can | ||
mitigate this by only exposing functionality that is absolutely needed. APIs are easy to grow, but near-impossible to | ||
shrink. | ||
|
||
## Design Details | ||
|
||
### Test Plan | ||
|
||
The following components will be unit tested: | ||
|
||
- The builtin strategies. | ||
- The builtin is-pruneable functions. | ||
- The main prune routine. | ||
|
||
The feature author will add an integration test suite that runs the prune routine in the use cases defined in the user | ||
stories. | ||
|
||
## Implementation History | ||
|
||
[operator-framework/operator-lib#75](https://github.com/operator-framework/operator-lib/pull/75): Implements a first | ||
pass of the prune package with only support for `Jobs` and `Pods`. The API is also slightly different than the one | ||
proposed in this EP. | ||
|
||
[operator-framework/operator-lib#105](https://github.com/operator-framework/operator-lib/pull/105): Refactor of the first implementation that uses the API designed in this EP. | ||
|
||
## Drawbacks | ||
|
||
The user will need to manually integrate this functionality into their operator since it is a library. | ||
|
||
## Alternatives | ||
|
||
### Scaffolding | ||
|
||
An alternative approach would be adding this logic to the core SDK and scaffolding it optionally during operation | ||
generation. The primary drawbacks with this approach are the increased complexity to the implementation and adding it to | ||
existing operators. | ||
Comment on lines
+162
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A second alternative would be to implement a brand new operator that exposes a new set of prune APIs, e.g.:
Where the spec could look something like: spec:
objects:
- group: example.com
version: v1
kind: MyKind
matchers:
- selector:
matchLabels:
example.com/is-completed: true
- maxAge: 10h
default:
matchers:
- selector:
matchLabels:
example.com/is-completed: true
- maxCount: 50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @joelanford would this operator be deployed with each operator? I can't see how a new operator would help individual operators cleanup their resources. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would likely be deployed once per cluster, and then two scenarios would be in play:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The feature already exists for jobs and TTL. Here is the KEP, which foresees that it can be extended to pods. IMHO a drawback of a generic controller is that it is either limited to the greatest common factor, which basically means the resource metadata or you need to implement special logic for each resource. Having the specialized logic together with the controller that created the resources may offer more flexibility to the operator author. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fgiloux Yep, all good points. I'm not necessarily suggesting we should actually pursue the alternative, but it may be helpful to consider it in whatever design we come up with. The main motivation for something like a separate controller is to make it extremely easy for an operator author to say, "here, prune this stuff in this way" without having to worry about the details and mechanics of how all that happens. We could implement this as a library that operator authors plug into their controllers as well. For example, perhaps they would just add something in maxAge := time.Minute * 60
if err := prune.Prune(ctx, mgr, prune.Config{
Selectors: prune.Selectors{
&myapi.OtherKind{}: prune.MaxAge(maxAge),
},
PruneInterval: 5 * time.Minute,
}); err != nll {
setupLog.Error(err, "pruner failed")
os.Exit(1)
} where the prune package has something like: package prune
type Selectors map[client.Object]Selector
type Selector interface {
Select(ctx context.Context, in []client.Object) ([]client.Object, error)
}
type SelectorFunc func(ctx context.Context, in []client.Object) ([]client.Object, error)
func (f SelectorFunc) Select(ctx context.Context, in []client.Object) ([]client.Object, error) {
return f(ctx, in)
}
func MaxAge(maxAge time.Duration) SelectorFunc {
return SelectorFunc(func(ctx context.Context, in []client.Object) ([]client.Object, error) {
var out []client.Object
for _, obj := range in {
if in.GetCreationTimestamp().Before(time.Now().Add(-maxAge)) {
out = append(out, obj)
}
}
return out
})
} |
||
|
||
### Separate Operator | ||
|
||
Another alternative would be exposing a set of pruning APIs that would configure an operator that handles all resource | ||
pruning. See [Appendix B](#appendix-b) for an example as to what the spec for a pruning CRD could look like. The | ||
advantage to this approach is that configuring pruning logic would be simple since the user would only have to add one | ||
resource to the cluster to enable pruning. The major disadvantage would be any operator that relies on or encourages | ||
pruning would add a dependent operator that must me managed. | ||
|
||
## Appendix A | ||
|
||
The following is the proposed Go API: | ||
|
||
```go | ||
// StrategyFunc takes a list of resources and returns the subset to prune. | ||
type StrategyFunc func(ctx context.Context, objs []client.Object) ([]client.Object, error) | ||
|
||
// Unpruneable is an error that indicates that the pruner should not prune an object. | ||
type Unpruneable struct { | ||
Object *client.Object | ||
Reason string | ||
} | ||
|
||
// Error returns a string reprenstation of an `Unpruneable` error. | ||
func (e *Unpruneable) Error() string { return "" } | ||
|
||
// IsPruneableFunc is a function that checks a the data of an object to see whether or not it is safe to prune it. | ||
ryantking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// It should return `nil` if it is safe to prune, `Unpruneable` if it is unsafe, or another error. | ||
// It should safely assert the object is the expected type, otherwise it might panic. | ||
type IsPruneableFunc func(obj client.Object) error | ||
|
||
// RegisterIsPruneableFunc registers a function to check whether it is safe to prune a resources of a certain type. | ||
func RegisterIsPrunableFunc(gvk schema.GroupVersionKind, isPruneable IsPruneableFunc) { /* ... */ } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure why IsPruneableFunc get registered and StrategyFunc not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think because |
||
|
||
// Pruner is an object that runs a prune job. | ||
type Pruner struct { | ||
// ... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having had a closer look at the EP I would very much like to see what you intend to have in |
||
} | ||
|
||
// PrunerOption configures the pruner. | ||
type PrunerOption func(p *Pruner) | ||
|
||
// NewPruner returns a pruner that uses the given strategy to prune objects. | ||
func NewPruner(client client.Client, gvk scheme.GroupVersionKind, *opts ...PrunerOption) (*Pruner, error) { | ||
return &Pruner{}, nil | ||
} | ||
|
||
// Prune performs a single instance. | ||
func (p Pruner) Prune(ctx context.Context) ([]client.Object, error) { return nil, nil } | ||
``` | ||
|
||
## Appendix B | ||
|
||
The following is an example of what a prune CRD could look like: | ||
|
||
```go | ||
spec: | ||
objects: | ||
- group: example.com | ||
version: v1 | ||
kind: MyKind | ||
matchers: | ||
- selector: | ||
matchLabels: | ||
example.com/is-completed: true | ||
- maxAge: 10h | ||
default: | ||
matchers: | ||
- selector: | ||
matchLabels: | ||
example.com/is-completed: true | ||
- maxCount: 50 | ||
``` |
Uh oh!
There was an error while loading. Please reload this page.