@@ -56,23 +56,38 @@ import (
5656)
5757
5858const (
59- requeueAfterTime = 10 * time .Minute
60- ConditionReasonCreateFleetAuthFailed = "CreateFleetAuthCheckFailed"
61- ConditionReasonCreateLaunchTemplateAuthFailed = "CreateLaunchTemplateAuthCheckFailed"
62- ConditionReasonRunInstancesAuthFailed = "RunInstancesAuthCheckFailed"
63- ConditionReasonInstanceProfileNotFound = "InstanceProfileNotFound"
64- ConditionReasonDependenciesNotReady = "DependenciesNotReady"
65- ConditionReasonTagValidationFailed = "TagValidationFailed"
66- ConditionReasonKubeletExpressionInvalid = "KubeletExpressionInvalid"
67- ConditionReasonKubeletExpressionEvalFailed = "KubeletExpressionEvaluationFailed"
68- ConditionReasonKubeletExpressionsDisabled = "KubeletExpressionsDisabled"
69- ConditionReasonDryRunDisabled = "DryRunDisabled"
59+ requeueAfterTime = 10 * time .Minute
60+ ConditionReasonCreateFleetAuthFailed = "CreateFleetAuthCheckFailed"
61+ ConditionReasonCreateLaunchTemplateAuthFailed = "CreateLaunchTemplateAuthCheckFailed"
62+ ConditionReasonRunInstancesAuthFailed = "RunInstancesAuthCheckFailed"
63+ ConditionReasonCreateFleetValidationFailed = "CreateFleetValidationFailed"
64+ ConditionReasonCreateLaunchTemplateValidationFailed = "CreateLaunchTemplateValidationFailed"
65+ ConditionReasonRunInstancesValidationFailed = "RunInstancesValidationFailed"
66+ ConditionReasonInstanceProfileNotFound = "InstanceProfileNotFound"
67+ ConditionReasonDependenciesNotReady = "DependenciesNotReady"
68+ ConditionReasonTagValidationFailed = "TagValidationFailed"
69+ ConditionReasonKubeletExpressionInvalid = "KubeletExpressionInvalid"
70+ ConditionReasonKubeletExpressionEvalFailed = "KubeletExpressionEvaluationFailed"
71+ ConditionReasonKubeletExpressionsDisabled = "KubeletExpressionsDisabled"
72+ ConditionReasonDryRunDisabled = "DryRunDisabled"
7073)
7174
7275var ValidationConditionMessages = map [string ]string {
73- ConditionReasonCreateFleetAuthFailed : "Controller isn't authorized to call ec2:CreateFleet" ,
74- ConditionReasonCreateLaunchTemplateAuthFailed : "Controller isn't authorized to call ec2:CreateLaunchTemplate" ,
75- ConditionReasonRunInstancesAuthFailed : "Controller isn't authorized to call ec2:RunInstances" ,
76+ ConditionReasonCreateFleetAuthFailed : "Controller isn't authorized to call ec2:CreateFleet" ,
77+ ConditionReasonCreateLaunchTemplateAuthFailed : "Controller isn't authorized to call ec2:CreateLaunchTemplate" ,
78+ ConditionReasonRunInstancesAuthFailed : "Controller isn't authorized to call ec2:RunInstances" ,
79+ ConditionReasonCreateFleetValidationFailed : "EC2 rejected the ec2:CreateFleet dry run" ,
80+ ConditionReasonCreateLaunchTemplateValidationFailed : "EC2 rejected the ec2:CreateLaunchTemplate request" ,
81+ ConditionReasonRunInstancesValidationFailed : "EC2 rejected the ec2:RunInstances dry run" ,
82+ }
83+
84+ // isTransientError returns true for errors that a retry can resolve without a change to the
85+ // EC2NodeClass, so validation should requeue rather than record a failure against the spec.
86+ func isTransientError (err error ) bool {
87+ return awserrors .IsRateLimitedError (err ) ||
88+ awserrors .IsServerError (err ) ||
89+ awserrors .IsNonTerminalError (err ) ||
90+ awserrors .IsInstanceProfileNotFound (err )
7691}
7792
7893// validationCacheEntry stores a failed validation result with both the condition reason and the
@@ -289,11 +304,17 @@ func (v *Validation) validateCreateLaunchTemplateAuthorization(
289304
290305 launchTemplates , err := v .launchTemplateProvider .EnsureAll (ctx , nodeClass , nodeClaim , instanceTypes [:1 ], karpv1 .CapacityTypeOnDemand , tags , string (tenancyType ))
291306 if err != nil {
292- if awserrors . IsRateLimitedError ( err ) || awserrors . IsServerError (err ) {
307+ if isTransientError (err ) {
293308 return nil , reconcile.Result {Requeue : true }, nil
294309 }
295310 if awserrors .IgnoreUnauthorizedOperationError (err ) != nil {
296- // We should only ever receive UnauthorizedOperation so if we receive any other error it would be an unexpected state
311+ // EC2 also rejects requests it considers malformed, which retrying can't resolve until the
312+ // EC2NodeClass itself changes. Surface those on the status condition rather than returning an
313+ // error that is only ever logged, and logged as if it were an authorization failure.
314+ if message , ok := awserrors .ToAPIErrorMessage (err ); ok {
315+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonCreateLaunchTemplateValidationFailed , message )
316+ return nil , reconcile.Result {RequeueAfter : requeueAfterTime }, nil
317+ }
297318 return nil , reconcile.Result {}, fmt .Errorf ("validating ec2:CreateLaunchTemplate authorization, %w" , err )
298319 }
299320 log .FromContext (ctx ).Error (err , "unauthorized to call ec2:CreateLaunchTemplate" )
@@ -321,12 +342,17 @@ func (v *Validation) validateCreateFleetAuthorization(
321342 if _ , err := v .ec2api .CreateFleet (ctx , createFleetInput , func (o * ec2.Options ) {
322343 o .Retryer = aws.NopRetryer {}
323344 }); awserrors .IgnoreDryRunError (err ) != nil {
324- if awserrors . IsRateLimitedError ( err ) || awserrors . IsServerError (err ) {
345+ if isTransientError (err ) {
325346 return reconcile.Result {Requeue : true }, nil
326347 }
327348 if awserrors .IgnoreUnauthorizedOperationError (err ) != nil {
328- // Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
329- // it would be an unexpected state
349+ // A dry run also fails when EC2 considers the request itself invalid, which retrying can't
350+ // resolve until the EC2NodeClass changes. Surface those on the status condition rather than
351+ // returning an error that is only ever logged, and logged as if it were an authorization failure.
352+ if message , ok := awserrors .ToAPIErrorMessage (err ); ok {
353+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonCreateFleetValidationFailed , message )
354+ return reconcile.Result {RequeueAfter : requeueAfterTime }, nil
355+ }
330356 return reconcile.Result {}, fmt .Errorf ("validating ec2:CreateFleet authorization, %w" , err )
331357 }
332358 log .FromContext (ctx ).Error (err , "unauthorized to call ec2:CreateFleet" )
@@ -376,12 +402,18 @@ func (v *Validation) validateRunInstancesAuthorization(
376402 // this means there is most likely an eventual consistency issue and we just need to requeue
377403 return reconcile.Result {Requeue : true }, nil
378404 }
379- if awserrors . IsRateLimitedError ( firstSubnetErr ) || awserrors . IsServerError (firstSubnetErr ) {
405+ if isTransientError (firstSubnetErr ) {
380406 return reconcile.Result {Requeue : true }, nil
381407 }
382408 if awserrors .IgnoreUnauthorizedOperationError (firstSubnetErr ) != nil {
383- // Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
384- // it would be an unexpected state
409+ // A dry run also fails when EC2 considers the request itself invalid, e.g. a block device mapping
410+ // whose volume is smaller than the AMI's snapshot. Retrying can't resolve that until the
411+ // EC2NodeClass changes, so surface it on the status condition rather than returning an error that
412+ // is only ever logged, and logged as if it were an authorization failure.
413+ if message , ok := awserrors .ToAPIErrorMessage (firstSubnetErr ); ok {
414+ v .updateCacheOnFailure (nodeClass , tags , ConditionReasonRunInstancesValidationFailed , message )
415+ return reconcile.Result {RequeueAfter : requeueAfterTime }, nil
416+ }
385417 return reconcile.Result {}, fmt .Errorf ("validating ec2:RunInstances authorization, %w" , firstSubnetErr )
386418 }
387419 log .FromContext (ctx ).Error (firstSubnetErr , "unauthorized to call ec2:RunInstances" )
0 commit comments