@@ -408,6 +408,75 @@ func TestK8sTaskExecutor_Handle_LaunchResource(t *testing.T) {
408408 assert .True (t , k8serrors .IsNotFound (err ))
409409 })
410410
411+ t .Run ("jobBadRequest" , func (t * testing.T ) {
412+ // BadRequest (HTTP 400) errors — typically from a validating admission
413+ // webhook — are intrinsic to the request payload and not transient.
414+ // Retrying with the same input will produce the same rejection.
415+ // They should be treated as a permanent failure (PhasePermanentFailure)
416+ // rather than the default retryable system error, so workflows surface
417+ // the validation error to the user instead of exhausting their retry
418+ // budget. See https://github.com/flyteorg/flyte/issues/6531.
419+ tctx := getMockTaskContext (PluginPhaseNotStarted , PluginPhaseNotStarted )
420+ mockResourceHandler := & pluginsk8sMock.Plugin {}
421+ mockResourceHandler .EXPECT ().GetProperties ().Return (k8s.PluginProperties {})
422+ mockResourceHandler .EXPECT ().BuildResource (mock .Anything , mock .Anything ).Return (& v1.Pod {}, nil )
423+ fakeClient := extendedFakeClient {
424+ Client : fake .NewClientBuilder ().WithRuntimeObjects ().Build (),
425+ CreateError : k8serrors .NewBadRequest ("admission webhook \" deny.example.com\" denied the request: invalid pod spec" ),
426+ }
427+ mockClientset := k8sfake .NewSimpleClientset ()
428+
429+ pluginManager , err := NewPluginManager (ctx , dummySetupContext (fakeClient ), k8s.PluginEntry {
430+ ID : "x" ,
431+ ResourceToWatch : & v1.Pod {},
432+ Plugin : mockResourceHandler ,
433+ }, NewResourceMonitorIndex (), mockClientset )
434+ assert .NoError (t , err )
435+
436+ transition , err := pluginManager .Handle (ctx , tctx )
437+ assert .NoError (t , err )
438+ assert .NotNil (t , transition )
439+ transitionInfo := transition .Info ()
440+ assert .NotNil (t , transitionInfo )
441+ assert .Equal (t , pluginsCore .PhasePermanentFailure , transitionInfo .Phase ())
442+ assert .Equal (t , "BadTaskFormat" , transitionInfo .Err ().GetCode ())
443+ })
444+
445+ t .Run ("jobInvalid" , func (t * testing.T ) {
446+ // Invalid (HTTP 422) errors indicate the request was well-formed but
447+ // the object failed validation (e.g. an invalid field value). Like
448+ // BadRequest, this is intrinsic to the payload and not transient, so
449+ // it should be a permanent failure.
450+ tctx := getMockTaskContext (PluginPhaseNotStarted , PluginPhaseNotStarted )
451+ mockResourceHandler := & pluginsk8sMock.Plugin {}
452+ mockResourceHandler .EXPECT ().GetProperties ().Return (k8s.PluginProperties {})
453+ mockResourceHandler .EXPECT ().BuildResource (mock .Anything , mock .Anything ).Return (& v1.Pod {}, nil )
454+ fakeClient := extendedFakeClient {
455+ Client : fake .NewClientBuilder ().WithRuntimeObjects ().Build (),
456+ CreateError : k8serrors .NewInvalid (
457+ schema.GroupKind {Group : "" , Kind : "Pod" },
458+ "test-pod" ,
459+ nil ,
460+ ),
461+ }
462+ mockClientset := k8sfake .NewSimpleClientset ()
463+
464+ pluginManager , err := NewPluginManager (ctx , dummySetupContext (fakeClient ), k8s.PluginEntry {
465+ ID : "x" ,
466+ ResourceToWatch : & v1.Pod {},
467+ Plugin : mockResourceHandler ,
468+ }, NewResourceMonitorIndex (), mockClientset )
469+ assert .NoError (t , err )
470+
471+ transition , err := pluginManager .Handle (ctx , tctx )
472+ assert .NoError (t , err )
473+ assert .NotNil (t , transition )
474+ transitionInfo := transition .Info ()
475+ assert .NotNil (t , transitionInfo )
476+ assert .Equal (t , pluginsCore .PhasePermanentFailure , transitionInfo .Phase ())
477+ assert .Equal (t , "BadTaskFormat" , transitionInfo .Err ().GetCode ())
478+ })
479+
411480 t .Run ("Insufficient resource blocking pod creation for the first time" , func (t * testing.T ) {
412481 tctx := getMockTaskContext (PluginPhaseNotStarted , PluginPhaseNotStarted )
413482 var tmpl * core.TaskTemplate
0 commit comments