Skip to content
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

feat: cluster consolidation #2123

Merged
merged 1 commit into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions charts/karpenter/crds/karpenter.sh_provisioners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ spec:
Node properties are determined from a combination of provisioner and
pod scheduling constraints.
properties:
consolidation:
description: Consolidation are the consolidation parameters
properties:
enabled:
description: Enabled enables consolidation if it has been set
type: boolean
type: object
kubeletConfiguration:
description: KubeletConfiguration are options passed to the kubelet
when provisioning nodes
Expand Down Expand Up @@ -207,8 +214,8 @@ spec:
will wait before attempting to delete a node, measured from when
the node is detected to be empty. A Node is considered to be empty
when it does not have pods scheduled to it, excluding daemonsets.
\n Termination due to underutilization is disabled if this field
is not set."
\n Termination due to no utilization is disabled if this field is
not set."
format: int64
type: integer
ttlSecondsUntilExpired:
Expand Down
7 changes: 5 additions & 2 deletions charts/karpenter/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ rules:
resources: ["awsnodetemplates"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods", "nodes", "persistentvolumes", "persistentvolumeclaims"]
resources: ["pods", "nodes", "persistentvolumes", "persistentvolumeclaims", "replicationcontrollers"]
verbs: ["get", "list", "watch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses", "csinodes"]
verbs: ["get", "watch", "list"]
- apiGroups: ["apps"]
resources: ["daemonsets"]
resources: ["daemonsets", "replicasets", "statefulsets"]
verbs: ["list", "watch"]
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
verbs: ["get", "watch", "list"]
- apiGroups: [ "policy" ]
resources: [ "poddisruptionbudgets" ]
verbs: [ "get", "list", "watch" ]
# Write
- apiGroups: ["karpenter.sh"]
resources: ["provisioners/status"]
Expand Down
13 changes: 9 additions & 4 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/clock"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
Expand All @@ -44,6 +45,7 @@ import (
"github.com/aws/karpenter/pkg/cloudprovider/registry"
"github.com/aws/karpenter/pkg/config"
"github.com/aws/karpenter/pkg/controllers"
"github.com/aws/karpenter/pkg/controllers/consolidation"
"github.com/aws/karpenter/pkg/controllers/counter"
metricspod "github.com/aws/karpenter/pkg/controllers/metrics/pod"
metricsprovisioner "github.com/aws/karpenter/pkg/controllers/metrics/provisioner"
Expand Down Expand Up @@ -113,16 +115,19 @@ func main() {
}

recorder := events.NewDedupeRecorder(events.NewRecorder(manager.GetEventRecorderFor(appName)))
cluster := state.NewCluster(cfg, manager.GetClient(), cloudProvider)
realClock := &clock.RealClock{}
cluster := state.NewCluster(realClock, cfg, manager.GetClient(), cloudProvider)
provisioner := provisioning.NewProvisioner(ctx, cfg, manager.GetClient(), clientSet.CoreV1(), recorder, cloudProvider, cluster)

consolidation.NewController(ctx, realClock, manager.GetClient(), provisioner, cloudProvider, recorder, cluster, manager.Elected())
metricsstate.StartMetricScraper(ctx, cluster)

if err := manager.RegisterControllers(ctx,
provisioning.NewController(ctx, cfg, manager.GetClient(), clientSet.CoreV1(), recorder, cloudProvider, cluster),
provisioning.NewController(manager.GetClient(), provisioner, recorder),
state.NewNodeController(manager.GetClient(), cluster),
state.NewPodController(manager.GetClient(), cluster),
node.NewController(manager.GetClient(), cloudProvider, cluster),
termination.NewController(ctx, manager.GetClient(), clientSet.CoreV1(), recorder, cloudProvider),
termination.NewController(ctx, realClock, manager.GetClient(), clientSet.CoreV1(), recorder, cloudProvider),
node.NewController(realClock, manager.GetClient(), cloudProvider, cluster),
metricspod.NewController(manager.GetClient()),
metricsprovisioner.NewController(manager.GetClient()),
counter.NewController(manager.GetClient(), cluster),
Expand Down
2 changes: 1 addition & 1 deletion hack/docs/metrics_gen_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func handleVariableDeclaration(v *ast.GenDecl) []metricInfo {
if funcPkg != "prometheus" {
continue
}
if len(ce.Args) != 2 {
if len(ce.Args) == 0 {
continue
}
arg := ce.Args[0].(*ast.CompositeLit)
Expand Down
11 changes: 6 additions & 5 deletions pkg/apis/provisioning/v1alpha5/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ var (
// Karpenter specific domains and labels
KarpenterLabelDomain = "karpenter.sh"

ProvisionerNameLabelKey = Group + "/provisioner-name"
DoNotEvictPodAnnotationKey = Group + "/do-not-evict"
EmptinessTimestampAnnotationKey = Group + "/emptiness-timestamp"
TerminationFinalizer = Group + "/termination"
ProvisionerNameLabelKey = Group + "/provisioner-name"
DoNotEvictPodAnnotationKey = Group + "/do-not-evict"
DoNotConsolidateNodeAnnotationKey = KarpenterLabelDomain + "/do-not-consolidate"
ellistarn marked this conversation as resolved.
Show resolved Hide resolved
EmptinessTimestampAnnotationKey = Group + "/emptiness-timestamp"
TerminationFinalizer = Group + "/termination"

LabelCapacityType = KarpenterLabelDomain + "/capacity-type"
LabelNodeInitialized = KarpenterLabelDomain + "/initialized"
Expand Down Expand Up @@ -64,7 +65,7 @@ var (
)

// RestrictedLabels are labels that should not be used
// because they may interfer the internal provisioning logic.
// because they may interfere with the internal provisioning logic.
RestrictedLabels = sets.NewString(
EmptinessTimestampAnnotationKey,
v1.LabelHostname,
Expand Down
10 changes: 9 additions & 1 deletion pkg/apis/provisioning/v1alpha5/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type ProvisionerSpec struct {
// detected to be empty. A Node is considered to be empty when it does not
// have pods scheduled to it, excluding daemonsets.
//
// Termination due to underutilization is disabled if this field is not set.
// Termination due to no utilization is disabled if this field is not set.
// +optional
TTLSecondsAfterEmpty *int64 `json:"ttlSecondsAfterEmpty,omitempty"`
// TTLSecondsUntilExpired is the number of seconds the controller will wait
Expand All @@ -70,6 +70,14 @@ type ProvisionerSpec struct {
TTLSecondsUntilExpired *int64 `json:"ttlSecondsUntilExpired,omitempty"`
// Limits define a set of bounds for provisioning capacity.
Limits *Limits `json:"limits,omitempty"`
// Consolidation are the consolidation parameters
// +optional
Consolidation *Consolidation `json:"consolidation,omitempty"`
}

type Consolidation struct {
// Enabled enables consolidation if it has been set
Enabled *bool `json:"enabled,omitempty"`
ellistarn marked this conversation as resolved.
Show resolved Hide resolved
}

// +kubebuilder:object:generate=false
Expand Down
7 changes: 6 additions & 1 deletion pkg/apis/provisioning/v1alpha5/provisioner_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"strconv"

"github.com/aws/aws-sdk-go/aws"
"go.uber.org/multierr"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
Expand All @@ -28,7 +29,7 @@ import (
)

var (
SupportedNodeSelectorOps sets.String = sets.NewString(
SupportedNodeSelectorOps = sets.NewString(
string(v1.NodeSelectorOpIn),
string(v1.NodeSelectorOpNotIn),
string(v1.NodeSelectorOpGt),
Expand Down Expand Up @@ -70,6 +71,10 @@ func (s *ProvisionerSpec) validateTTLSecondsAfterEmpty() (errs *apis.FieldError)
if ptr.Int64Value(s.TTLSecondsAfterEmpty) < 0 {
return errs.Also(apis.ErrInvalidValue("cannot be negative", "ttlSecondsAfterEmpty"))
}
// TTLSecondsAfterEmpty and consolidation are mutually exclusive
Copy link
Contributor

Choose a reason for hiding this comment

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

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 far as I know, controller-gen doesn't support this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great suggestion, but I'm cautious of scheme-level validation. It feels a bit half baked and it's challenging to model enough of your validation using it that it becomes worth it. Given that we're already using a webhook, I'd lean towards making our validation logic explicit and avoiding schema-level validation (one thing to think about vs two)

if s.Consolidation != nil && aws.BoolValue(s.Consolidation.Enabled) && s.TTLSecondsAfterEmpty != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

ptr is probably better to use here so we're not depending on the aws-sdk in Karpenter core. Feel free to change in follow-up.

return errs.Also(apis.ErrMultipleOneOf("ttlSecondsAfterEmpty", "consolidation.enabled"))
}
return errs
}

Expand Down
20 changes: 20 additions & 0 deletions pkg/apis/provisioning/v1alpha5/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package v1alpha5

import (
"context"
"github.com/aws/aws-sdk-go/aws"
"strings"
"testing"

Expand Down Expand Up @@ -64,6 +65,25 @@ var _ = Describe("Validation", func() {
provisioner.Spec.TTLSecondsAfterEmpty = nil
Expect(provisioner.Validate(ctx)).To(Succeed())
})
It("should succeed on a valid empty ttl", func() {
provisioner.Spec.TTLSecondsAfterEmpty = aws.Int64(30)
Expect(provisioner.Validate(ctx)).To(Succeed())
})
It("should fail if both consolidation and TTLSecondsAfterEmpty are enabled", func() {
provisioner.Spec.TTLSecondsAfterEmpty = ptr.Int64(30)
provisioner.Spec.Consolidation = &Consolidation{Enabled: aws.Bool(true)}
Expect(provisioner.Validate(ctx)).ToNot(Succeed())
})
It("should succeed if consolidation is off and TTLSecondsAfterEmpty is set", func() {
provisioner.Spec.TTLSecondsAfterEmpty = ptr.Int64(30)
provisioner.Spec.Consolidation = &Consolidation{Enabled: aws.Bool(false)}
Expect(provisioner.Validate(ctx)).To(Succeed())
})
It("should succeed if consolidation is on and TTLSecondsAfterEmpty is not set", func() {
provisioner.Spec.TTLSecondsAfterEmpty = nil
provisioner.Spec.Consolidation = &Consolidation{Enabled: aws.Bool(true)}
Expect(provisioner.Validate(ctx)).To(Succeed())
})

Context("Limits", func() {
It("should allow undefined limits", func() {
Expand Down
25 changes: 25 additions & 0 deletions pkg/apis/provisioning/v1alpha5/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/cloudprovider/aws/cloudprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ func init() {
v1alpha5.NormalizedLabels = functional.UnionStringMaps(v1alpha5.NormalizedLabels, map[string]string{"topology.ebs.csi.aws.com/zone": v1.LabelTopologyZone})
}

var _ cloudprovider.CloudProvider = (*CloudProvider)(nil)

type CloudProvider struct {
instanceTypeProvider *InstanceTypeProvider
subnetProvider *SubnetProvider
Expand Down
Loading