Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ sha2 = "0.10"
base64 = "0.22"
petname = "2"
indicatif = "0.17"
percent-encoding = "2"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ veld stop --name dev
| `veld restart [--name <n>]` | Restart an environment |
| `veld status [--name <n>] [--json]` | Show run status |
| `veld urls [--name <n>] [--json]` | Show URLs for a run |
| `veld postico [--name <n>] [--node <n>] [--print] [--json]` | Open a run's database in Postico (macOS); `--print` emits the connection URL |
| `veld logs [--name <n>] [--node <n>] [--lines <n>] [-f] [--since <d>] [--source <s>] [-s <term>] [-C <n>]` | View logs (`-f` follow, `-s` search, `-C` context lines) |
| `veld graph [NODE:VARIANT...]` | Print dependency graph |
| `veld nodes` | List all nodes and variants |
Expand Down
49 changes: 49 additions & 0 deletions crates/veld-core/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ impl NodeState {
})
.collect()
}

/// Output keys that, when all present, mark this node as a database a
/// client can connect to. User/password are optional and not required here.
pub const DATABASE_OUTPUT_KEYS: [&'static str; 3] = ["DB_HOST", "DB_PORT", "DB_NAME"];

/// True if this node exposes the outputs needed to build a database
/// connection URL. Shared by the `veld postico` command and the management
/// dashboard so both agree on what counts as a database node.
pub fn exposes_database(&self) -> bool {
Self::DATABASE_OUTPUT_KEYS
.iter()
.all(|k| self.outputs.contains_key(*k))
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -365,3 +378,39 @@ impl GlobalRegistry {
atomic_write(&path, &data)
}
}

#[cfg(test)]
mod tests {
use super::*;

fn node_with_outputs(pairs: &[(&str, &str)]) -> NodeState {
let mut ns = NodeState::new("database", "dblab");
ns.outputs = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
ns
}

#[test]
fn exposes_database_requires_all_keys() {
let ns = node_with_outputs(&[
("DB_HOST", "localhost"),
("DB_PORT", "5432"),
("DB_NAME", "app"),
]);
assert!(ns.exposes_database());
}

#[test]
fn exposes_database_false_when_a_key_is_missing() {
let ns = node_with_outputs(&[("DB_HOST", "localhost"), ("DB_PORT", "5432")]);
assert!(!ns.exposes_database());
}

#[test]
fn exposes_database_false_for_non_database_node() {
let ns = node_with_outputs(&[("PORT", "3000")]);
assert!(!ns.exposes_database());
}
}
12 changes: 12 additions & 0 deletions crates/veld-daemon/assets/management-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@
h+='<button data-action="copy-url" data-url="'+esc(url)+'">Copy</button>';
h+='<button data-action="open-url" data-url="'+esc(url)+'">Open</button>';
}
if(n.database&&run.status==='running'){
h+='<button data-action="open-postico" title="Open the database in Postico (macOS)">Postico</button>';
}
h+='</td>';
h+='<td class="svc-dim">'+esc(n.variant)+'</td>';
h+='<td class="svc-pid">'+(n.pid||'')+'</td></tr>';
Expand Down Expand Up @@ -475,6 +478,15 @@
if(copyUrl){navigator.clipboard.writeText(copyUrl.dataset.url).then(()=>{copyUrl.classList.add('ok');copyUrl.textContent='Copied';setTimeout(()=>{copyUrl.classList.remove('ok');copyUrl.textContent='Copy';},1500);});return;}
const openUrl=e.target.closest('[data-action="open-url"]');
if(openUrl){window.open(openUrl.dataset.url,'_blank','noopener');return;}
const postico=e.target.closest('[data-action="open-postico"]');
if(postico&&card){
const btn=postico,orig=btn.textContent;btn.disabled=true;btn.textContent='Opening…';
fetch('/api/environments/'+encodeURIComponent(card.dataset.run)+'/postico',{method:'POST',headers:{'X-Veld-Request':'1'}})
.then(r=>{if(!r.ok)btn.textContent='Failed';})
.catch(()=>{btn.textContent='Failed';})
.finally(()=>{setTimeout(()=>{btn.textContent=orig;btn.disabled=false;},1500);});
return;
}
if(e.target.closest('[data-action="copy-path"]')&&card){const btn=e.target.closest('[data-action="copy-path"]');navigator.clipboard.writeText(card.dataset.path).then(()=>{btn.classList.add('ok');btn.textContent='Copied';setTimeout(()=>{btn.classList.remove('ok');btn.textContent='Copy path';},1500);});return;}
if(e.target.closest('[data-action="open-terminal"]')&&card){fetch('/api/open-terminal',{method:'POST',headers:{'Content-Type':'application/json','X-Veld-Request':'1'},body:JSON.stringify({path:card.dataset.path})});return;}
if(e.target.closest('[data-action="stop-env"]')&&card){
Expand Down
22 changes: 22 additions & 0 deletions crates/veld-daemon/src/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub fn routes() -> Router {
.route("/api/open-terminal", post(open_terminal))
.route("/api/environments/{run}/stop", post(stop_environment))
.route("/api/environments/{run}/restart", post(restart_environment))
.route("/api/environments/{run}/postico", post(open_postico))
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -77,12 +78,19 @@ struct NodeInfo {
consecutive_failures: u32,
#[serde(skip_serializing_if = "Option::is_none")]
last_liveness_error: Option<String>,
/// True if this node exposes a database the dashboard can open in Postico.
#[serde(skip_serializing_if = "is_false")]
database: bool,
}

fn is_zero(v: &u32) -> bool {
*v == 0
}

fn is_false(v: &bool) -> bool {
!*v
}

async fn list_environments() -> Result<Json<EnvironmentList>, StatusCode> {
let registry = GlobalRegistry::load().map_err(|e| {
warn!("failed to load global registry: {e}");
Expand Down Expand Up @@ -115,6 +123,7 @@ async fn list_environments() -> Result<Json<EnvironmentList>, StatusCode> {
recovery_count: ns.recovery_count,
consecutive_failures: ns.consecutive_failures,
last_liveness_error: ns.last_liveness_error.clone(),
database: ns.exposes_database(),
})
.collect()
})
Expand Down Expand Up @@ -417,6 +426,19 @@ async fn restart_environment(
run_veld_command(&run_name, "restart").await
}

/// Open the run's database in Postico by delegating to `veld postico`, which
/// reads the connection details and shells out to the app. The credentials
/// never reach the browser — the daemon hands off to the CLI.
async fn open_postico(headers: axum::http::HeaderMap, Path(run_name): Path<String>) -> StatusCode {
if let Err(s) = check_csrf(&headers) {
return s;
}
if let Err(s) = validate_run_name(&run_name) {
return s;
}
run_veld_command(&run_name, "postico").await
}

/// Spawn `veld <action> --name <run>` in the project directory via a login
/// shell. The project_root is looked up from the GlobalRegistry (never
/// supplied by the client) to prevent directory traversal.
Expand Down
1 change: 1 addition & 0 deletions crates/veld/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dirs = { workspace = true }
reqwest = { workspace = true }
libc = "0.2"
indicatif = { workspace = true }
percent-encoding = { workspace = true }

[dev-dependencies]
tempfile = "3"
1 change: 1 addition & 0 deletions crates/veld/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod init;
pub mod list;
pub mod logs;
pub mod nodes;
pub mod postico;
pub mod presets;
pub mod restart;
pub mod runs;
Expand Down
Loading
Loading