feat(secret): added get_or_prompt functionality and debuggable ipxe chainloading boot file and some misc stuff
Some checks failed
Run Check Script / check (pull_request) Failing after 29s

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-09-03 12:09:44 -04:00
parent 160939de21
commit b765e9b7dc
10 changed files with 173 additions and 38 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ private_repos/
### Harmony ###
harmony.log
data/okd/installation_files*
### Helm ###
# Chart dependencies

1
Cargo.lock generated
View File

@ -2427,6 +2427,7 @@ dependencies = [
"harmony_secret_derive",
"http 1.3.1",
"infisical",
"inquire",
"lazy_static",
"log",
"pretty_assertions",

View File

@ -22,7 +22,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: String::from("opnsense-1"),
};
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
let config = SecretManager::get_or_prompt::<OPNSenseFirewallConfig>().await;
let config = config.unwrap();
let opnsense = Arc::new(

View File

@ -16,7 +16,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: String::from("opnsense-1"),
};
let config = SecretManager::get::<OPNSenseFirewallCredentials>().await;
let config = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>().await;
let config = config.unwrap();
let opnsense = Arc::new(

View File

@ -425,7 +425,8 @@ impl OKDSetup02BootstrapInterpret {
topology: &HAClusterTopology,
) -> Result<(), InterpretError> {
let okd_bin_path = PathBuf::from("./data/okd/bin");
let okd_installation_path_str = "./data/okd/installation_files";
let okd_installation_path_str =
format!("./data/okd/installation_files_{}", inventory.location.name);
let okd_images_path = &PathBuf::from("./data/okd/installer_image/");
let okd_installation_path = &PathBuf::from(okd_installation_path_str);
@ -450,8 +451,8 @@ impl OKDSetup02BootstrapInterpret {
);
}
let redhat_secret = SecretManager::get::<RedhatSecret>().await?;
let ssh_key = SecretManager::get::<SshKeyPair>().await?;
let redhat_secret = SecretManager::get_or_prompt::<RedhatSecret>().await?;
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await?;
let install_config_yaml = InstallConfigYaml {
cluster_name: &topology.get_cluster_name(),
@ -562,38 +563,14 @@ impl OKDSetup02BootstrapInterpret {
.interpret(inventory, topology)
.await?;
let run_command =
async |cmd: &str, args: Vec<&str>| -> Result<std::process::Output, InterpretError> {
let output = Command::new(cmd).args(&args).output().await.map_err(|e| {
InterpretError::new(format!("Failed to launch command {cmd} : {e}"))
})?;
let stdout = String::from_utf8(output.stdout.clone()).unwrap();
info!("{cmd} stdout :\n\n{}", stdout);
let stderr = String::from_utf8(output.stderr.clone()).unwrap();
info!("{cmd} stderr :\n\n{}", stderr);
info!("{cmd} exit status : {}", output.status);
if !output.status.success() {
return Err(InterpretError::new(format!(
"Command execution failed, exit code {} : {} {}",
output.status,
cmd,
args.join(" ")
)));
}
Ok(output)
};
info!("Successfully prepared ignition files for OKD installation");
// ignition_files_http_path // = PathBuf::from("okd_ignition_files");
info!(
r#"Uploading images, they can be refreshed with a command similar to this one: openshift-install coreos print-stream-json | grep -Eo '"https.*(kernel.|initramfs.|rootfs.)\w+(\.img)?"' | grep x86_64 | xargs -n 1 curl -LO"#
);
warn!(
"TODO push installer image files with `scp -r data/okd/installer_image/* root@192.168.1.1:/usr/local/http/scos/` until performance issue is resolved"
);
inquire::Confirm::new(
"push installer image files with `scp -r data/okd/installer_image/* root@192.168.1.1:/usr/local/http/scos/` until performance issue is resolved").prompt().expect("Prompt error");
&format!("push installer image files with `scp -r {}/* root@{}:/usr/local/http/scos/` until performance issue is resolved", okd_images_path.to_string_lossy(), topology.http_server.get_ip())).prompt().expect("Prompt error");
// let scos_http_path = PathBuf::from("scos");
// StaticFilesHttpScore {
@ -636,10 +613,12 @@ impl OKDSetup02BootstrapInterpret {
) -> Result<(), InterpretError> {
let content = BootstrapIpxeTpl {
http_ip: &topology.http_server.get_ip().to_string(),
scos_path: "scos", // TODO use some constant
installation_device: "/dev/sda", // TODO do something smart based on the host drives
// topology. Something like use the smallest device
// above 200G that is an ssd
scos_path: "scos", // TODO use some constant
ignition_http_path: "okd_ignition_files", // TODO use proper variable
installation_device: "/dev/sda",
// TODO do something smart based on the host drives
// topology. Something like use the smallest device
// above 200G that is an ssd
}
.to_string();
@ -735,7 +714,7 @@ impl Interpret<HAClusterTopology> for OKDSetup02BootstrapInterpret {
self.prepare_ignition_files(inventory, topology).await?;
self.render_per_mac_pxe(inventory, topology).await?;
self.setup_bootstrap_load_balancer(inventory, topology)
.await?;
.await?;
// TODO https://docs.okd.io/latest/installing/installing_bare_metal/upi/installing-bare-metal.html#installation-user-provisioned-validating-dns_installing-bare-metal
// self.validate_dns_config(inventory, topology).await?;

View File

@ -15,4 +15,5 @@ pub struct BootstrapIpxeTpl<'a> {
pub http_ip: &'a str,
pub scos_path: &'a str,
pub installation_device: &'a str,
pub ignition_http_path: &'a str,
}

View File

@ -1,6 +1,122 @@
#!ipxe
# Default chainloader with optional debug mode.
# - Press any key within 3 seconds at start to enable debug mode.
# - In debug mode: confirmations and extra sleeps are enabled.
# - In production (no key pressed): continues without prompts.
# Config
set base-url http://{{ gateway_ip }}:8080
set hostfile ${base-url}/byMAC/01-${mac:hexhyp}.ipxe
set macfile 01-${mac:hexhyp}
set hostfile ${base-url}/byMAC/${macfile}.ipxe
set fallback ${base-url}/fallback.ipxe
chain ${hostfile} || chain ${base-url}/fallback.ipxe
# Verbosity (1..4)
set debug 2
# State
set debugmode 0
echo
echo === iPXE chainload stage (default) ===
echo MAC: ${mac}
echo Base URL: ${base-url}
echo Host file: ${hostfile}
echo Fallback : ${fallback}
echo ======================================
echo
echo Press any key within 3 seconds to enter DEBUG MODE...
prompt --timeout 3 Entering debug mode... && set debugmode 1 || set debugmode 0
iseq ${debugmode} 1 && goto :debug_enabled || goto :debug_disabled
:debug_enabled
echo DEBUG MODE: ON (confirmations and extra sleeps enabled)
sleep 1
goto :start
:debug_disabled
echo DEBUG MODE: OFF (no confirmations; production behavior)
sleep 1
goto :start
:start
# Show network status briefly in both modes
ifstat
iseq ${debugmode} 1 && sleep 2 || sleep 0
# Probe host-specific script via HTTP HEAD
echo
echo Probing host-specific script: ${hostfile}
http --head ${hostfile}
iseq ${rc} 0 && goto :has_hostfile || goto :no_hostfile
:has_hostfile
echo Found host-specific script: ${hostfile}
iseq ${debugmode} 1 && goto :confirm_host || goto :chain_host
:confirm_host
prompt --timeout 8 Press Enter to chain host script, Esc to abort... && goto :chain_host || goto :abort
:chain_host
echo Chaining ${hostfile} ...
iseq ${debugmode} 1 && sleep 2 || sleep 0
chain ${hostfile} || goto :host_chain_fail
# On success, control does not return.
:host_chain_fail
echo ERROR: chain to ${hostfile} failed (rc=${rc})
iseq ${debugmode} 1 && sleep 5 || sleep 1
goto :try_fallback
:no_hostfile
echo NOT FOUND or unreachable: ${hostfile} (rc=${rc})
iseq ${debugmode} 1 && sleep 2 || sleep 0
:try_fallback
echo
echo Probing fallback script: ${fallback}
http --head ${fallback}
iseq ${rc} 0 && goto :has_fallback || goto :fallback_missing
:has_fallback
iseq ${debugmode} 1 && goto :confirm_fallback || goto :chain_fallback
:confirm_fallback
prompt --timeout 8 Press Enter to chain fallback, Esc to shell... && goto :chain_fallback || goto :shell
:chain_fallback
echo Chaining ${fallback} ...
iseq ${debugmode} 1 && sleep 2 || sleep 0
chain ${fallback} || goto :fallback_chain_fail
# On success, control does not return.
:fallback_chain_fail
echo ERROR: chain to fallback failed (rc=${rc})
iseq ${debugmode} 1 && sleep 5 || sleep 1
goto :shell
:fallback_missing
echo ERROR: Fallback script not reachable: ${fallback} (rc=${rc})
iseq ${debugmode} 1 && sleep 5 || sleep 1
goto :shell
:abort
echo Aborted by user.
iseq ${debugmode} 1 && sleep 2 || sleep 1
goto :shell
:shell
echo
echo === iPXE debug shell ===
echo Try:
echo dhcp
echo ifstat
echo ping {{ gateway_ip }}
echo http ${hostfile}
echo http ${fallback}
echo chain ${hostfile}
echo chain ${fallback}
sleep 1
shell
exit

View File

@ -2,6 +2,6 @@ set base-url http://{{ http_ip }}:8080
set scos-base-url = ${base-url}/{{ scos_path }}
set installation-device = {{ installation_device }}
kernel ${scos-base-url}/scos-live-kernel.x86_64 initrd=main coreos.live.rootfs_url=${scos-base-url}/scos-live-rootfs.x86_64.img coreos.inst.install_dev=${installation-device} coreos.inst.ignition_url=${base-url}/bootstrap.ign
kernel ${scos-base-url}/scos-live-kernel.x86_64 initrd=main coreos.live.rootfs_url=${scos-base-url}/scos-live-rootfs.x86_64.img coreos.inst.install_dev=${installation-device} coreos.inst.ignition_url=${base-url}/{{ ignition_http_path }}/bootstrap.ign
initrd --name main ${scos-base-url}/scos-live-initramfs.x86_64.img
boot

View File

@ -18,6 +18,7 @@ infisical = { git = "https://github.com/jggc/rust-sdk.git", branch = "patch-1" }
tokio.workspace = true
async-trait.workspace = true
http.workspace = true
inquire.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@ -110,6 +110,42 @@ impl SecretManager {
})
}
pub async fn get_or_prompt<T: Secret>() -> Result<T, SecretStoreError> {
let secret = Self::get::<T>().await;
let manager = get_secret_manager().await;
let prompted = secret.is_err();
let secret = secret.or_else(|e| -> Result<T, SecretStoreError> {
debug!("Could not get secret : {e}");
let ns = &manager.namespace;
let key = T::KEY;
let secret_json = inquire::Text::new(&format!(
"Secret not found for {} {}, paste the JSON here :",
ns, key
))
.prompt()
.map_err(|e| {
SecretStoreError::Store(format!("Failed to prompt secret {ns} {key} : {e}").into())
})?;
let secret: T = serde_json::from_str(&secret_json).map_err(|e| {
SecretStoreError::Deserialization {
key: T::KEY.to_string(),
source: e,
}
})?;
Ok(secret)
})?;
if prompted {
Self::set(&secret).await?;
}
Ok(secret)
}
/// Serializes and stores a secret.
pub async fn set<T: Secret>(secret: &T) -> Result<(), SecretStoreError> {
let manager = get_secret_manager().await;