@@ -30,6 +30,7 @@ import {
3030 type Watch ,
3131} from "@kubernetes/client-node" ;
3232import type { K8sApi } from "../clients/kubernetes.js" ;
33+ import { WorkloadNotCreatedError } from "./createErrors.js" ;
3334import { getRestoreRunnerId , getRunnerId } from "../util.js" ;
3435import type { WorkloadManagerCreateOptions } from "./types.js" ;
3536
@@ -574,12 +575,13 @@ describe("run-crd carries every shared create-option or excludes it on purpose",
574575} ) ;
575576
576577describe ( "RunCrdWorkloadManager.create" , ( ) => {
577- const manager = ( ) =>
578+ const manager = ( releaseUnstartedRuns ?: boolean ) =>
578579 new RunCrdWorkloadManager ( {
579580 workloadApiProtocol : "http" ,
580581 workloadApiPort : 8020 ,
581582 namespace : "v4-runs" ,
582583 runtime : "microvm" ,
584+ releaseUnstartedRuns,
583585 } ) ;
584586
585587 beforeEach ( ( ) => {
@@ -588,6 +590,12 @@ describe("RunCrdWorkloadManager.create", () => {
588590 getRunner . mockReset ( ) ;
589591 deleteRunner . mockReset ( ) ;
590592 deleteRunner . mockResolvedValue ( { } ) ;
593+ createSecret . mockReset ( ) ;
594+ createSecret . mockResolvedValue ( { metadata : { uid : "uid-secret" } } ) ;
595+ deleteSecret . mockReset ( ) ;
596+ deleteSecret . mockResolvedValue ( { } ) ;
597+ patchObject . mockReset ( ) ;
598+ patchObject . mockResolvedValue ( { } ) ;
591599 } ) ;
592600
593601 // keyedOptions() is dequeued at 03:00:00, so the default is an earlier delivery.
@@ -641,9 +649,6 @@ describe("RunCrdWorkloadManager.create", () => {
641649 . mockRejectedValueOnce ( { code : 409 } )
642650 . mockResolvedValueOnce ( { metadata : { uid : "uid-recreated" } } ) ;
643651 getRunner . mockResolvedValue ( inTheWay ( "snapshot_earlier" ) ) ;
644- createSecret . mockReset ( ) ;
645- createSecret . mockResolvedValue ( { metadata : { uid : "uid-secret" } } ) ;
646- patchObject . mockResolvedValue ( { } ) ;
647652
648653 await manager ( ) . create ( keyedOptions ( { deploymentToken : "tok" } ) ) ;
649654
@@ -706,17 +711,130 @@ describe("RunCrdWorkloadManager.create", () => {
706711 expect ( deleteRunner ) . not . toHaveBeenCalled ( ) ;
707712 expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
708713 } ) ;
714+
715+ const apiError = ( code : number , message = "refused" ) => ( {
716+ code,
717+ body : JSON . stringify ( { kind : "Status" , code, message } ) ,
718+ } ) ;
719+ const withToken = keyedOptions ( { deploymentToken : "tok" } ) ;
720+
721+ it . each ( [
722+ [ "off" , false ] ,
723+ [ "unset" , undefined ] ,
724+ ] ) ( "rethrows a refused Runner create as it is with the option %s" , async ( _ , flag ) => {
725+ const failure = apiError ( 422 ) ;
726+ createRunner . mockRejectedValueOnce ( failure ) ;
727+
728+ await expect ( manager ( flag ) . create ( keyedOptions ( ) ) ) . rejects . toBe ( failure ) ;
729+ expect ( createRunner ) . toHaveBeenCalledTimes ( 1 ) ;
730+ } ) ;
731+
732+ it ( "rethrows a refused token Secret create as it is with the option off" , async ( ) => {
733+ const failure = apiError ( 403 ) ;
734+ createSecret . mockRejectedValueOnce ( failure ) ;
735+
736+ await expect ( manager ( false ) . create ( withToken ) ) . rejects . toBe ( failure ) ;
737+ expect ( createRunner ) . not . toHaveBeenCalled ( ) ;
738+ } ) ;
739+
740+ describe ( "with releases on" , ( ) => {
741+ it ( "reports a Runner create the API refused as not created" , async ( ) => {
742+ createRunner . mockRejectedValueOnce ( apiError ( 422 , "spec.machine: Unsupported value" ) ) ;
743+
744+ const failed = manager ( true ) . create ( keyedOptions ( ) ) ;
745+
746+ await expect ( failed ) . rejects . toBeInstanceOf ( WorkloadNotCreatedError ) ;
747+ await expect ( failed ) . rejects . toMatchObject ( { message : "spec.machine: Unsupported value" } ) ;
748+ expect ( createRunner ) . toHaveBeenCalledTimes ( 1 ) ;
749+ expect ( getRunner ) . not . toHaveBeenCalled ( ) ;
750+ } ) ;
751+
752+ it ( "releases the token Secret of a refused create" , async ( ) => {
753+ createRunner . mockRejectedValueOnce ( apiError ( 422 ) ) ;
754+
755+ await expect ( manager ( true ) . create ( withToken ) ) . rejects . toThrow ( WorkloadNotCreatedError ) ;
756+
757+ expect ( createSecret ) . toHaveBeenCalledTimes ( 1 ) ;
758+ expect ( deleteSecret ) . toHaveBeenCalledTimes ( 1 ) ;
759+ } ) ;
760+
761+ it ( "reports a refused token Secret create as not created, with nothing to delete" , async ( ) => {
762+ createSecret . mockRejectedValueOnce ( apiError ( 403 , "secrets is forbidden" ) ) ;
763+
764+ const failed = manager ( true ) . create ( withToken ) ;
765+
766+ await expect ( failed ) . rejects . toBeInstanceOf ( WorkloadNotCreatedError ) ;
767+ await expect ( failed ) . rejects . toMatchObject ( { message : "secrets is forbidden" } ) ;
768+ expect ( createRunner ) . not . toHaveBeenCalled ( ) ;
769+ expect ( deleteSecret ) . not . toHaveBeenCalled ( ) ;
770+ } ) ;
771+
772+ it ( "keeps the token Secret a redrive found, which it did not create" , async ( ) => {
773+ createSecret . mockRejectedValueOnce ( { code : 409 } ) ;
774+ createRunner . mockRejectedValueOnce ( apiError ( 422 ) ) ;
775+
776+ await expect ( manager ( true ) . create ( withToken ) ) . rejects . toThrow ( WorkloadNotCreatedError ) ;
777+
778+ expect ( deleteSecret ) . not . toHaveBeenCalled ( ) ;
779+ } ) ;
780+
781+ it ( "rethrows a Runner create that failed with 500 as it is" , async ( ) => {
782+ const failure = apiError ( 500 ) ;
783+ createRunner . mockRejectedValueOnce ( failure ) ;
784+
785+ await expect ( manager ( true ) . create ( keyedOptions ( ) ) ) . rejects . toBe ( failure ) ;
786+ expect ( createRunner ) . toHaveBeenCalledTimes ( 1 ) ;
787+ } ) ;
788+
789+ it ( "rethrows a token Secret create that failed with 500 as it is" , async ( ) => {
790+ const failure = apiError ( 500 ) ;
791+ createSecret . mockRejectedValueOnce ( failure ) ;
792+
793+ await expect ( manager ( true ) . create ( withToken ) ) . rejects . toBe ( failure ) ;
794+ expect ( createRunner ) . not . toHaveBeenCalled ( ) ;
795+ } ) ;
796+
797+ it ( "reports a refused replacement as not created, with both attempts' token Secrets deleted" , async ( ) => {
798+ createRunner . mockRejectedValueOnce ( { code : 409 } ) . mockRejectedValueOnce ( apiError ( 403 ) ) ;
799+ createSecret
800+ . mockResolvedValueOnce ( { metadata : { uid : "uid-secret-1" } } )
801+ . mockResolvedValueOnce ( { metadata : { uid : "uid-secret-2" } } ) ;
802+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_earlier" ) ) ;
803+
804+ await expect ( manager ( true ) . create ( withToken ) ) . rejects . toThrow ( WorkloadNotCreatedError ) ;
805+
806+ expect ( deleteRunner ) . toHaveBeenCalledTimes ( 1 ) ;
807+ expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
808+ const [ first , second ] = createSecret . mock . calls . map ( ( [ { body } ] ) => body . metadata . name ) ;
809+ const deleted = deleteSecret . mock . calls . map ( ( [ { name, body } ] ) => [
810+ name ,
811+ body . preconditions . uid ,
812+ ] ) ;
813+ expect ( deleted ) . toEqual ( [
814+ [ first , "uid-secret-1" ] ,
815+ [ second , "uid-secret-2" ] ,
816+ ] ) ;
817+ } ) ;
818+
819+ it ( "does not wrap a template key refusal" , async ( ) => {
820+ const failed = manager ( true ) . create ( createOptions ( { templateKey : undefined } ) ) ;
821+
822+ await expect ( failed ) . rejects . toThrow ( "TemplateKeyMissing" ) ;
823+ await expect ( failed ) . rejects . not . toBeInstanceOf ( WorkloadNotCreatedError ) ;
824+ } ) ;
825+ } ) ;
709826} ) ;
710827
711828describe ( "RunCrdWorkloadManager.restore" , ( ) => {
712829 const checkpoint = { id : "checkpoint_abc" , location : "node-a/6f1c2a9e-snap" } ;
713830
714- function manager ( runtime : "container" | "microvm" = "microvm" ) {
831+ function manager ( runtime : "container" | "microvm" = "microvm" , releaseUnstartedRuns ?: boolean ) {
715832 return new RunCrdWorkloadManager ( {
716833 workloadApiProtocol : "http" ,
717834 workloadApiPort : 8020 ,
718835 namespace : "v4-runs" ,
719836 runtime,
837+ releaseUnstartedRuns,
720838 } ) ;
721839 }
722840
@@ -753,6 +871,14 @@ describe("RunCrdWorkloadManager.restore", () => {
753871 expect ( body . spec . restore ) . toEqual ( { snapshotID : "6f1c2a9e-snap" , node : "node-a" } ) ;
754872 } ) ;
755873
874+ it ( "reports a refused resume create as not created, with releases on" , async ( ) => {
875+ createRunner . mockRejectedValueOnce ( { code : 422 } ) ;
876+
877+ await expect (
878+ manager ( "microvm" , true ) . restore ( keyedOptions ( ) , checkpoint )
879+ ) . rejects . toBeInstanceOf ( WorkloadNotCreatedError ) ;
880+ } ) ;
881+
756882 it ( "creates no resume without a template key" , async ( ) => {
757883 await expect ( manager ( ) . restore ( createOptions ( ) , checkpoint ) ) . rejects . toThrow (
758884 "TemplateKeyMissing"
0 commit comments