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
4 changes: 2 additions & 2 deletions crates/intar-ui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
42 changes: 24 additions & 18 deletions crates/intar-ui/src/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -670,7 +676,7 @@ impl ScenarioTreeScreen<'_> {
("PGUP/PGDN", "Scroll"),
("?", "Help"),
("T", "Theme"),
("R", "Reset"),
("R", "Restart"),
("Q", "Quit"),
];

Expand Down Expand Up @@ -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),
Expand Down
102 changes: 70 additions & 32 deletions crates/intar-vm/src/vm_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,71 +21,109 @@ 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(),
content: script,
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<String, VmError> {
fn render_step_script(
vm_slug: &str,
step_slug: &str,
step: &VmStep,
hidden: bool,
) -> Result<String, VmError> {
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,
Expand Down Expand Up @@ -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'"));
Expand Down
27 changes: 24 additions & 3 deletions scenarios/k3s-ha.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 -
Expand Down Expand Up @@ -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
Expand All @@ -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 -
Expand Down Expand Up @@ -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
Expand All @@ -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 -
Expand Down
20 changes: 19 additions & 1 deletion scripts/e2e-nginx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down