Skip to content
Merged
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
15 changes: 8 additions & 7 deletions Cargo.lock

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

15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ serde_json.workspace = true
serde_yaml_ng.workspace = true
clap.workspace = true
rustix.workspace = true
signal-hook.workspace = true
time.workspace = true

[dev-dependencies]
Expand All @@ -54,20 +55,20 @@ members = ["crates/*"]
default-members = [".", "crates/*"]

[workspace.package]
version = "3.3.0"
version = "3.4.0"
edition = "2024"
rust-version = "1.87"
license = "MIT"
repository = "https://github.com/execution-associates/isb"
homepage = "https://github.com/execution-associates/isb"

[workspace.dependencies]
isb-core = { path = "crates/isb-core", version = "=3.3.0" }
isb-server = { path = "crates/isb-server", version = "=3.3.0" }
isb-apps = { path = "crates/isb-apps", version = "=3.3.0" }
isb-daemon = { path = "crates/isb-daemon", version = "=3.3.0" }
isb-tui = { path = "crates/isb-tui", version = "=3.3.0" }
isb-egress = { path = "crates/isb-egress", version = "=3.3.0" }
isb-core = { path = "crates/isb-core", version = "=3.4.0" }
isb-server = { path = "crates/isb-server", version = "=3.4.0" }
isb-apps = { path = "crates/isb-apps", version = "=3.4.0" }
isb-daemon = { path = "crates/isb-daemon", version = "=3.4.0" }
isb-tui = { path = "crates/isb-tui", version = "=3.4.0" }
isb-egress = { path = "crates/isb-egress", version = "=3.4.0" }

serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
3 changes: 2 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ cannot exec or read secrets.
`stdin`. No port-forward: `curl` through `app_exec`. CLI: `isb instance
ls|get|exec|restart`, `isb app exec|logs|restart|scale|top|events`,
`isb stack exec|logs` (`--failed`: the last replica that failed to start),
`isb cp`.
`isb cp`. `exec -it` is for a person at a terminal (refused without one);
agents run commands to completion.
- **Data:** `database_create`, `database_get`, `backup_*`, `volume_*`,
`job_*`. **Secrets:** `secret_create`, `secret_set` (values base64; a new
version rolls the stacks using it), `secret_list` (no values).
Expand Down
5 changes: 3 additions & 2 deletions crates/isb-core/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,12 +874,13 @@ fn has_input(fd: impl rustix::fd::AsFd, wait: Duration) -> bool {
}

/// Puts the local terminal in raw mode for the life of the guard.
struct RawMode {
pub struct RawMode {
saved: rustix::termios::Termios,
}

impl RawMode {
fn enable() -> Option<RawMode> {
/// `None` when stdin is not a terminal (nothing to restore).
pub fn enable() -> Option<RawMode> {
let stdin = std::io::stdin();
let saved = rustix::termios::tcgetattr(&stdin).ok()?;
let mut raw = saved.clone();
Expand Down
2 changes: 1 addition & 1 deletion crates/isb-daemon/src/daemon/kube/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ impl Run {

/// Which replica of an app a call means: the one at `slot`, the one named
/// `name`, else a running one, preferring those in rotation and healthy.
pub(super) fn pick_replica<'a>(
pub(in crate::daemon) fn pick_replica<'a>(
app: &str,
replicas: &'a [InstanceStatus],
slot: Option<u32>,
Expand Down
1 change: 1 addition & 0 deletions crates/isb-daemon/src/daemon/kube/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::stack::controller::ServiceStatus;

#[cfg(test)]
use self::{exec::*, files::*, look::*};
pub(super) use exec::pick_replica;
pub(super) use look::{SINCE_NOTE, read_lines, since_cutoff, window_logs};

/// Tools that only read.
Expand Down
96 changes: 55 additions & 41 deletions crates/isb-daemon/src/daemon/terminal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! The web terminal's other end: a login shell in one of an app's replicas,
//! for [`crate::server::terminal`]; or a login shell in any instance of the
//! org the caller may exec into (a sandbox, a replica). Nothing outside the
//! org is reachable this way.
//! The web terminal's other end: a login shell, or a command (`isb ...
//! exec -t`), in one of an app's or a stack service's replicas, for
//! [`crate::server::terminal`]; or in any instance of the org the caller
//! may exec into (a sandbox, a replica). Nothing outside the org is
//! reachable this way.

use std::sync::Arc;
use std::time::Duration;
Expand All @@ -24,6 +25,9 @@ struct ExecPty {
ctl: ExecController,
done: bool,
instance: String,
/// What runs, for the message when it cannot: "the shell", or the
/// command's name.
what: String,
/// Counts the session (a sandbox's activity: the reaper leaves it
/// alone) until it closes.
guard: Option<super::sandboxes::SessionGuard>,
Expand Down Expand Up @@ -52,7 +56,8 @@ impl Pty for ExecPty {
Err(Err(e)) => {
self.done = true;
PtyOutput::Failed(format!(
"the shell could not run: {e}. Images built FROM scratch have no /bin/sh"
"{} could not run: {e}. Images built FROM scratch have no /bin/sh",
self.what
))
}
}
Expand Down Expand Up @@ -86,70 +91,79 @@ pub(super) fn terminal(d: Arc<Daemon>) -> Terminal {
info.status.to_lowercase()
)));
}
let mut pty = shell(&oc, name, t)?;
let mut pty = open(&oc, name, t)?;
pty.guard = Some(d.sandboxes.session(&org.incus_project(), name));
return Ok(pty);
}
let app = d.apps.get(org, &t.app)?;
let stack = crate::stack::qualified(org, &app.spec.stack()?);
// An app is the service of its own stack; a stack names its own.
let (stack, service) = match &t.stack {
Some(s) => (crate::stack::qualified(org, s), t.app.clone()),
None => {
let app = d.apps.get(org, &t.app)?;
(
crate::stack::qualified(org, &app.spec.stack()?),
t.app.clone(),
)
}
};
let what = t.target();
let st = d
.ctl
.status(&stack)
.map_err(|_| Error::NotFound(format!("{} is not deployed", t.app)))?;
.map_err(|_| Error::NotFound(format!("{what} is not deployed")))?;
let svc = st
.services
.iter()
.find(|s| s.service == t.app)
.ok_or_else(|| Error::NotFound(format!("{} is not deployed", t.app)))?;
let inst = match t.slot {
Some(n) => svc
.instances
.iter()
.find(|i| i.slot == n)
.ok_or_else(|| Error::NotFound(format!("{} has no replica {n}", t.app)))?,
None => svc
.instances
.iter()
.filter(|i| i.status == "Running")
.min_by_key(|i| (!i.in_rotation, i.slot))
.ok_or_else(|| Error::NotFound(format!("{} has no running replica", t.app)))?,
.find(|s| s.service == service)
.ok_or_else(|| Error::NotFound(format!("{what} is not deployed")))?;
let inst = super::kube::pick_replica(&what, &svc.instances, t.slot, None)?;
let pty = open(&oc, &inst.name, t)?;
let opened = match &t.command {
Some(_) => "exec -t",
None => "terminal",
};
if inst.status != "Running" {
return Err(Error::invalid(format!(
"replica {} is {}, not running",
inst.slot,
inst.status.to_lowercase()
)));
}
let pty = shell(&oc, &inst.name, t)?;
d.ctl.service_event(
"info",
&stack,
&t.app,
format!("terminal opened on replica {} by {c}", inst.slot),
&service,
format!("{opened} opened on replica {} by {c}", inst.slot),
);
Ok(pty)
},
)
}

/// A login shell in `instance`, on a pseudo-terminal of the asked size.
fn shell(oc: &crate::client::Client, instance: &str, t: &TermRequest) -> Result<Box<ExecPty>> {
/// The terminal's program in `instance`, on a pseudo-terminal of the asked
/// size: its command (as the exec tools would run it), else a login shell.
fn open(oc: &crate::client::Client, instance: &str, t: &TermRequest) -> Result<Box<ExecPty>> {
let sb = Sandbox::get(oc, instance)?;
let mut opts = ExecOptions::default()
.tty(true)
.stdin(Stdin::Piped)
.env("TERM", "xterm-256color");
let mut opts = ExecOptions::default().tty(true).stdin(Stdin::Piped);
opts.width = Some(t.cols);
opts.height = Some(t.rows);
let (argv, what) = match &t.command {
Some(cmd) => {
opts.user = cmd.user.clone();
opts.cwd = cmd.cwd.clone();
opts.env = cmd.env.clone();
(cmd.argv.clone(), format!("{:?}", cmd.argv[0]))
}
None => (
["/bin/sh", "-c", SHELL].map(String::from).to_vec(),
"the shell".to_string(),
),
};
opts.env
.entry("TERM".into())
.or_insert_with(|| "xterm-256color".into());
let stream = sb
.exec_stream(["/bin/sh", "-c", SHELL], opts)
.map_err(|e| Error::invalid(format!("cannot start a shell in {instance}: {e}")))?;
.exec_stream(argv, opts)
.map_err(|e| Error::invalid(format!("cannot start {what} in {instance}: {e}")))?;
Ok(Box::new(ExecPty {
ctl: stream.controller(),
stream,
done: false,
instance: instance.to_string(),
what,
guard: None,
}))
}
Loading
Loading