Skip to content

Commit fb7d6af

Browse files
committed
feat: custom lifecyclehooks for machinepools
1 parent 1313226 commit fb7d6af

24 files changed

+938
-17
lines changed

config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,54 @@ spec:
839839
after it enters the InService state.
840840
If no value is supplied by user a default value of 300 seconds is set
841841
type: string
842+
lifecycleHooks:
843+
description: AWSLifecycleHooks specifies lifecycle hooks for the managed
844+
node group.
845+
items:
846+
description: AWSLifecycleHook describes an AWS lifecycle hook
847+
properties:
848+
defaultResult:
849+
description: The default result for the lifecycle hook. The
850+
possible values are CONTINUE and ABANDON.
851+
enum:
852+
- CONTINUE
853+
- ABANDON
854+
type: string
855+
heartbeatTimeout:
856+
description: |-
857+
The maximum time, in seconds, that an instance can remain in a Pending:Wait or
858+
Terminating:Wait state. The maximum is 172800 seconds (48 hours) or 100 times
859+
HeartbeatTimeout, whichever is smaller.
860+
format: duration
861+
type: string
862+
lifecycleTransition:
863+
description: The state of the EC2 instance to which to attach
864+
the lifecycle hook.
865+
enum:
866+
- autoscaling:EC2_INSTANCE_LAUNCHING
867+
- autoscaling:EC2_INSTANCE_TERMINATING
868+
type: string
869+
name:
870+
description: The name of the lifecycle hook.
871+
type: string
872+
notificationMetadata:
873+
description: Contains additional metadata that will be passed
874+
to the notification target.
875+
type: string
876+
notificationTargetARN:
877+
description: |-
878+
The ARN of the notification target that Amazon EC2 Auto Scaling uses to
879+
notify you when an instance is in the transition state for the lifecycle hook.
880+
type: string
881+
roleARN:
882+
description: |-
883+
The ARN of the IAM role that allows the Auto Scaling group to publish to the
884+
specified notification target.
885+
type: string
886+
required:
887+
- lifecycleTransition
888+
type: object
889+
type: array
842890
maxSize:
843891
default: 1
844892
description: MaxSize defines the maximum size of the group.

config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,54 @@ spec:
846846
type: string
847847
description: Labels specifies labels for the Kubernetes node objects
848848
type: object
849+
lifecycleHooks:
850+
description: AWSLifecycleHooks specifies lifecycle hooks for the managed
851+
node group.
852+
items:
853+
description: AWSLifecycleHook describes an AWS lifecycle hook
854+
properties:
855+
defaultResult:
856+
description: The default result for the lifecycle hook. The
857+
possible values are CONTINUE and ABANDON.
858+
enum:
859+
- CONTINUE
860+
- ABANDON
861+
type: string
862+
heartbeatTimeout:
863+
description: |-
864+
The maximum time, in seconds, that an instance can remain in a Pending:Wait or
865+
Terminating:Wait state. The maximum is 172800 seconds (48 hours) or 100 times
866+
HeartbeatTimeout, whichever is smaller.
867+
format: duration
868+
type: string
869+
lifecycleTransition:
870+
description: The state of the EC2 instance to which to attach
871+
the lifecycle hook.
872+
enum:
873+
- autoscaling:EC2_INSTANCE_LAUNCHING
874+
- autoscaling:EC2_INSTANCE_TERMINATING
875+
type: string
876+
name:
877+
description: The name of the lifecycle hook.
878+
type: string
879+
notificationMetadata:
880+
description: Contains additional metadata that will be passed
881+
to the notification target.
882+
type: string
883+
notificationTargetARN:
884+
description: |-
885+
The ARN of the notification target that Amazon EC2 Auto Scaling uses to
886+
notify you when an instance is in the transition state for the lifecycle hook.
887+
type: string
888+
roleARN:
889+
description: |-
890+
The ARN of the IAM role that allows the Auto Scaling group to publish to the
891+
specified notification target.
892+
type: string
893+
required:
894+
- lifecycleTransition
895+
type: object
896+
type: array
849897
providerIDList:
850898
description: |-
851899
ProviderIDList are the provider IDs of instances in the

exp/api/v1beta1/conversion.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
5050
if restored.Spec.AvailabilityZoneSubnetType != nil {
5151
dst.Spec.AvailabilityZoneSubnetType = restored.Spec.AvailabilityZoneSubnetType
5252
}
53+
if restored.Spec.AWSLifecycleHooks != nil {
54+
dst.Spec.AWSLifecycleHooks = restored.Spec.AWSLifecycleHooks
55+
}
5356

5457
if restored.Spec.AWSLaunchTemplate.PrivateDNSName != nil {
5558
dst.Spec.AWSLaunchTemplate.PrivateDNSName = restored.Spec.AWSLaunchTemplate.PrivateDNSName
@@ -109,6 +112,9 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
109112
if restored.Spec.AvailabilityZoneSubnetType != nil {
110113
dst.Spec.AvailabilityZoneSubnetType = restored.Spec.AvailabilityZoneSubnetType
111114
}
115+
if restored.Spec.AWSLifecycleHooks != nil {
116+
dst.Spec.AWSLifecycleHooks = restored.Spec.AWSLifecycleHooks
117+
}
112118

113119
return nil
114120
}

exp/api/v1beta1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exp/api/v1beta2/awsmachinepool_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ type AWSMachinePoolSpec struct {
101101
// SuspendProcesses defines a list of processes to suspend for the given ASG. This is constantly reconciled.
102102
// If a process is removed from this list it will automatically be resumed.
103103
SuspendProcesses *SuspendProcessesTypes `json:"suspendProcesses,omitempty"`
104+
105+
// AWSLifecycleHooks specifies lifecycle hooks for the managed node group.
106+
// +optional
107+
AWSLifecycleHooks []AWSLifecycleHook `json:"lifecycleHooks,omitempty"`
104108
}
105109

106110
// SuspendProcessesTypes contains user friendly auto-completable values for suspended process names.

exp/api/v1beta2/awsmachinepool_webhook.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ func (r *AWSMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error {
4242
// +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
4343
// +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
4444

45-
var _ webhook.Defaulter = &AWSMachinePool{}
46-
var _ webhook.Validator = &AWSMachinePool{}
45+
var (
46+
_ webhook.Defaulter = &AWSMachinePool{}
47+
_ webhook.Validator = &AWSMachinePool{}
48+
)
4749

4850
func (r *AWSMachinePool) validateDefaultCoolDown() field.ErrorList {
4951
var allErrs field.ErrorList
@@ -108,6 +110,7 @@ func (r *AWSMachinePool) validateAdditionalSecurityGroups() field.ErrorList {
108110
}
109111
return allErrs
110112
}
113+
111114
func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
112115
var allErrs field.ErrorList
113116
if r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil && r.Spec.MixedInstancesPolicy != nil {
@@ -116,6 +119,37 @@ func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
116119
return allErrs
117120
}
118121

122+
func (r *AWSMachinePool) validateLifecycleHooks() field.ErrorList {
123+
return validateLifecycleHooks(r.Spec.AWSLifecycleHooks)
124+
}
125+
126+
func validateLifecycleHooks(hooks []AWSLifecycleHook) field.ErrorList {
127+
var allErrs field.ErrorList
128+
129+
for _, hook := range hooks {
130+
if hook.Name == "" {
131+
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.name"), "Name is required"))
132+
}
133+
if hook.NotificationTargetARN != nil && hook.RoleARN == nil {
134+
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.roleARN"), "RoleARN is required if NotificationTargetARN is provided"))
135+
}
136+
if hook.RoleARN != nil && hook.NotificationTargetARN == nil {
137+
allErrs = append(allErrs, field.Required(field.NewPath("spec.lifecycleHooks.notificationTargetARN"), "NotificationTargetARN is required if RoleARN is provided"))
138+
}
139+
if hook.LifecycleTransition != LifecycleTransitionInstanceLaunch && hook.LifecycleTransition != LifecycleTransitionInstanceTerminate {
140+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.lifecycleTransition"), hook.LifecycleTransition, "LifecycleTransition must be either EC2_INSTANCE_LAUNCHING or EC2_INSTANCE_TERMINATING"))
141+
}
142+
if hook.DefaultResult != nil && (*hook.DefaultResult != DefaultResultContinue && *hook.DefaultResult != DefaultResultAbandon) {
143+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.defaultResult"), *hook.DefaultResult, "DefaultResult must be either CONTINUE or ABANDON"))
144+
}
145+
if hook.HeartbeatTimeout != nil && (hook.HeartbeatTimeout.Seconds() < float64(30) || hook.HeartbeatTimeout.Seconds() > float64(172800)) {
146+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.lifecycleHooks.heartbeatTimeout"), *hook.HeartbeatTimeout, "HeartbeatTimeout must be between 30 and 172800 seconds"))
147+
}
148+
}
149+
150+
return allErrs
151+
}
152+
119153
// ValidateCreate will do any extra validation when creating a AWSMachinePool.
120154
func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
121155
log.Info("AWSMachinePool validate create", "machine-pool", klog.KObj(r))
@@ -128,6 +162,7 @@ func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
128162
allErrs = append(allErrs, r.validateSubnets()...)
129163
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
130164
allErrs = append(allErrs, r.validateSpotInstances()...)
165+
allErrs = append(allErrs, r.validateLifecycleHooks()...)
131166

132167
if len(allErrs) == 0 {
133168
return nil, nil
@@ -149,6 +184,7 @@ func (r *AWSMachinePool) ValidateUpdate(_ runtime.Object) (admission.Warnings, e
149184
allErrs = append(allErrs, r.validateSubnets()...)
150185
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
151186
allErrs = append(allErrs, r.validateSpotInstances()...)
187+
allErrs = append(allErrs, r.validateLifecycleHooks()...)
152188

153189
if len(allErrs) == 0 {
154190
return nil, nil

exp/api/v1beta2/awsmachinepool_webhook_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package v1beta2
1919
import (
2020
"strings"
2121
"testing"
22+
"time"
2223

2324
"github.com/aws/aws-sdk-go/aws"
2425
. "github.com/onsi/gomega"
@@ -153,6 +154,33 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) {
153154
},
154155
wantErr: true,
155156
},
157+
{
158+
name: "Should fail if either roleARN or notifcationARN is set but not both",
159+
pool: &AWSMachinePool{
160+
Spec: AWSMachinePoolSpec{
161+
AWSLifecycleHooks: []AWSLifecycleHook{
162+
{
163+
RoleARN: aws.String("role-arn"),
164+
},
165+
},
166+
},
167+
},
168+
wantErr: true,
169+
},
170+
{
171+
name: "Should fail if the heartbeat timeout is less than 30 seconds",
172+
pool: &AWSMachinePool{
173+
Spec: AWSMachinePoolSpec{
174+
AWSLifecycleHooks: []AWSLifecycleHook{
175+
{
176+
RoleARN: aws.String("role-arn"),
177+
HeartbeatTimeout: &metav1.Duration{Duration: 29 * time.Second},
178+
},
179+
},
180+
},
181+
},
182+
wantErr: true,
183+
},
156184
}
157185
for _, tt := range tests {
158186
t.Run(tt.name, func(t *testing.T) {

exp/api/v1beta2/awsmanagedmachinepool_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ type AWSManagedMachinePoolSpec struct {
159159
// are prohibited (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html).
160160
// +optional
161161
AWSLaunchTemplate *AWSLaunchTemplate `json:"awsLaunchTemplate,omitempty"`
162+
163+
// AWSLifecycleHooks specifies lifecycle hooks for the managed node group.
164+
// +optional
165+
AWSLifecycleHooks []AWSLifecycleHook `json:"lifecycleHooks,omitempty"`
162166
}
163167

164168
// ManagedMachinePoolScaling specifies scaling options.

exp/api/v1beta2/awsmanagedmachinepool_webhook.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ func (r *AWSManagedMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error
5151
// +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
5252
// +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
5353

54-
var _ webhook.Defaulter = &AWSManagedMachinePool{}
55-
var _ webhook.Validator = &AWSManagedMachinePool{}
54+
var (
55+
_ webhook.Defaulter = &AWSManagedMachinePool{}
56+
_ webhook.Validator = &AWSManagedMachinePool{}
57+
)
5658

5759
func (r *AWSManagedMachinePool) validateScaling() field.ErrorList {
5860
var allErrs field.ErrorList
@@ -138,6 +140,10 @@ func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList {
138140
return allErrs
139141
}
140142

143+
func (r *AWSManagedMachinePool) validateLifecycleHooks() field.ErrorList {
144+
return validateLifecycleHooks(r.Spec.AWSLifecycleHooks)
145+
}
146+
141147
// ValidateCreate will do any extra validation when creating a AWSManagedMachinePool.
142148
func (r *AWSManagedMachinePool) ValidateCreate() (admission.Warnings, error) {
143149
mmpLog.Info("AWSManagedMachinePool validate create", "managed-machine-pool", klog.KObj(r))
@@ -159,6 +165,9 @@ func (r *AWSManagedMachinePool) ValidateCreate() (admission.Warnings, error) {
159165
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
160166
allErrs = append(allErrs, errs...)
161167
}
168+
if errs := r.validateLifecycleHooks(); len(errs) > 0 {
169+
allErrs = append(allErrs, errs...)
170+
}
162171

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

@@ -196,6 +205,9 @@ func (r *AWSManagedMachinePool) ValidateUpdate(old runtime.Object) (admission.Wa
196205
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
197206
allErrs = append(allErrs, errs...)
198207
}
208+
if errs := r.validateLifecycleHooks(); len(errs) > 0 {
209+
allErrs = append(allErrs, errs...)
210+
}
199211

200212
if len(allErrs) == 0 {
201213
return nil, nil

exp/api/v1beta2/conditions_consts.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ const (
5454
InstanceRefreshNotReadyReason = "InstanceRefreshNotReady"
5555
// InstanceRefreshFailedReason used to report when there instance refresh is not initiated.
5656
InstanceRefreshFailedReason = "InstanceRefreshFailed"
57+
58+
// LifecycleHookReadyCondition reports on the status of the lifecycle hook.
59+
LifecycleHookReadyCondition clusterv1.ConditionType = "LifecycleHookReady"
60+
// LifecycleHookNotFoundReason used when the lifecycle hook couldn't be retrieved.
61+
LifecycleHookNotFoundReason = "LifecycleHookNotFound"
62+
// LifecycleHookExistsCondition reports on the existence of the lifecycle hook.
63+
LifecycleHookExistsCondition clusterv1.ConditionType = "LifecycleHookExists"
64+
// LifecycleHookCreationFailedReason used for failures during lifecycle hook creation.
65+
LifecycleHookCreationFailedReason = "LifecycleHookCreationFailed"
66+
// LifecycleHookUpdateFailedReason used for failures during lifecycle hook update.
67+
LifecycleHookUpdateFailedReason = "LifecycleHookUpdateFailed"
68+
// LifecycleHookDeletionFailedReason used for failures during lifecycle hook deletion.
69+
LifecycleHookDeletionFailedReason = "LifecycleHookDeletionFailed"
5770
)
5871

5972
const (

0 commit comments

Comments
 (0)