provisioning/docs/book/development/CTRL-C_IMPLEMENTATION_NOTES.html
Jesús Pérez 6a59d34bb1
chore: update provisioning configuration and documentation
Update configuration files, templates, and internal documentation
for the provisioning repository system.

Configuration Updates:
- KMS configuration modernization
- Plugin system settings
- Service port mappings
- Test cluster topologies
- Installation configuration examples
- VM configuration defaults
- Cedar authorization policies

Documentation Updates:
- Library module documentation
- Extension API guides
- AI system documentation
- Service management guides
- Test environment setup
- Plugin usage guides
- Validator configuration documentation

All changes are backward compatible.
2025-12-11 21:50:42 +00:00

475 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="en" class="ayu sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Ctrl-C Implementation Notes - Provisioning Platform Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Complete documentation for the Provisioning Platform - Infrastructure automation with Nushell, KCL, and Rust">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="highlight-css" href="../highlight.css">
<link rel="stylesheet" id="tomorrow-night-css" href="../tomorrow-night.css">
<link rel="stylesheet" id="ayu-highlight-css" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- Provide site root and default themes to javascript -->
<script>
const path_to_root = "../";
const default_light_theme = "ayu";
const default_dark_theme = "navy";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="mdbook-help-container">
<div id="mdbook-help-popup">
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
<div>
<p>Press <kbd></kbd> or <kbd></kbd> to navigate between chapters</p>
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
<p>Press <kbd>?</kbd> to show this help</p>
<p>Press <kbd>Esc</kbd> to hide this help</p>
</div>
</div>
</div>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
let theme = localStorage.getItem('mdbook-theme');
let sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
let theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('ayu')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
let sidebar = null;
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Provisioning Platform Documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/provisioning/provisioning-platform" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/provisioning/provisioning-platform/edit/main/provisioning/docs/src/development/CTRL-C_IMPLEMENTATION_NOTES.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="ctrl-c-handling-implementation-notes"><a class="header" href="#ctrl-c-handling-implementation-notes">CTRL-C Handling Implementation Notes</a></h1>
<h2 id="overview"><a class="header" href="#overview">Overview</a></h2>
<p>Implemented graceful CTRL-C handling for sudo password prompts during server creation/generation operations.</p>
<h2 id="problem-statement"><a class="header" href="#problem-statement">Problem Statement</a></h2>
<p>When <code>fix_local_hosts: true</code> is set, the provisioning tool requires sudo access to modify <code>/etc/hosts</code> and SSH config. When a user cancels the sudo password prompt (no password, wrong password, timeout), the system would:</p>
<ol>
<li>Exit with code 1 (sudo failed)</li>
<li>Propagate null values up the call stack</li>
<li>Show cryptic Nushell errors about pipeline failures</li>
<li>Leave the operation in an inconsistent state</li>
</ol>
<p><strong>Important Unix Limitation</strong>: Pressing CTRL-C at the sudo password prompt sends SIGINT to the entire process group, interrupting Nushell before exit code handling can occur. This <strong>cannot be caught</strong> and is expected Unix behavior.</p>
<h2 id="solution-architecture"><a class="header" href="#solution-architecture">Solution Architecture</a></h2>
<h3 id="key-principle-return-values-not-exit-codes"><a class="header" href="#key-principle-return-values-not-exit-codes">Key Principle: Return Values, Not Exit Codes</a></h3>
<p>Instead of using <code>exit 130</code> which kills the entire process, we use <strong>return values</strong> to signal cancellation and let each layer of the call stack handle it gracefully.</p>
<h3 id="three-layer-approach"><a class="header" href="#three-layer-approach">Three-Layer Approach</a></h3>
<ol>
<li>
<p><strong>Detection Layer</strong> (ssh.nu helper functions)</p>
<ul>
<li>Detects sudo cancellation via exit code + stderr</li>
<li>Returns <code>false</code> instead of calling <code>exit</code></li>
</ul>
</li>
<li>
<p><strong>Propagation Layer</strong> (ssh.nu core functions)</p>
<ul>
<li><code>on_server_ssh()</code>: Returns <code>false</code> on cancellation</li>
<li><code>server_ssh()</code>: Uses <code>reduce</code> to propagate failures</li>
</ul>
</li>
<li>
<p><strong>Handling Layer</strong> (create.nu, generate.nu)</p>
<ul>
<li>Checks return values</li>
<li>Displays user-friendly messages</li>
<li>Returns <code>false</code> to caller</li>
</ul>
</li>
</ol>
<h2 id="implementation-details"><a class="header" href="#implementation-details">Implementation Details</a></h2>
<h3 id="1-helper-functions-sshnu11-32"><a class="header" href="#1-helper-functions-sshnu11-32">1. Helper Functions (ssh.nu:11-32)</a></h3>
<pre><code class="language-nushell">def check_sudo_cached []: nothing -&gt; bool {
let result = (do --ignore-errors { ^sudo -n true } | complete)
$result.exit_code == 0
}
def run_sudo_with_interrupt_check [
command: closure
operation_name: string
]: nothing -&gt; bool {
let result = (do --ignore-errors { do $command } | complete)
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
print "\n⚠ Operation cancelled - sudo password required but not provided"
print " Run 'sudo -v' first to cache credentials, or run without --fix-local-hosts"
return false # Signal cancellation
} else if $result.exit_code != 0 and $result.exit_code != 1 {
error make {msg: $"($operation_name) failed: ($result.stderr)"}
}
true
}
</code></pre>
<p><strong>Design Decision</strong>: Return <code>bool</code> instead of throwing error or calling <code>exit</code>. This allows the caller to decide how to handle cancellation.</p>
<h3 id="2-pre-emptive-warning-sshnu155-160"><a class="header" href="#2-pre-emptive-warning-sshnu155-160">2. Pre-emptive Warning (ssh.nu:155-160)</a></h3>
<pre><code class="language-nushell">if $server.fix_local_hosts and not (check_sudo_cached) {
print "\n⚠ Sudo access required for --fix-local-hosts"
print " You will be prompted for your password, or press CTRL-C to cancel"
print " Tip: Run 'sudo -v' beforehand to cache credentials\n"
}
</code></pre>
<p><strong>Design Decision</strong>: Warn users upfront so theyre not surprised by the password prompt.</p>
<h3 id="3-ctrl-c-detection-sshnu171-199"><a class="header" href="#3-ctrl-c-detection-sshnu171-199">3. CTRL-C Detection (ssh.nu:171-199)</a></h3>
<p>All sudo commands wrapped with detection:</p>
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo &lt;command&gt; } | complete)
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
print "\n⚠ Operation cancelled"
return false
}
</code></pre>
<p><strong>Design Decision</strong>: Use <code>do --ignore-errors</code> + <code>complete</code> to capture both exit code and stderr without throwing exceptions.</p>
<h3 id="4-state-accumulation-pattern-sshnu122-129"><a class="header" href="#4-state-accumulation-pattern-sshnu122-129">4. State Accumulation Pattern (ssh.nu:122-129)</a></h3>
<p>Using Nushells <code>reduce</code> instead of mutable variables:</p>
<pre><code class="language-nushell">let all_succeeded = ($settings.data.servers | reduce -f true { |server, acc|
if $text_match == null or $server.hostname == $text_match {
let result = (on_server_ssh $settings $server $ip_type $request_from $run)
$acc and $result
} else {
$acc
}
})
</code></pre>
<p><strong>Design Decision</strong>: Nushell doesnt allow mutable variable capture in closures. Use <code>reduce</code> for accumulating boolean state across iterations.</p>
<h3 id="5-caller-handling-createnu262-266-generatenu269-273"><a class="header" href="#5-caller-handling-createnu262-266-generatenu269-273">5. Caller Handling (create.nu:262-266, generate.nu:269-273)</a></h3>
<pre><code class="language-nushell">let ssh_result = (on_server_ssh $settings $server "pub" "create" false)
if not $ssh_result {
_print "\n✗ Server creation cancelled"
return false
}
</code></pre>
<p><strong>Design Decision</strong>: Check return value and provide context-specific message before returning.</p>
<h2 id="error-flow-diagram"><a class="header" href="#error-flow-diagram">Error Flow Diagram</a></h2>
<pre><code>User presses CTRL-C during password prompt
sudo exits with code 1, stderr: "password is required"
do --ignore-errors captures exit code &amp; stderr
Detection logic identifies cancellation
Print user-friendly message
Return false (not exit!)
on_server_ssh returns false
Caller (create.nu/generate.nu) checks return value
Print "✗ Server creation cancelled"
Return false to settings.nu
settings.nu handles false gracefully (no append)
Clean exit, no cryptic errors
</code></pre>
<h2 id="nushell-idioms-used"><a class="header" href="#nushell-idioms-used">Nushell Idioms Used</a></h2>
<h3 id="1-do---ignore-errors--complete"><a class="header" href="#1-do---ignore-errors--complete">1. <code>do --ignore-errors</code> + <code>complete</code></a></h3>
<p>Captures both stdout, stderr, and exit code without throwing:</p>
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo command } | complete)
# result = { stdout: "...", stderr: "...", exit_code: 1 }
</code></pre>
<h3 id="2-reduce-for-accumulation"><a class="header" href="#2-reduce-for-accumulation">2. <code>reduce</code> for Accumulation</a></h3>
<p>Instead of mutable variables in loops:</p>
<pre><code class="language-nushell"># ❌ BAD - mutable capture in closure
mut all_succeeded = true
$servers | each { |s|
$all_succeeded = false # Error: capture of mutable variable
}
# ✅ GOOD - reduce with accumulator
let all_succeeded = ($servers | reduce -f true { |s, acc|
$acc and (check_server $s)
})
</code></pre>
<h3 id="3-early-returns-for-error-handling"><a class="header" href="#3-early-returns-for-error-handling">3. Early Returns for Error Handling</a></h3>
<pre><code class="language-nushell">if not $condition {
print "Error message"
return false
}
# Continue with happy path
</code></pre>
<h2 id="testing-scenarios"><a class="header" href="#testing-scenarios">Testing Scenarios</a></h2>
<h3 id="scenario-1-ctrl-c-during-first-sudo-command"><a class="header" href="#scenario-1-ctrl-c-during-first-sudo-command">Scenario 1: CTRL-C During First Sudo Command</a></h3>
<pre><code class="language-bash">provisioning -c server create
# Password: [CTRL-C]
# Expected Output:
# ⚠ Operation cancelled - sudo password required but not provided
# Run 'sudo -v' first to cache credentials
# ✗ Server creation cancelled
</code></pre>
<h3 id="scenario-2-pre-cached-credentials"><a class="header" href="#scenario-2-pre-cached-credentials">Scenario 2: Pre-cached Credentials</a></h3>
<pre><code class="language-bash">sudo -v
provisioning -c server create
# Expected: No password prompt, smooth operation
</code></pre>
<h3 id="scenario-3-wrong-password-3-times"><a class="header" href="#scenario-3-wrong-password-3-times">Scenario 3: Wrong Password 3 Times</a></h3>
<pre><code class="language-bash">provisioning -c server create
# Password: [wrong]
# Password: [wrong]
# Password: [wrong]
# Expected: Same as CTRL-C (treated as cancellation)
</code></pre>
<h3 id="scenario-4-multiple-servers-cancel-on-second"><a class="header" href="#scenario-4-multiple-servers-cancel-on-second">Scenario 4: Multiple Servers, Cancel on Second</a></h3>
<pre><code class="language-bash"># If creating multiple servers and CTRL-C on second:
# - First server completes successfully
# - Second server shows cancellation message
# - Operation stops, doesn't proceed to third
</code></pre>
<h2 id="maintenance-notes"><a class="header" href="#maintenance-notes">Maintenance Notes</a></h2>
<h3 id="adding-new-sudo-commands"><a class="header" href="#adding-new-sudo-commands">Adding New Sudo Commands</a></h3>
<p>When adding new sudo commands to the codebase:</p>
<ol>
<li>Wrap with <code>do --ignore-errors</code> + <code>complete</code></li>
<li>Check for exit code 1 + “password is required”</li>
<li>Return <code>false</code> on cancellation</li>
<li>Let caller handle the <code>false</code> return value</li>
</ol>
<p>Example template:</p>
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo new-command } | complete)
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
print "\n⚠ Operation cancelled - sudo password required"
return false
}
</code></pre>
<h3 id="common-pitfalls"><a class="header" href="#common-pitfalls">Common Pitfalls</a></h3>
<ol>
<li><strong>Dont use <code>exit</code></strong>: It kills the entire process</li>
<li><strong>Dont use mutable variables in closures</strong>: Use <code>reduce</code> instead</li>
<li><strong>Dont ignore return values</strong>: Always check and propagate</li>
<li><strong>Dont forget the pre-check warning</strong>: Users should know sudo is needed</li>
</ol>
<h2 id="future-improvements"><a class="header" href="#future-improvements">Future Improvements</a></h2>
<ol>
<li><strong>Sudo Credential Manager</strong>: Optionally use a credential manager (keychain, etc.)</li>
<li><strong>Sudo-less Mode</strong>: Alternative implementation that doesnt require root</li>
<li><strong>Timeout Handling</strong>: Detect when sudo times out waiting for password</li>
<li><strong>Multiple Password Attempts</strong>: Distinguish between CTRL-C and wrong password</li>
</ol>
<h2 id="references"><a class="header" href="#references">References</a></h2>
<ul>
<li>Nushell <code>complete</code> command: https://www.nushell.sh/commands/docs/complete.html</li>
<li>Nushell <code>reduce</code> command: https://www.nushell.sh/commands/docs/reduce.html</li>
<li>Sudo exit codes: man sudo (exit code 1 = authentication failure)</li>
<li>POSIX signal conventions: SIGINT (CTRL-C) = 130</li>
</ul>
<h2 id="related-files"><a class="header" href="#related-files">Related Files</a></h2>
<ul>
<li><code>provisioning/core/nulib/servers/ssh.nu</code> - Core implementation</li>
<li><code>provisioning/core/nulib/servers/create.nu</code> - Calls on_server_ssh</li>
<li><code>provisioning/core/nulib/servers/generate.nu</code> - Calls on_server_ssh</li>
<li><code>docs/troubleshooting/CTRL-C_SUDO_HANDLING.md</code> - User-facing docs</li>
<li><code>docs/quick-reference/SUDO_PASSWORD_HANDLING.md</code> - Quick reference</li>
</ul>
<h2 id="changelog"><a class="header" href="#changelog">Changelog</a></h2>
<ul>
<li><strong>2025-01-XX</strong>: Initial implementation with return values (v2)</li>
<li><strong>2025-01-XX</strong>: Fixed mutable variable capture with <code>reduce</code> pattern</li>
<li><strong>2025-01-XX</strong>: First attempt with <code>exit 130</code> (reverted, caused process termination)</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../development/kcl/VALIDATION_EXECUTIVE_SUMMARY.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../guides/from-scratch.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../development/kcl/VALIDATION_EXECUTIVE_SUMMARY.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../guides/from-scratch.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>