@@ -18,12 +18,14 @@ use crate::cli::parser::CliParser;
1818
1919use crate :: connection:: ssh:: SshFactory ;
2020use crate :: connection:: local:: LocalFactory ;
21+ use crate :: connection:: chroot:: ChrootFactory ;
2122use crate :: connection:: no:: NoFactory ;
2223use crate :: playbooks:: traversal:: { playbook_traversal, RunState } ;
2324use crate :: playbooks:: context:: PlaybookContext ;
2425use crate :: playbooks:: visitor:: { PlaybookVisitor , CheckMode } ;
2526use crate :: inventory:: inventory:: Inventory ;
2627use std:: sync:: { Arc , RwLock } ;
28+ use std:: path:: PathBuf ;
2729
2830// code behind *most* playbook related CLI commands, launched from main.rs
2931
@@ -54,9 +56,25 @@ pub fn playbook_simulate(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser)
5456}
5557
5658pub fn playbook_pull ( inventory : & Arc < RwLock < Inventory > > , parser : & CliParser ) -> i32 {
57- // Pull mode is essentially local mode, but designed for external integration
58- // It runs playbooks locally on the target host with optional inventory for variables
59- return playbook_with_pull ( inventory, parser, CheckMode :: No ) ;
59+ // Pull mode: runs playbooks locally (or inside a chroot).
60+ // With --url, downloads the playbook from an HTTPS URL or clones a git repo first.
61+ // With --chroot, executes all commands inside the chroot environment.
62+
63+ // Handle --url: download playbook to temp location
64+ let url_playbook_path: Option < PathBuf > = match & parser. pull_url {
65+ Some ( url) => {
66+ match fetch_playbook_url ( url) {
67+ Ok ( path) => Some ( path) ,
68+ Err ( e) => {
69+ println ! ( "failed to fetch playbook from URL: {}" , e) ;
70+ return 1 ;
71+ }
72+ }
73+ }
74+ None => None ,
75+ } ;
76+
77+ return playbook_with_pull ( inventory, parser, CheckMode :: No , url_playbook_path. as_ref ( ) ) ;
6078}
6179
6280fn playbook ( inventory : & Arc < RwLock < Inventory > > , parser : & CliParser , check_mode : CheckMode , connection_mode : ConnectionMode ) -> i32 {
@@ -96,22 +114,38 @@ fn playbook(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser, check_mode:
96114 } ;
97115}
98116
99- fn playbook_with_pull ( inventory : & Arc < RwLock < Inventory > > , parser : & CliParser , check_mode : CheckMode ) -> i32 {
117+ fn playbook_with_pull ( inventory : & Arc < RwLock < Inventory > > , parser : & CliParser , check_mode : CheckMode , url_playbook : Option < & PathBuf > ) -> i32 {
118+ // If a URL-fetched playbook exists, add it to playbook_paths
119+ let playbook_paths = if let Some ( pb_path) = url_playbook {
120+ let paths = Arc :: new ( RwLock :: new ( vec ! [ pb_path. clone( ) ] ) ) ;
121+ // Also include any explicitly specified playbooks
122+ for p in parser. playbook_paths . read ( ) . unwrap ( ) . iter ( ) {
123+ paths. write ( ) . unwrap ( ) . push ( p. clone ( ) ) ;
124+ }
125+ paths
126+ } else {
127+ Arc :: clone ( & parser. playbook_paths )
128+ } ;
129+
130+ // Choose connection factory: ChrootFactory if --chroot, otherwise LocalFactory
131+ let connection_factory: Arc < RwLock < dyn crate :: connection:: factory:: ConnectionFactory > > =
132+ if let Some ( ref chroot_path) = parser. chroot_path {
133+ Arc :: new ( RwLock :: new ( ChrootFactory :: new ( inventory, chroot_path. clone ( ) ) ) )
134+ } else {
135+ Arc :: new ( RwLock :: new ( LocalFactory :: new ( inventory) ) )
136+ } ;
137+
100138 let run_state = Arc :: new ( RunState {
101- // every object gets an inventory, though with local modes it's empty.
102139 inventory : Arc :: clone ( inventory) ,
103- playbook_paths : Arc :: clone ( & parser . playbook_paths ) ,
140+ playbook_paths,
104141 role_paths : Arc :: clone ( & parser. role_paths ) ,
105142 module_paths : Arc :: clone ( & parser. module_paths ) ,
106143 limit_hosts : parser. limit_hosts . clone ( ) ,
107144 limit_groups : parser. limit_groups . clone ( ) ,
108145 batch_size : parser. batch_size . clone ( ) ,
109- // the context is constructed with an instance of the parser instead of having a back-reference
110- // to run-state. Context should mostly *not* get parameters from the parser unless they
111- // are going to appear in variables.
112146 context : Arc :: new ( RwLock :: new ( PlaybookContext :: new ( parser) ) ) ,
113147 visitor : Arc :: new ( RwLock :: new ( PlaybookVisitor :: new ( check_mode) ) ) ,
114- connection_factory : Arc :: new ( RwLock :: new ( LocalFactory :: new ( inventory ) ) ) ,
148+ connection_factory,
115149 tags : parser. tags . clone ( ) ,
116150 allow_localhost_delegation : parser. allow_localhost_delegation ,
117151 is_pull_mode : true ,
@@ -129,3 +163,103 @@ fn playbook_with_pull(inventory: &Arc<RwLock<Inventory>>, parser: &CliParser, ch
129163 } ;
130164}
131165
166+ /// Fetch a playbook from a URL (HTTPS) or clone a git repository.
167+ ///
168+ /// - HTTPS URL ending in .yml/.yaml → downloaded to a temp file
169+ /// - Git URL (ending in .git or containing github/gitlab) → shallow clone
170+ /// - Returns the path to the playbook file or directory
171+ fn fetch_playbook_url ( url : & str ) -> Result < PathBuf , String > {
172+ let temp_dir = std:: env:: temp_dir ( ) . join ( "jetpack-pull" ) ;
173+ let _ = std:: fs:: create_dir_all ( & temp_dir) ;
174+
175+ if url. ends_with ( ".yml" ) || url. ends_with ( ".yaml" ) {
176+ // HTTPS direct download of a single playbook file
177+ let filename = url. rsplit ( '/' ) . next ( ) . unwrap_or ( "playbook.yml" ) ;
178+ let dest = temp_dir. join ( filename) ;
179+
180+ println ! ( "pulling playbook from {}" , url) ;
181+
182+ // Use curl (universally available) to download
183+ let output = std:: process:: Command :: new ( "curl" )
184+ . arg ( "-sfL" )
185+ . arg ( "--connect-timeout" )
186+ . arg ( "30" )
187+ . arg ( "-o" )
188+ . arg ( dest. to_str ( ) . unwrap ( ) )
189+ . arg ( url)
190+ . output ( )
191+ . map_err ( |e| format ! ( "curl failed: {}" , e) ) ?;
192+
193+ if !output. status . success ( ) {
194+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
195+ return Err ( format ! ( "curl failed ({}): {}" , output. status, stderr) ) ;
196+ }
197+
198+ if !dest. exists ( ) {
199+ return Err ( format ! ( "download succeeded but file not found: {:?}" , dest) ) ;
200+ }
201+
202+ Ok ( dest)
203+ } else if url. ends_with ( ".git" )
204+ || url. contains ( "github.com" )
205+ || url. contains ( "gitlab.com" )
206+ || url. contains ( "codeberg.org" )
207+ || url. starts_with ( "git@" )
208+ {
209+ // Git clone
210+ let repo_dir = temp_dir. join ( "repo" ) ;
211+ let _ = std:: fs:: remove_dir_all ( & repo_dir) ; // clean previous
212+
213+ println ! ( "cloning repository from {}" , url) ;
214+
215+ let output = std:: process:: Command :: new ( "git" )
216+ . arg ( "clone" )
217+ . arg ( "--depth" )
218+ . arg ( "1" )
219+ . arg ( url)
220+ . arg ( repo_dir. to_str ( ) . unwrap ( ) )
221+ . output ( )
222+ . map_err ( |e| format ! ( "git clone failed: {}" , e) ) ?;
223+
224+ if !output. status . success ( ) {
225+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
226+ return Err ( format ! ( "git clone failed ({}): {}" , output. status, stderr) ) ;
227+ }
228+
229+ // Look for playbook files in the cloned repo
230+ // Check common locations: playbook.yml, site.yml, main.yml, playbooks/
231+ for candidate in & [ "playbook.yml" , "site.yml" , "main.yml" , "playbook.yaml" , "site.yaml" ] {
232+ let path = repo_dir. join ( candidate) ;
233+ if path. exists ( ) {
234+ return Ok ( path) ;
235+ }
236+ }
237+
238+ // If no standard playbook found, return the repo dir
239+ // (user should specify --playbook to pick the right file)
240+ Ok ( repo_dir)
241+ } else {
242+ // Assume HTTPS URL to a raw file
243+ let dest = temp_dir. join ( "playbook.yml" ) ;
244+
245+ println ! ( "pulling playbook from {}" , url) ;
246+
247+ let output = std:: process:: Command :: new ( "curl" )
248+ . arg ( "-sfL" )
249+ . arg ( "--connect-timeout" )
250+ . arg ( "30" )
251+ . arg ( "-o" )
252+ . arg ( dest. to_str ( ) . unwrap ( ) )
253+ . arg ( url)
254+ . output ( )
255+ . map_err ( |e| format ! ( "curl failed: {}" , e) ) ?;
256+
257+ if !output. status . success ( ) {
258+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
259+ return Err ( format ! ( "curl failed ({}): {}" , output. status, stderr) ) ;
260+ }
261+
262+ Ok ( dest)
263+ }
264+ }
265+
0 commit comments