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(awsmachinepool): custom lifecyclehooks for machinepools #4875

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,54 @@ spec:
after it enters the InService state.
If no value is supplied by user a default value of 300 seconds is set
type: string
lifecycleHooks:
description: AWSLifecycleHooks specifies lifecycle hooks for the managed
node group.
items:
description: AWSLifecycleHook describes an AWS lifecycle hook
properties:
defaultResult:
description: The default result for the lifecycle hook. The
possible values are CONTINUE and ABANDON.
enum:
- CONTINUE
- ABANDON
type: string
heartbeatTimeout:
description: |-
The maximum time, in seconds, that an instance can remain in a Pending:Wait or
Terminating:Wait state. The maximum is 172800 seconds (48 hours) or 100 times
HeartbeatTimeout, whichever is smaller.
format: duration
type: string
lifecycleTransition:
description: The state of the EC2 instance to which to attach
the lifecycle hook.
enum:
- autoscaling:EC2_INSTANCE_LAUNCHING
- autoscaling:EC2_INSTANCE_TERMINATING
type: string
name:
description: The name of the lifecycle hook.
type: string
notificationMetadata:
description: Contains additional metadata that will be passed
to the notification target.
type: string
notificationTargetARN:
description: |-
The ARN of the notification target that Amazon EC2 Auto Scaling uses to
notify you when an instance is in the transition state for the lifecycle hook.
type: string
roleARN:
description: |-
The ARN of the IAM role that allows the Auto Scaling group to publish to the
specified notification target.
type: string
required:
- lifecycleTransition
type: object
type: array
maxSize:
default: 1
description: MaxSize defines the maximum size of the group.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,54 @@ spec:
type: string
description: Labels specifies labels for the Kubernetes node objects
type: object
lifecycleHooks:
description: AWSLifecycleHooks specifies lifecycle hooks for the managed
node group.
items:
description: AWSLifecycleHook describes an AWS lifecycle hook
properties:
defaultResult:
description: The default result for the lifecycle hook. The
possible values are CONTINUE and ABANDON.
enum:
- CONTINUE
- ABANDON
type: string
heartbeatTimeout:
description: |-
The maximum time, in seconds, that an instance can remain in a Pending:Wait or
Terminating:Wait state. The maximum is 172800 seconds (48 hours) or 100 times
HeartbeatTimeout, whichever is smaller.
format: duration
type: string
lifecycleTransition:
description: The state of the EC2 instance to which to attach
the lifecycle hook.
enum:
- autoscaling:EC2_INSTANCE_LAUNCHING
- autoscaling:EC2_INSTANCE_TERMINATING
type: string
name:
description: The name of the lifecycle hook.
type: string
notificationMetadata:
description: Contains additional metadata that will be passed
to the notification target.
type: string
notificationTargetARN:
description: |-
The ARN of the notification target that Amazon EC2 Auto Scaling uses to
notify you when an instance is in the transition state for the lifecycle hook.
type: string
roleARN:
description: |-
The ARN of the IAM role that allows the Auto Scaling group to publish to the
specified notification target.
type: string
required:
- lifecycleTransition
type: object
type: array
providerIDList:
description: |-
ProviderIDList are the provider IDs of instances in the
Expand Down
6 changes: 6 additions & 0 deletions exp/api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
if restored.Spec.AvailabilityZoneSubnetType != nil {
dst.Spec.AvailabilityZoneSubnetType = restored.Spec.AvailabilityZoneSubnetType
}
if restored.Spec.AWSLifecycleHooks != nil {
dst.Spec.AWSLifecycleHooks = restored.Spec.AWSLifecycleHooks
}

if restored.Spec.AWSLaunchTemplate.PrivateDNSName != nil {
dst.Spec.AWSLaunchTemplate.PrivateDNSName = restored.Spec.AWSLaunchTemplate.PrivateDNSName
Expand Down Expand Up @@ -109,6 +112,9 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
if restored.Spec.AvailabilityZoneSubnetType != nil {
dst.Spec.AvailabilityZoneSubnetType = restored.Spec.AvailabilityZoneSubnetType
}
if restored.Spec.AWSLifecycleHooks != nil {
dst.Spec.AWSLifecycleHooks = restored.Spec.AWSLifecycleHooks
}

return nil
}
Expand Down
2 changes: 2 additions & 0 deletions exp/api/v1beta1/zz_generated.conversion.go

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

4 changes: 4 additions & 0 deletions exp/api/v1beta2/awsmachinepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ type AWSMachinePoolSpec struct {
// SuspendProcesses defines a list of processes to suspend for the given ASG. This is constantly reconciled.
// If a process is removed from this list it will automatically be resumed.
SuspendProcesses *SuspendProcessesTypes `json:"suspendProcesses,omitempty"`

// AWSLifecycleHooks specifies lifecycle hooks for the managed node group.
// +optional
AWSLifecycleHooks []AWSLifecycleHook `json:"lifecycleHooks,omitempty"`
}

// SuspendProcessesTypes contains user friendly auto-completable values for suspended process names.
Expand Down
40 changes: 38 additions & 2 deletions exp/api/v1beta2/awsmachinepool_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ func (r *AWSMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error {
// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2-awsmachinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmachinepools,versions=v1beta2,name=validation.awsmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta2-awsmachinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmachinepools,versions=v1beta2,name=default.awsmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1

var _ webhook.Defaulter = &AWSMachinePool{}
var _ webhook.Validator = &AWSMachinePool{}
var (
_ webhook.Defaulter = &AWSMachinePool{}
_ webhook.Validator = &AWSMachinePool{}
)

func (r *AWSMachinePool) validateDefaultCoolDown() field.ErrorList {
var allErrs field.ErrorList
Expand Down Expand Up @@ -108,6 +110,7 @@ func (r *AWSMachinePool) validateAdditionalSecurityGroups() field.ErrorList {
}
return allErrs
}

func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
var allErrs field.ErrorList
if r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil && r.Spec.MixedInstancesPolicy != nil {
Expand All @@ -116,6 +119,37 @@ func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
return allErrs
}

func (r *AWSMachinePool) validateLifecycleHooks() field.ErrorList {
return validateLifecycleHooks(r.Spec.AWSLifecycleHooks)
}

func validateLifecycleHooks(hooks []AWSLifecycleHook) field.ErrorList {
var allErrs field.ErrorList

for _, hook := range hooks {
if hook.Name == "" {
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.name"), "Name is required"))
}
if hook.NotificationTargetARN != nil && hook.RoleARN == nil {
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.roleARN"), "RoleARN is required if NotificationTargetARN is provided"))
}
if hook.RoleARN != nil && hook.NotificationTargetARN == nil {
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.notificationTargetARN"), "NotificationTargetARN is required if RoleARN is provided"))
}
if hook.LifecycleTransition != LifecycleTransitionInstanceLaunch && hook.LifecycleTransition != LifecycleTransitionInstanceTerminate {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.lifecycleTransition"), hook.LifecycleTransition, "LifecycleTransition must be either EC2_INSTANCE_LAUNCHING or EC2_INSTANCE_TERMINATING"))
}
if hook.DefaultResult != nil && (*hook.DefaultResult != DefaultResultContinue && *hook.DefaultResult != DefaultResultAbandon) {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.defaultResult"), *hook.DefaultResult, "DefaultResult must be either CONTINUE or ABANDON"))
}
if hook.HeartbeatTimeout != nil && (hook.HeartbeatTimeout.Seconds() < float64(30) || hook.HeartbeatTimeout.Seconds() > float64(172800)) {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.heartbeatTimeout"), *hook.HeartbeatTimeout, "HeartbeatTimeout must be between 30 and 172800 seconds"))
}
}

return allErrs
}

// ValidateCreate will do any extra validation when creating a AWSMachinePool.
func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
log.Info("AWSMachinePool validate create", "machine-pool", klog.KObj(r))
Expand All @@ -128,6 +162,7 @@ func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
allErrs = append(allErrs, r.validateSubnets()...)
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
allErrs = append(allErrs, r.validateSpotInstances()...)
allErrs = append(allErrs, r.validateLifecycleHooks()...)

if len(allErrs) == 0 {
return nil, nil
Expand All @@ -149,6 +184,7 @@ func (r *AWSMachinePool) ValidateUpdate(_ runtime.Object) (admission.Warnings, e
allErrs = append(allErrs, r.validateSubnets()...)
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
allErrs = append(allErrs, r.validateSpotInstances()...)
allErrs = append(allErrs, r.validateLifecycleHooks()...)

if len(allErrs) == 0 {
return nil, nil
Expand Down
28 changes: 28 additions & 0 deletions exp/api/v1beta2/awsmachinepool_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1beta2
import (
"strings"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -153,6 +154,33 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) {
},
wantErr: true,
},
{
name: "Should fail if either roleARN or notifcationARN is set but not both",
pool: &AWSMachinePool{
Spec: AWSMachinePoolSpec{
AWSLifecycleHooks: []AWSLifecycleHook{
{
RoleARN: aws.String("role-arn"),
},
},
},
},
wantErr: true,
},
{
name: "Should fail if the heartbeat timeout is less than 30 seconds",
pool: &AWSMachinePool{
Spec: AWSMachinePoolSpec{
AWSLifecycleHooks: []AWSLifecycleHook{
{
RoleARN: aws.String("role-arn"),
HeartbeatTimeout: &metav1.Duration{Duration: 29 * time.Second},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions exp/api/v1beta2/awsmanagedmachinepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ type AWSManagedMachinePoolSpec struct {
// are prohibited (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html).
// +optional
AWSLaunchTemplate *AWSLaunchTemplate `json:"awsLaunchTemplate,omitempty"`

// AWSLifecycleHooks specifies lifecycle hooks for the managed node group.
// +optional
AWSLifecycleHooks []AWSLifecycleHook `json:"lifecycleHooks,omitempty"`
}

// ManagedMachinePoolScaling specifies scaling options.
Expand Down
16 changes: 14 additions & 2 deletions exp/api/v1beta2/awsmanagedmachinepool_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ func (r *AWSManagedMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error
// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2-awsmanagedmachinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepools,versions=v1beta2,name=validation.awsmanagedmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta2-awsmanagedmachinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepools,versions=v1beta2,name=default.awsmanagedmachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1

var _ webhook.Defaulter = &AWSManagedMachinePool{}
var _ webhook.Validator = &AWSManagedMachinePool{}
var (
_ webhook.Defaulter = &AWSManagedMachinePool{}
_ webhook.Validator = &AWSManagedMachinePool{}
)

func (r *AWSManagedMachinePool) validateScaling() field.ErrorList {
var allErrs field.ErrorList
Expand Down Expand Up @@ -138,6 +140,10 @@ func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList {
return allErrs
}

func (r *AWSManagedMachinePool) validateLifecycleHooks() field.ErrorList {
return validateLifecycleHooks(r.Spec.AWSLifecycleHooks)
}

// ValidateCreate will do any extra validation when creating a AWSManagedMachinePool.
func (r *AWSManagedMachinePool) ValidateCreate() (admission.Warnings, error) {
mmpLog.Info("AWSManagedMachinePool validate create", "managed-machine-pool", klog.KObj(r))
Expand All @@ -159,6 +165,9 @@ func (r *AWSManagedMachinePool) ValidateCreate() (admission.Warnings, error) {
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateLifecycleHooks(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}

allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)

Expand Down Expand Up @@ -196,6 +205,9 @@ func (r *AWSManagedMachinePool) ValidateUpdate(old runtime.Object) (admission.Wa
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateLifecycleHooks(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}

if len(allErrs) == 0 {
return nil, nil
Expand Down
13 changes: 13 additions & 0 deletions exp/api/v1beta2/conditions_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ const (
InstanceRefreshNotReadyReason = "InstanceRefreshNotReady"
// InstanceRefreshFailedReason used to report when there instance refresh is not initiated.
InstanceRefreshFailedReason = "InstanceRefreshFailed"

// LifecycleHookReadyCondition reports on the status of the lifecycle hook.
LifecycleHookReadyCondition clusterv1.ConditionType = "LifecycleHookReady"
// LifecycleHookNotFoundReason used when the lifecycle hook couldn't be retrieved.
LifecycleHookNotFoundReason = "LifecycleHookNotFound"
// LifecycleHookExistsCondition reports on the existence of the lifecycle hook.
LifecycleHookExistsCondition clusterv1.ConditionType = "LifecycleHookExists"
// LifecycleHookCreationFailedReason used for failures during lifecycle hook creation.
LifecycleHookCreationFailedReason = "LifecycleHookCreationFailed"
// LifecycleHookUpdateFailedReason used for failures during lifecycle hook update.
LifecycleHookUpdateFailedReason = "LifecycleHookUpdateFailed"
// LifecycleHookDeletionFailedReason used for failures during lifecycle hook deletion.
LifecycleHookDeletionFailedReason = "LifecycleHookDeletionFailed"
)

const (
Expand Down
Loading