@@ -82,6 +82,14 @@ impl ChallengeOrchestrator {
8282
8383 /// Add and start a new challenge
8484 pub async fn add_challenge ( & self , config : ChallengeContainerConfig ) -> anyhow:: Result < ( ) > {
85+ // Pull image first to ensure it's available
86+ tracing:: info!(
87+ image = %config. docker_image,
88+ challenge = %config. name,
89+ "Pulling Docker image before starting challenge"
90+ ) ;
91+ self . docker . pull_image ( & config. docker_image ) . await ?;
92+
8593 let instance = self . docker . start_challenge ( & config) . await ?;
8694 self . challenges
8795 . write ( )
@@ -90,6 +98,65 @@ impl ChallengeOrchestrator {
9098 Ok ( ( ) )
9199 }
92100
101+ /// Refresh a challenge (re-pull image and restart container)
102+ pub async fn refresh_challenge ( & self , challenge_id : ChallengeId ) -> anyhow:: Result < ( ) > {
103+ // Get current config
104+ let instance = self
105+ . challenges
106+ . read ( )
107+ . get ( & challenge_id)
108+ . cloned ( )
109+ . ok_or_else ( || anyhow:: anyhow!( "Challenge not found: {}" , challenge_id) ) ?;
110+
111+ tracing:: info!(
112+ challenge_id = %challenge_id,
113+ image = %instance. image,
114+ "Refreshing challenge (re-pulling image and restarting)"
115+ ) ;
116+
117+ // Stop current container
118+ self . docker . stop_container ( & instance. container_id ) . await ?;
119+
120+ // Re-pull the image (force fresh pull)
121+ self . docker . pull_image ( & instance. image ) . await ?;
122+
123+ // We need the full config to restart - get it from state or recreate
124+ // For now, create a minimal config from the instance
125+ let config = ChallengeContainerConfig {
126+ challenge_id,
127+ name : format ! ( "challenge-{}" , challenge_id) ,
128+ docker_image : instance. image . clone ( ) ,
129+ mechanism_id : 0 , // Default, should be stored
130+ emission_weight : 1.0 ,
131+ timeout_secs : 3600 ,
132+ cpu_cores : 2.0 ,
133+ memory_mb : 4096 ,
134+ gpu_required : false ,
135+ } ;
136+
137+ // Start new container
138+ let new_instance = self . docker . start_challenge ( & config) . await ?;
139+ self . challenges . write ( ) . insert ( challenge_id, new_instance) ;
140+
141+ tracing:: info!( challenge_id = %challenge_id, "Challenge refreshed successfully" ) ;
142+ Ok ( ( ) )
143+ }
144+
145+ /// Refresh all challenges (re-pull images and restart all containers)
146+ pub async fn refresh_all_challenges ( & self ) -> anyhow:: Result < ( ) > {
147+ let challenge_ids: Vec < ChallengeId > = self . challenges . read ( ) . keys ( ) . cloned ( ) . collect ( ) ;
148+
149+ tracing:: info!( count = challenge_ids. len( ) , "Refreshing all challenges" ) ;
150+
151+ for id in challenge_ids {
152+ if let Err ( e) = self . refresh_challenge ( id) . await {
153+ tracing:: error!( challenge_id = %id, error = %e, "Failed to refresh challenge" ) ;
154+ }
155+ }
156+
157+ Ok ( ( ) )
158+ }
159+
93160 /// Update a challenge (pull new image, restart container)
94161 pub async fn update_challenge ( & self , config : ChallengeContainerConfig ) -> anyhow:: Result < ( ) > {
95162 // Stop old container if exists - get container_id first to avoid holding lock across await
0 commit comments