Skip to content

Commit 61c560c

Browse files
committed
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
1 parent e6a359e commit 61c560c

5 files changed

Lines changed: 139 additions & 56 deletions

File tree

crates/intar-ui/src/app.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,8 +1009,8 @@ impl App {
10091009
fn draw_overlays(&self, f: &mut ratatui::Frame, area: Rect) {
10101010
if self.flags.show_confirm_reset {
10111011
let dialog = ConfirmDialog {
1012-
title: "Reset Scenario",
1013-
message: "Reset scenario to initial state?\nAll progress will be lost.",
1012+
title: "Restart Scenario",
1013+
message: "Restart scenario from the initial state?\nAll progress will be lost.",
10141014
theme: &self.theme,
10151015
};
10161016
f.render_widget(dialog, area);

crates/intar-ui/src/widgets.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ impl CompletedScreen<'_> {
169169
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
170170
let block = Block::default()
171171
.borders(Borders::ALL)
172-
.padding(Padding::uniform(1))
173-
.border_type(BorderType::Plain)
172+
.border_type(BorderType::Thick)
174173
.border_style(Style::default().fg(self.theme.border))
175174
.style(Style::default().bg(self.theme.surface));
176175

@@ -190,23 +189,30 @@ impl CompletedScreen<'_> {
190189
.bold()
191190
};
192191

193-
let spans = vec![
194-
Span::styled(" Q ", key_style),
195-
Span::raw(" quit "),
196-
Span::styled(" R ", key_style),
197-
Span::raw(" reset "),
198-
Span::styled(" T ", key_style),
199-
Span::raw(" theme"),
192+
let keys = vec![
193+
("?", "Help"),
194+
("R", "Restart"),
195+
("T", "Theme"),
196+
("Q", "Quit"),
200197
];
201198

199+
let mut spans = Vec::new();
200+
for (key, desc) in keys {
201+
spans.push(Span::styled(format!(" {key} "), key_style));
202+
spans.push(Span::styled(
203+
format!(" {desc} "),
204+
Style::default().fg(self.theme.dim),
205+
));
206+
spans.push(Span::raw(" "));
207+
}
208+
209+
let content_area = Layout::vertical([Constraint::Length(1)])
210+
.flex(ratatui::layout::Flex::Center)
211+
.split(inner)[0];
212+
202213
Paragraph::new(Line::from(spans))
203-
.alignment(Alignment::Center)
204-
.style(
205-
Style::default()
206-
.fg(self.theme.secondary)
207-
.bg(self.theme.surface),
208-
)
209-
.render(inner, buf);
214+
.alignment(Alignment::Left)
215+
.render(content_area, buf);
210216
}
211217
}
212218

@@ -670,7 +676,7 @@ impl ScenarioTreeScreen<'_> {
670676
("PGUP/PGDN", "Scroll"),
671677
("?", "Help"),
672678
("T", "Theme"),
673-
("R", "Reset"),
679+
("R", "Restart"),
674680
("Q", "Quit"),
675681
];
676682

@@ -928,7 +934,7 @@ impl Widget for HelpOverlay<'_> {
928934
]),
929935
Line::from(vec![
930936
Span::styled(" R ", key_style),
931-
Span::raw(" Reset scenario"),
937+
Span::raw(" Restart scenario"),
932938
]),
933939
Line::from(vec![
934940
Span::styled(" T ", key_style),

crates/intar-vm/src/vm_steps.rs

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,71 +21,109 @@ pub fn apply_vm_steps_to_cloud_init(
2121

2222
for step in steps {
2323
let step_slug = slugify(&step.name);
24-
let script_path = format!("/usr/local/bin/intar-step-{vm_slug}-{step_slug}.sh");
25-
let script = render_step_script(&vm_slug, &step_slug, step)?;
24+
let hidden = is_hidden_step(step);
25+
let script_path = if hidden {
26+
format!("/run/intar-step-{vm_slug}-{step_slug}.sh")
27+
} else {
28+
format!("/usr/local/bin/intar-step-{vm_slug}-{step_slug}.sh")
29+
};
30+
let script = render_step_script(&vm_slug, &step_slug, step, hidden)?;
2631

2732
config.write_files.push(WriteFile {
2833
path: script_path.clone(),
2934
content: script,
3035
permissions: Some("0755".into()),
3136
});
3237

33-
append_runcmd_line(
34-
&mut runcmd,
35-
&format!("cloud-init-per once intar-step-{vm_slug}-{step_slug} {script_path}"),
36-
);
38+
if hidden {
39+
append_runcmd_line(&mut runcmd, &format!("bash {script_path}"));
40+
} else {
41+
append_runcmd_line(
42+
&mut runcmd,
43+
&format!("cloud-init-per once intar-step-{vm_slug}-{step_slug} {script_path}"),
44+
);
45+
}
3746
}
3847

3948
config.runcmd = Some(runcmd);
4049
Ok(())
4150
}
4251

43-
fn render_step_script(vm_slug: &str, step_slug: &str, step: &VmStep) -> Result<String, VmError> {
52+
fn render_step_script(
53+
vm_slug: &str,
54+
step_slug: &str,
55+
step: &VmStep,
56+
hidden: bool,
57+
) -> Result<String, VmError> {
4458
let mut script = String::new();
4559

46-
render_step_header(&mut script, vm_slug, step_slug)?;
60+
render_step_header(&mut script, vm_slug, step_slug, hidden)?;
4761

4862
for (idx, action) in step.actions.iter().enumerate() {
4963
render_action(&mut script, step_slug, idx, action)?;
5064
}
5165

52-
render_step_footer(&mut script, vm_slug, step_slug)?;
66+
render_step_footer(&mut script, vm_slug, step_slug, hidden)?;
5367

5468
Ok(script)
5569
}
5670

57-
fn render_step_header(script: &mut String, vm_slug: &str, step_slug: &str) -> Result<(), VmError> {
71+
fn render_step_header(
72+
script: &mut String,
73+
vm_slug: &str,
74+
step_slug: &str,
75+
hidden: bool,
76+
) -> Result<(), VmError> {
5877
writeln!(script, "#!/usr/bin/env bash")
5978
.map_err(|_| VmError::CloudInit("format error".into()))?;
6079
writeln!(script, "set -euo pipefail").map_err(|_| VmError::CloudInit("format error".into()))?;
6180

62-
writeln!(script, "LOG_DIR=/var/log/intar")
81+
if hidden {
82+
writeln!(script, "trap 'rm -f -- \"$0\"' EXIT")
83+
.map_err(|_| VmError::CloudInit("format error".into()))?;
84+
writeln!(script, "exec >/dev/null 2>&1")
85+
.map_err(|_| VmError::CloudInit("format error".into()))?;
86+
} else {
87+
writeln!(script, "LOG_DIR=/var/log/intar")
88+
.map_err(|_| VmError::CloudInit("format error".into()))?;
89+
writeln!(script, "mkdir -p \"$LOG_DIR\"")
90+
.map_err(|_| VmError::CloudInit("format error".into()))?;
91+
writeln!(
92+
script,
93+
"exec >\"$LOG_DIR/step-{vm_slug}-{step_slug}.log\" 2>&1"
94+
)
6395
.map_err(|_| VmError::CloudInit("format error".into()))?;
64-
writeln!(script, "mkdir -p \"$LOG_DIR\"")
96+
writeln!(
97+
script,
98+
"echo \"[intar] step {vm_slug}/{step_slug} starting\""
99+
)
65100
.map_err(|_| VmError::CloudInit("format error".into()))?;
66-
writeln!(
67-
script,
68-
"exec >\"$LOG_DIR/step-{vm_slug}-{step_slug}.log\" 2>&1"
69-
)
70-
.map_err(|_| VmError::CloudInit("format error".into()))?;
71-
writeln!(
72-
script,
73-
"echo \"[intar] step {vm_slug}/{step_slug} starting\""
74-
)
75-
.map_err(|_| VmError::CloudInit("format error".into()))?;
101+
}
76102

77103
Ok(())
78104
}
79105

80-
fn render_step_footer(script: &mut String, vm_slug: &str, step_slug: &str) -> Result<(), VmError> {
81-
writeln!(
82-
script,
83-
"echo \"[intar] step {vm_slug}/{step_slug} complete\""
84-
)
85-
.map_err(|_| VmError::CloudInit("format error".into()))?;
106+
fn render_step_footer(
107+
script: &mut String,
108+
vm_slug: &str,
109+
step_slug: &str,
110+
hidden: bool,
111+
) -> Result<(), VmError> {
112+
if !hidden {
113+
writeln!(
114+
script,
115+
"echo \"[intar] step {vm_slug}/{step_slug} complete\""
116+
)
117+
.map_err(|_| VmError::CloudInit("format error".into()))?;
118+
}
86119
Ok(())
87120
}
88121

122+
fn is_hidden_step(step: &VmStep) -> bool {
123+
let name = step.name.to_lowercase();
124+
name.starts_with("break") || name.contains("break-") || name.contains("break_")
125+
}
126+
89127
fn render_action(
90128
script: &mut String,
91129
step_slug: &str,
@@ -497,16 +535,16 @@ mod tests {
497535

498536
let runcmd = config.runcmd.as_deref().unwrap();
499537
assert!(runcmd.contains("echo pre"));
500-
assert!(runcmd.contains(
501-
"cloud-init-per once intar-step-web-break-nginx /usr/local/bin/intar-step-web-break-nginx.sh"
502-
));
538+
assert!(runcmd.contains("bash /run/intar-step-web-break-nginx.sh"));
503539

504540
let script = config
505541
.write_files
506542
.iter()
507-
.find(|f| f.path == "/usr/local/bin/intar-step-web-break-nginx.sh")
543+
.find(|f| f.path == "/run/intar-step-web-break-nginx.sh")
508544
.map(|f| f.content.as_str())
509545
.unwrap();
546+
assert!(script.contains("trap 'rm -f -- \"$0\"' EXIT"));
547+
assert!(script.contains("exec >/dev/null 2>&1"));
510548
assert!(script.contains("systemctl stop 'nginx'"));
511549
assert!(script.contains("rm -f -- '/etc/nginx/sites-enabled/default'"));
512550
assert!(script.contains("export KUBECONFIG='/etc/rancher/k3s/k3s.yaml'"));

scenarios/k3s-ha.hcl

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ export K3S_TOKEN="intar-cluster-token"
9292
9393
ip link set "$CLUSTER_IF" up || true
9494
95+
mkdir -p /etc/rancher/k3s
96+
cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF'
97+
mirrors:
98+
docker.io:
99+
registry.k8s.io:
100+
REGISTRY_EOF
101+
95102
# Most Ubuntu cloud images ship curl; install only if missing to avoid slow apt runs.
96103
if ! command -v curl >/dev/null 2>&1; then
97104
export DEBIAN_FRONTEND=noninteractive
@@ -108,7 +115,7 @@ for _ in $(seq 1 30); do
108115
done
109116
[ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; }
110117
111-
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"
118+
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"
112119
113120
if [ "$HOSTNAME" = "k3s-1" ]; then
114121
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh -
@@ -185,6 +192,13 @@ export K3S_TOKEN="intar-cluster-token"
185192
186193
ip link set "$CLUSTER_IF" up || true
187194
195+
mkdir -p /etc/rancher/k3s
196+
cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF'
197+
mirrors:
198+
docker.io:
199+
registry.k8s.io:
200+
REGISTRY_EOF
201+
188202
if ! command -v curl >/dev/null 2>&1; then
189203
export DEBIAN_FRONTEND=noninteractive
190204
apt-get update -qq
@@ -199,7 +213,7 @@ for _ in $(seq 1 30); do
199213
done
200214
[ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; }
201215
202-
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"
216+
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"
203217
204218
if [ "$HOSTNAME" = "k3s-1" ]; then
205219
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh -
@@ -239,6 +253,13 @@ export K3S_TOKEN="intar-cluster-token"
239253
240254
ip link set "$CLUSTER_IF" up || true
241255
256+
mkdir -p /etc/rancher/k3s
257+
cat > /etc/rancher/k3s/registries.yaml <<'REGISTRY_EOF'
258+
mirrors:
259+
docker.io:
260+
registry.k8s.io:
261+
REGISTRY_EOF
262+
242263
if ! command -v curl >/dev/null 2>&1; then
243264
export DEBIAN_FRONTEND=noninteractive
244265
apt-get update -qq
@@ -253,7 +274,7 @@ for _ in $(seq 1 30); do
253274
done
254275
[ -n "$NODE_IP" ] || { echo "No IPv4 on $CLUSTER_IF"; exit 1; }
255276
256-
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"
277+
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"
257278
258279
if [ "$HOSTNAME" = "k3s-1" ]; then
259280
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="$COMMON_ARGS --cluster-init" sh -

scripts/e2e-nginx.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ fi
158158
echo "Waiting for break-nginx step..."
159159
break_ready=""
160160
for _ in $(seq 1 "$max_attempts"); do
161-
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
161+
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
162162
break_ready="yes"
163163
break
164164
fi
@@ -173,6 +173,24 @@ if [[ -z "$break_ready" ]]; then
173173
exit 1
174174
fi
175175

176+
echo "Checking break traces..."
177+
trace_ok=""
178+
for _ in $(seq 1 "$max_attempts"); do
179+
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
180+
trace_ok="yes"
181+
break
182+
fi
183+
sleep "$poll_secs"
184+
done
185+
186+
if [[ -z "$trace_ok" ]]; then
187+
echo "Break traces were detected on the VM."
188+
"$INTAR_BIN" logs --vm webserver --log-type console || true
189+
"${TMUX_CMD[@]}" capture-pane -pt "$SESSION_NAME" -S -2000 > "$UI_LOG" 2>/dev/null || true
190+
snapshot_logs
191+
exit 1
192+
fi
193+
176194
echo "Applying nginx fix..."
177195
fix_ok=""
178196
for _ in $(seq 1 "$max_attempts"); do

0 commit comments

Comments
 (0)