99All commands support remote execution via the global ``--host`` flag.
1010"""
1111
12+ from __future__ import annotations
13+
1214import contextlib
1315import json
1416import logging
1517import time
1618from pathlib import Path
17- from typing import Annotated
19+ from typing import TYPE_CHECKING , Annotated
1820
1921import cyclopts
2022
2123from rots import context
2224from rots .config import Config
2325
26+ if TYPE_CHECKING :
27+ from ots_shared .ssh import Executor
28+
2429from ..common import DryRun , JsonOutput
2530from ._helpers import (
2631 ProbeResult ,
2732 ProxyError ,
2833 adapt_to_json ,
34+ collect_local_files ,
2935 evaluate_assertions ,
3036 find_free_port ,
37+ find_template_in_dir ,
3138 parse_trace_url ,
3239 patch_caddy_json ,
40+ push_files_to_remote ,
3341 reload_caddy ,
3442 render_template ,
3543 run_caddy ,
@@ -93,11 +101,13 @@ def render(
93101 print (rendered )
94102 return
95103
96- # Validate before writing
97- validate_caddy_config (rendered , executor = ex )
104+ # Validate before writing — pass source_dir so relative imports resolve
105+ from ots_shared .ssh import is_remote
106+
107+ source_dir = tpl .parent
108+ validate_caddy_config (rendered , executor = ex , source_dir = source_dir )
98109
99110 # Write to output path
100- from ots_shared .ssh import is_remote
101111
102112 if is_remote (ex ):
103113 ex .run (["mkdir" , "-p" , str (out .parent )], timeout = 15 )
@@ -116,27 +126,53 @@ def render(
116126
117127@app .command
118128def push (
119- template_file : Annotated [
129+ source : Annotated [
120130 Path ,
121131 cyclopts .Parameter (
122- help = "Local Caddyfile.template to push to the remote host" ,
132+ help = "Local file or directory to push to the remote host" ,
123133 ),
124134 ],
125135 output : Output = None ,
126136 dry_run : DryRun = False ,
137+ remote_dir : Annotated [
138+ Path | None ,
139+ cyclopts .Parameter (
140+ name = "--remote-dir" ,
141+ help = "Override remote destination directory" ,
142+ ),
143+ ] = None ,
144+ template : Annotated [
145+ str | None ,
146+ cyclopts .Parameter (
147+ name = "--template" ,
148+ help = "Template file within directory to render (auto-detected from *.template)" ,
149+ ),
150+ ] = None ,
151+ no_render : Annotated [
152+ bool ,
153+ cyclopts .Parameter (
154+ name = "--no-render" ,
155+ negative = [],
156+ help = "Skip render/validate/reload after pushing" ,
157+ ),
158+ ] = False ,
127159) -> None :
128- """Push a local Caddyfile.template, render it, and reload Caddy.
160+ """Push a local file or directory, render template, and reload Caddy.
161+
162+ When *source* is a single file, pushes it to the remote template path,
163+ renders with envsubst, validates, and reloads Caddy.
129164
130- Combines three steps into one:
131- 1. Push local template to remote /etc/onetimesecret/Caddyfile.template
132- 2. Render with envsubst using HOST environment
133- 3. Reload Caddy to apply
165+ When *source* is a directory, pushes all files (recursively, skipping
166+ hidden files) to the remote destination, then optionally renders a
167+ ``*.template`` file found within and reloads Caddy.
134168
135169 Requires --host (pushing to localhost is not useful).
136170
137171 Examples:
138172 ots --host eu-web-01 proxy push Caddyfile.template
139- ots --host eu-web-01 proxy push Caddyfile.template --dry-run
173+ ots --host eu-web-01 proxy push caddy/ --remote-dir /etc/onetimesecret/
174+ ots --host eu-web-01 proxy push caddy/ --template Caddyfile.template
175+ ots --host eu-web-01 proxy push caddy/ --no-render
140176 """
141177 from ots_shared .ssh import is_remote
142178
@@ -146,43 +182,129 @@ def push(
146182 if not is_remote (ex ):
147183 raise SystemExit ("proxy push requires a remote host. Use --host to specify one." )
148184
149- if not template_file .exists ():
150- raise SystemExit (f"Local template not found: { template_file } " )
185+ if not source .exists ():
186+ raise SystemExit (f"Local source not found: { source } " )
187+
188+ try :
189+ if source .is_dir ():
190+ _push_directory (
191+ source ,
192+ cfg = cfg ,
193+ executor = ex ,
194+ output = output ,
195+ dry_run = dry_run ,
196+ remote_dir = remote_dir ,
197+ template_name = template ,
198+ no_render = no_render ,
199+ )
200+ else :
201+ _push_file (source , cfg = cfg , executor = ex , output = output , dry_run = dry_run )
202+ except ProxyError as e :
203+ raise SystemExit (str (e )) from e
204+
151205
206+ def _push_file (
207+ source : Path ,
208+ * ,
209+ cfg : Config ,
210+ executor : Executor ,
211+ output : Path | None ,
212+ dry_run : bool ,
213+ ) -> None :
214+ """Push a single template file, render, validate, and reload."""
152215 tpl_dest = cfg .proxy_template
153216 out = output or cfg .proxy_config
217+ content = source .read_text ()
154218
155- try :
156- content = template_file .read_text ()
219+ if dry_run :
220+ print (f"Would push: { source } -> { tpl_dest } " )
221+ print (f"Would render: { tpl_dest } -> { out } " )
222+ print ("Would reload Caddy" )
223+ return
157224
158- if dry_run :
159- print (f"Would push: { template_file } -> { tpl_dest } " )
160- print (f"Would render: { tpl_dest } -> { out } " )
225+ # Step 1: Push template to remote
226+ result = executor .run (["mkdir" , "-p" , str (tpl_dest .parent )], timeout = 15 )
227+ result = executor .run (["tee" , str (tpl_dest )], input = content , timeout = 15 )
228+ if not result .ok :
229+ raise ProxyError (f"Failed to write { tpl_dest } : { result .stderr } " )
230+ print (f"[ok] Pushed { source } -> { tpl_dest } " )
231+
232+ # Step 2: Render template on remote
233+ rendered = render_template (tpl_dest , executor = executor )
234+ validate_caddy_config (rendered , executor = executor , source_dir = tpl_dest .parent )
235+ result = executor .run (["mkdir" , "-p" , str (out .parent )], timeout = 15 )
236+ result = executor .run (["tee" , str (out )], input = rendered , timeout = 15 )
237+ if not result .ok :
238+ raise ProxyError (f"Failed to write { out } : { result .stderr } " )
239+ print (f"[ok] Rendered { tpl_dest } -> { out } " )
240+
241+ # Step 3: Reload Caddy
242+ reload_caddy (executor = executor )
243+ print ("[ok] Caddy reloaded" )
244+
245+
246+ def _push_directory (
247+ source : Path ,
248+ * ,
249+ cfg : Config ,
250+ executor : Executor ,
251+ output : Path | None ,
252+ dry_run : bool ,
253+ remote_dir : Path | None ,
254+ template_name : str | None ,
255+ no_render : bool ,
256+ ) -> None :
257+ """Push a directory of files, optionally render and reload."""
258+ dest = remote_dir or cfg .proxy_template .parent
259+ out = output or cfg .proxy_config
260+
261+ files = collect_local_files (source )
262+ if not files :
263+ raise ProxyError (f"No files found in { source } " )
264+
265+ # Determine template file once for both dry_run and real execution
266+ tpl_name : str | None = None
267+ if not no_render :
268+ if template_name :
269+ tpl_name = template_name
270+ else :
271+ found = find_template_in_dir (source )
272+ if found :
273+ tpl_name = found .name
274+
275+ if dry_run :
276+ print (f"Would push { len (files )} file(s) to { dest } :" )
277+ push_files_to_remote (source , dest , executor = executor , dry_run = True )
278+ if tpl_name :
279+ print (f"Would render: { dest / tpl_name } -> { out } " )
161280 print ("Would reload Caddy" )
162- return
281+ elif not no_render :
282+ print ("No template found; skipping render/reload" )
283+ return
163284
164- # Step 1: Push template to remote
165- result = ex .run (["mkdir" , "-p" , str (tpl_dest .parent )], timeout = 15 )
166- result = ex .run (["tee" , str (tpl_dest )], input = content , timeout = 15 )
167- if not result .ok :
168- raise ProxyError (f"Failed to write { tpl_dest } : { result .stderr } " )
169- print (f"[ok] Pushed { template_file } -> { tpl_dest } " )
170-
171- # Step 2: Render template on remote
172- rendered = render_template (tpl_dest , executor = ex )
173- validate_caddy_config (rendered , executor = ex )
174- result = ex .run (["mkdir" , "-p" , str (out .parent )], timeout = 15 )
175- result = ex .run (["tee" , str (out )], input = rendered , timeout = 15 )
176- if not result .ok :
177- raise ProxyError (f"Failed to write { out } : { result .stderr } " )
178- print (f"[ok] Rendered { tpl_dest } -> { out } " )
285+ # Push all files
286+ print (f"Pushing { len (files )} file(s) to { dest } :" )
287+ push_files_to_remote (source , dest , executor = executor )
288+ print (f"[ok] Pushed { len (files )} file(s)" )
179289
180- # Step 3: Reload Caddy
181- reload_caddy (executor = ex )
182- print ("[ok] Caddy reloaded" )
290+ if not tpl_name :
291+ if not no_render :
292+ print ("No template found; skipping render/reload" )
293+ return
183294
184- except ProxyError as e :
185- raise SystemExit (str (e )) from e
295+ tpl_path = dest / tpl_name
296+
297+ # Render, validate, reload
298+ rendered = render_template (tpl_path , executor = executor )
299+ validate_caddy_config (rendered , executor = executor , source_dir = dest )
300+ result = executor .run (["mkdir" , "-p" , str (out .parent )], timeout = 15 )
301+ result = executor .run (["tee" , str (out )], input = rendered , timeout = 15 )
302+ if not result .ok :
303+ raise ProxyError (f"Failed to write { out } : { result .stderr } " )
304+ print (f"[ok] Rendered { tpl_path } -> { out } " )
305+
306+ reload_caddy (executor = executor )
307+ print ("[ok] Caddy reloaded" )
186308
187309
188310@app .command
0 commit comments