@@ -51,20 +51,35 @@ import (
5151)
5252
5353const (
54- requeueAfterTime = 10 * time .Minute
55- ConditionReasonCreateFleetAuthFailed = "CreateFleetAuthCheckFailed"
56- ConditionReasonCreateLaunchTemplateAuthFailed = "CreateLaunchTemplateAuthCheckFailed"
57- ConditionReasonRunInstancesAuthFailed = "RunInstancesAuthCheckFailed"
58- ConditionReasonInstanceProfileNotFound = "InstanceProfileNotFound"
59- ConditionReasonDependenciesNotReady = "DependenciesNotReady"
60- ConditionReasonTagValidationFailed = "TagValidationFailed"
61- ConditionReasonDryRunDisabled = "DryRunDisabled"
54+ requeueAfterTime = 10 * time .Minute
55+ ConditionReasonCreateFleetAuthFailed = "CreateFleetAuthCheckFailed"
56+ ConditionReasonCreateLaunchTemplateAuthFailed = "CreateLaunchTemplateAuthCheckFailed"
57+ ConditionReasonRunInstancesAuthFailed = "RunInstancesAuthCheckFailed"
58+ ConditionReasonCreateFleetValidationFailed = "CreateFleetValidationFailed"
59+ ConditionReasonCreateLaunchTemplateValidationFailed = "CreateLaunchTemplateValidationFailed"
60+ ConditionReasonRunInstancesValidationFailed = "RunInstancesValidationFailed"
61+ ConditionReasonInstanceProfileNotFound = "InstanceProfileNotFound"
62+ ConditionReasonDependenciesNotReady = "DependenciesNotReady"
63+ ConditionReasonTagValidationFailed = "TagValidationFailed"
64+ ConditionReasonDryRunDisabled = "DryRunDisabled"
6265)
6366
6467var ValidationConditionMessages = map [string ]string {
65- ConditionReasonCreateFleetAuthFailed : "Controller isn't authorized to call ec2:CreateFleet" ,
66- ConditionReasonCreateLaunchTemplateAuthFailed : "Controller isn't authorized to call ec2:CreateLaunchTemplate" ,
67- ConditionReasonRunInstancesAuthFailed : "Controller isn't authorized to call ec2:RunInstances" ,
68+ ConditionReasonCreateFleetAuthFailed : "Controller isn't authorized to call ec2:CreateFleet" ,
69+ ConditionReasonCreateLaunchTemplateAuthFailed : "Controller isn't authorized to call ec2:CreateLaunchTemplate" ,
70+ ConditionReasonRunInstancesAuthFailed : "Controller isn't authorized to call ec2:RunInstances" ,
71+ ConditionReasonCreateFleetValidationFailed : "EC2 rejected the ec2:CreateFleet dry run" ,
72+ ConditionReasonCreateLaunchTemplateValidationFailed : "EC2 rejected the ec2:CreateLaunchTemplate request" ,
73+ ConditionReasonRunInstancesValidationFailed : "EC2 rejected the ec2:RunInstances dry run" ,
74+ }
75+
76+ // isTransientError returns true for errors that a retry can resolve without a change to the
77+ // EC2NodeClass, so validation should requeue rather than record a failure against the spec.
78+ func isTransientError (err error ) bool {
79+ return awserrors .IsRateLimitedError (err ) ||
80+ awserrors .IsServerError (err ) ||
81+ awserrors .IsNonTerminalError (err ) ||
82+ awserrors .IsInstanceProfileNotFound (err )
6883}
6984
7085// validationCacheEntry stores a failed validation result with both the condition reason and the
@@ -239,11 +254,17 @@ func (v *Validation) validateCreateLaunchTemplateAuthorization(
239254
240255 launchTemplates , err := v .launchTemplateProvider .EnsureAll (ctx , nodeClass , nodeClaim , instanceTypes [:1 ], karpv1 .CapacityTypeOnDemand , tags , string (tenancyType ))
241256 if err != nil {
242- if awserrors . IsRateLimitedError ( err ) || awserrors . IsServerError (err ) {
257+ if isTransientError (err ) {
243258 return nil , reconcile.Result {Requeue : true }, nil
244259 }
245260 if awserrors .IgnoreUnauthorizedOperationError (err ) != nil {
246- // We should only ever receive UnauthorizedOperation so if we receive any other error it would be an unexpected state
261+ // EC2 also rejects requests it considers malformed, which retrying can't resolve until the
262+ // EC2NodeClass itself changes. Surface those on the status condition rather than returning an
263+ // error that is only ever logged, and logged as if it were an authorization failure.
264+ if message , ok := awserrors .ToAPIErrorMessage (err ); ok {
265+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonCreateLaunchTemplateValidationFailed , message )
266+ return nil , reconcile.Result {RequeueAfter : requeueAfterTime }, nil
267+ }
247268 return nil , reconcile.Result {}, fmt .Errorf ("validating ec2:CreateLaunchTemplate authorization, %w" , err )
248269 }
249270 log .FromContext (ctx ).Error (err , "unauthorized to call ec2:CreateLaunchTemplate" )
@@ -271,12 +292,17 @@ func (v *Validation) validateCreateFleetAuthorization(
271292 if _ , err := v .ec2api .CreateFleet (ctx , createFleetInput , func (o * ec2.Options ) {
272293 o .Retryer = aws.NopRetryer {}
273294 }); awserrors .IgnoreDryRunError (err ) != nil {
274- if awserrors . IsRateLimitedError ( err ) || awserrors . IsServerError (err ) {
295+ if isTransientError (err ) {
275296 return reconcile.Result {Requeue : true }, nil
276297 }
277298 if awserrors .IgnoreUnauthorizedOperationError (err ) != nil {
278- // Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
279- // it would be an unexpected state
299+ // A dry run also fails when EC2 considers the request itself invalid, which retrying can't
300+ // resolve until the EC2NodeClass changes. Surface those on the status condition rather than
301+ // returning an error that is only ever logged, and logged as if it were an authorization failure.
302+ if message , ok := awserrors .ToAPIErrorMessage (err ); ok {
303+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonCreateFleetValidationFailed , message )
304+ return reconcile.Result {RequeueAfter : requeueAfterTime }, nil
305+ }
280306 return reconcile.Result {}, fmt .Errorf ("validating ec2:CreateFleet authorization, %w" , err )
281307 }
282308 log .FromContext (ctx ).Error (err , "unauthorized to call ec2:CreateFleet" )
@@ -326,12 +352,18 @@ func (v *Validation) validateRunInstancesAuthorization(
326352 // this means there is most likely an eventual consistency issue and we just need to requeue
327353 return reconcile.Result {Requeue : true }, nil
328354 }
329- if awserrors . IsRateLimitedError ( firstSubnetErr ) || awserrors . IsServerError (firstSubnetErr ) {
355+ if isTransientError (firstSubnetErr ) {
330356 return reconcile.Result {Requeue : true }, nil
331357 }
332358 if awserrors .IgnoreUnauthorizedOperationError (firstSubnetErr ) != nil {
333- // Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
334- // it would be an unexpected state
359+ // A dry run also fails when EC2 considers the request itself invalid, e.g. a block device mapping
360+ // whose volume is smaller than the AMI's snapshot. Retrying can't resolve that until the
361+ // EC2NodeClass changes, so surface it on the status condition rather than returning an error that
362+ // is only ever logged, and logged as if it were an authorization failure.
363+ if message , ok := awserrors .ToAPIErrorMessage (firstSubnetErr ); ok {
364+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonRunInstancesValidationFailed , message )
365+ return reconcile.Result {RequeueAfter : requeueAfterTime }, nil
366+ }
335367 return reconcile.Result {}, fmt .Errorf ("validating ec2:RunInstances authorization, %w" , firstSubnetErr )
336368 }
337369 log .FromContext (ctx ).Error (firstSubnetErr , "unauthorized to call ec2:RunInstances" )
0 commit comments