From 61c560ca68b006001c3f031f2d6e409efdbc37be Mon Sep 17 00:00:00 2001 From: Stefan Ruzitschka <362487+icepuma@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:20:24 +0100 Subject: [PATCH] feat(vm): hide break traces and align completion footer Why: - break steps should leave no on-guest artifacts - completion footer should match main footer styling Impact: - break steps run from /run, self-delete, and suppress logs - completion footer now uses restart wording and aligned keys - k3s scenario uses embedded registry mirror - e2e harness validates no break traces - Tests: e2e-nginx.sh --- crates/intar-ui/src/app.rs | 4 +- crates/intar-ui/src/widgets.rs | 42 +++++++------ crates/intar-vm/src/vm_steps.rs | 102 ++++++++++++++++++++++---------- scenarios/k3s-ha.hcl | 27 ++++++++- scripts/e2e-nginx.sh | 20 ++++++- 5 files changed, 139 insertions(+), 56 deletions(-) diff --git a/crates/intar-ui/src/app.rs b/crates/intar-ui/src/app.rs index 43abaa9..fcfa183 100644 --- a/crates/intar-ui/src/app.rs +++ b/crates/intar-ui/src/app.rs @@ -1009,8 +1009,8 @@ impl App { fn draw_overlays(&self, f: &mut ratatui::Frame, area: Rect) { if self.flags.show_confirm_reset { let dialog = ConfirmDialog { - title: "Reset Scenario", - message: "Reset scenario to initial state?\nAll progress will be lost.", + title: "Restart Scenario", + message: "Restart scenario from the initial state?\nAll progress will be lost.", theme: &self.theme, }; f.render_widget(dialog, area); diff --git a/crates/intar-ui/src/widgets.rs b/crates/intar-ui/src/widgets.rs index 9361258..1334d76 100644 --- a/crates/intar-ui/src/widgets.rs +++ b/crates/intar-ui/src/widgets.rs @@ -169,8 +169,7 @@ impl CompletedScreen<'_> { fn render_footer(&self, area: Rect, buf: &mut Buffer) { let block = Block::default() .borders(Borders::ALL) - .padding(Padding::uniform(1)) - .border_type(BorderType::Plain) + .border_type(BorderType::Thick) .border_style(Style::default().fg(self.theme.border)) .style(Style::default().bg(self.theme.surface)); @@ -190,23 +189,30 @@ impl CompletedScreen<'_> { .bold() }; - let spans = vec![ - Span::styled(" Q ", key_style), - Span::raw(" quit "), - Span::styled(" R ", key_style), - Span::raw(" reset "), - Span::styled(" T ", key_style), - Span::raw(" theme"), + let keys = vec![ + ("?", "Help"), + ("R", "Restart"), + ("T", "Theme"), + ("Q", "Quit"), ]; + let mut spans = Vec::new(); + for (key, desc) in keys { + spans.push(Span::styled(format!(" {key} "), key_style)); + spans.push(Span::styled( + format!(" {desc} "), + Style::default().fg(self.theme.dim), + )); + spans.push(Span::raw(" ")); + } + + let content_area = Layout::vertical([Constraint::Length(1)]) + .flex(ratatui::layout::Flex::Center) + .split(inner)[0]; + Paragraph::new(Line::from(spans)) - .alignment(Alignment::Center) - .style( - Style::default() - .fg(self.theme.secondary) - .bg(self.theme.surface), - ) - .render(inner, buf); + .alignment(Alignment::Left) + .render(content_area, buf); } } @@ -670,7 +676,7 @@ impl ScenarioTreeScreen<'_> { ("PGUP/PGDN", "Scroll"), ("?", "Help"), ("T", "Theme"), - ("R", "Reset"), + ("R", "Restart"), ("Q", "Quit"), ]; @@ -928,7 +934,7 @@ impl Widget for HelpOverlay<'_> { ]), Line::from(vec![ Span::styled(" R ", key_style), - Span::raw(" Reset scenario"), + Span::raw(" Restart scenario"), ]), Line::from(vec![ Span::styled(" T ", key_style), diff --git a/crates/intar-vm/src/vm_steps.rs b/crates/intar-vm/src/vm_steps.rs index 68ee0b6..ec3ad6a 100644 --- a/crates/intar-vm/src/vm_steps.rs +++ b/crates/intar-vm/src/vm_steps.rs @@ -21,8 +21,13 @@ pub fn apply_vm_steps_to_cloud_init( for step in steps { let step_slug = slugify(&step.name); - let script_path = format!("/usr/local/bin/intar-step-{vm_slug}-{step_slug}.sh"); - let script = render_step_script(&vm_slug, &step_slug, step)?; + let hidden = is_hidden_step(step); + let script_path = if hidden { + format!("/run/intar-step-{vm_slug}-{step_slug}.sh") + } else { + format!("/usr/local/bin/intar-step-{vm_slug}-{step_slug}.sh") + }; + let script = render_step_script(&vm_slug, &step_slug, step, hidden)?; config.write_files.push(WriteFile { path: script_path.clone(), @@ -30,62 +35,95 @@ pub fn apply_vm_steps_to_cloud_init( permissions: Some("0755".into()), }); - append_runcmd_line( - &mut runcmd, - &format!("cloud-init-per once intar-step-{vm_slug}-{step_slug} {script_path}"), - ); + if hidden { + append_runcmd_line(&mut runcmd, &format!("bash {script_path}")); + } else { + append_runcmd_line( + &mut runcmd, + &format!("cloud-init-per once intar-step-{vm_slug}-{step_slug} {script_path}"), + ); + } } config.runcmd = Some(runcmd); Ok(()) } -fn render_step_script(vm_slug: &str, step_slug: &str, step: &VmStep) -> Result { +fn render_step_script( + vm_slug: &str, + step_slug: &str, + step: &VmStep, + hidden: bool, +) -> Result { let mut script = String::new(); - render_step_header(&mut script, vm_slug, step_slug)?; + render_step_header(&mut script, vm_slug, step_slug, hidden)?; for (idx, action) in step.actions.iter().enumerate() { render_action(&mut script, step_slug, idx, action)?; } - render_step_footer(&mut script, vm_slug, step_slug)?; + render_step_footer(&mut script, vm_slug, step_slug, hidden)?; Ok(script) } -fn render_step_header(script: &mut String, vm_slug: &str, step_slug: &str) -> Result<(), VmError> { +fn render_step_header( + script: &mut String, + vm_slug: &str, + step_slug: &str, + hidden: bool, +) -> Result<(), VmError> { writeln!(script, "#!/usr/bin/env bash") .map_err(|_| VmError::CloudInit("format error".into()))?; writeln!(script, "set -euo pipefail").map_err(|_| VmError::CloudInit("format error".into()))?; - writeln!(script, "LOG_DIR=/var/log/intar") + if hidden { + writeln!(script, "trap 'rm -f -- \"$0\"' EXIT") + .map_err(|_| VmError::CloudInit("format error".into()))?; + writeln!(script, "exec >/dev/null 2>&1") + .map_err(|_| VmError::CloudInit("format error".into()))?; + } else { + writeln!(script, "LOG_DIR=/var/log/intar") + .map_err(|_| VmError::CloudInit("format error".into()))?; + writeln!(script, "mkdir -p \"$LOG_DIR\"") + .map_err(|_| VmError::CloudInit("format error".into()))?; + writeln!( + script, + "exec >\"$LOG_DIR/step-{vm_slug}-{step_slug}.log\" 2>&1" + ) .map_err(|_| VmError::CloudInit("format error".into()))?; - writeln!(script, "mkdir -p \"$LOG_DIR\"") + writeln!( + script, + "echo \"[intar] step {vm_slug}/{step_slug} starting\"" + ) .map_err(|_| VmError::CloudInit("format error".into()))?; - writeln!( - script, - "exec >\"$LOG_DIR/step-{vm_slug}-{step_slug}.log\" 2>&1" - ) - .map_err(|_| VmError::CloudInit("format error".into()))?; - writeln!( - script, - "echo \"[intar] step {vm_slug}/{step_slug} starting\"" - ) - .map_err(|_| VmError::CloudInit("format error".into()))?; + } Ok(()) } -fn render_step_footer(script: &mut String, vm_slug: &str, step_slug: &str) -> Result<(), VmError> { - writeln!( - script, - "echo \"[intar] step {vm_slug}/{step_slug} complete\"" - ) - .map_err(|_| VmError::CloudInit("format error".into()))?; +fn render_step_footer( + script: &mut String, + vm_slug: &str, + step_slug: &str, + hidden: bool, +) -> Result<(), VmError> { + if !hidden { + writeln!( + script, + "echo \"[intar] step {vm_slug}/{step_slug} complete\"" + ) + .map_err(|_| VmError::CloudInit("format error".into()))?; + } Ok(()) } +fn is_hidden_step(step: &VmStep) -> bool { + let name = step.name.to_lowercase(); + name.starts_with("break") || name.contains("break-") || name.contains("break_") +} + fn render_action( script: &mut String, step_slug: &str, @@ -497,16 +535,16 @@ mod tests { let runcmd = config.runcmd.as_deref().unwrap(); assert!(runcmd.contains("echo pre")); - assert!(runcmd.contains( - "cloud-init-per once intar-step-web-break-nginx /usr/local/bin/intar-step-web-break-nginx.sh" - )); + assert!(runcmd.contains("bash /run/intar-step-web-break-nginx.sh")); let script = config .write_files .iter() - .find(|f| f.path == "/usr/local/bin/intar-step-web-break-nginx.sh") + .find(|f| f.path == "/run/intar-step-web-break-nginx.sh") .map(|f| f.content.as_str()) .unwrap(); + assert!(script.contains("trap 'rm -f -- \"$0\"' EXIT")); + assert!(script.contains("exec >/dev/null 2>&1")); assert!(script.contains("systemctl stop 'nginx'")); assert!(script.contains("rm -f -- '/etc/nginx/sites-enabled/default'")); assert!(script.contains("export KUBECONFIG='/etc/rancher/k3s/k3s.yaml'")); diff --git a/scenarios/k3s-ha.hcl b/scenarios/k3s-ha.hcl index 921ed85..1713a5f 100644 --- a/scenarios/k3s-ha.hcl +++ b/scenarios/k3s-ha.hcl @@ -92,6 +92,13 @@ export K3S_TOKEN="intar-cluster-token" ip link set "$CLUSTER_IF" up || true +mkdir -p /etc/rancher/k3s +cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF' +mirrors: + docker.io: + registry.k8s.io: +REGISTRY_EOF + # Most Ubuntu cloud images ship curl; install only if missing to avoid slow apt runs. if ! command -v curl >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive @@ -108,7 +115,7 @@ for _ in $(seq 1 30); do done [ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; } -COMMON_ARGS="server --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" +COMMON_ARGS="server --embedded-registry --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" if [ "$HOSTNAME" = "k3s-1" ]; then curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh - @@ -185,6 +192,13 @@ export K3S_TOKEN="intar-cluster-token" ip link set "$CLUSTER_IF" up || true +mkdir -p /etc/rancher/k3s +cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF' +mirrors: + docker.io: + registry.k8s.io: +REGISTRY_EOF + if ! command -v curl >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq @@ -199,7 +213,7 @@ for _ in $(seq 1 30); do done [ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; } -COMMON_ARGS="server --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" +COMMON_ARGS="server --embedded-registry --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" if [ "$HOSTNAME" = "k3s-1" ]; then curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh - @@ -239,6 +253,13 @@ export K3S_TOKEN="intar-cluster-token" ip link set "$CLUSTER_IF" up || true +mkdir -p /etc/rancher/k3s +cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF' +mirrors: + docker.io: + registry.k8s.io: +REGISTRY_EOF + if ! command -v curl >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq @@ -253,7 +274,7 @@ for _ in $(seq 1 30); do done [ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; } -COMMON_ARGS="server --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" +COMMON_ARGS="server --embedded-registry --flannel-iface $CLUSTER_IF --node-ip $NODE_IP --advertise-address $NODE_IP --tls-san $HOSTNAME --tls-san $HOSTNAME.intar --tls-san k3s-server --tls-san k3s-server.intar" if [ "$HOSTNAME" = "k3s-1" ]; then curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh - diff --git a/scripts/e2e-nginx.sh b/scripts/e2e-nginx.sh index 0bb30cf..fc05c84 100755 --- a/scripts/e2e-nginx.sh +++ b/scripts/e2e-nginx.sh @@ -158,7 +158,7 @@ fi echo "Waiting for break-nginx step..." break_ready="" for _ in $(seq 1 "$max_attempts"); do - if "$INTAR_BIN" ssh webserver --command "sudo -n test -f /var/log/intar/step-webserver-break-nginx.log && sudo -n grep -q \"step webserver/break-nginx complete\" /var/log/intar/step-webserver-break-nginx.log" >/dev/null 2>&1; then + if "$INTAR_BIN" ssh webserver --command 'sudo -n systemctl is-active --quiet nginx; svc=$?; test ! -f /etc/nginx/sites-enabled/default; file=$?; [ $svc -ne 0 ] && [ $file -eq 0 ]' >/dev/null 2>&1; then break_ready="yes" break fi @@ -173,6 +173,24 @@ if [[ -z "$break_ready" ]]; then exit 1 fi +echo "Checking break traces..." +trace_ok="" +for _ in $(seq 1 "$max_attempts"); do + if "$INTAR_BIN" ssh webserver --command "sudo -n test ! -f /var/log/intar/step-webserver-break-nginx.log && test ! -f /usr/local/bin/intar-step-webserver-break-nginx.sh && test ! -f /run/intar-step-webserver-break-nginx.sh" >/dev/null 2>&1; then + trace_ok="yes" + break + fi + sleep "$poll_secs" +done + +if [[ -z "$trace_ok" ]]; then + echo "Break traces were detected on the VM." + "$INTAR_BIN" logs --vm webserver --log-type console || true + "${TMUX_CMD[@]}" capture-pane -pt "$SESSION_NAME" -S -2000 > "$UI_LOG" 2>/dev/null || true + snapshot_logs + exit 1 +fi + echo "Applying nginx fix..." fix_ok="" for _ in $(seq 1 "$max_attempts"); do