use crate::core::types::{Resource, ResourceType};
use crate::resources;
use provable_contracts_macros::contract;
#[derive(Clone, Copy)]
struct ScriptHandlers {
check: fn(&Resource) -> String,
apply: fn(&Resource) -> String,
state_query: fn(&Resource) -> String,
}
macro_rules! from_module {
($($seg:ident)::+) => {
ScriptHandlers {
check: $($seg)::+::check_script,
apply: $($seg)::+::apply_script,
state_query: $($seg)::+::state_query_script,
}
};
}
const NOT_DISPATCHABLE: &str = "codegen not implemented for recipe (expand first)";
fn handlers(resource_type: &ResourceType) -> Option<ScriptHandlers> {
match resource_type {
ResourceType::Package
| ResourceType::File
| ResourceType::Service
| ResourceType::Mount
| ResourceType::User
| ResourceType::Cron
| ResourceType::Network => handlers_host(resource_type),
ResourceType::Docker
| ResourceType::Pepita
| ResourceType::Model
| ResourceType::Gpu
| ResourceType::Task
| ResourceType::WasmBundle
| ResourceType::Image => handlers_workload(resource_type),
ResourceType::Build
| ResourceType::GithubRelease
| ResourceType::OverlayInterface
| ResourceType::DiskBudget
| ResourceType::BackupSync
| ResourceType::NasArchive => handlers_fleet(resource_type),
ResourceType::Recipe => None,
}
}
fn handlers_host(resource_type: &ResourceType) -> Option<ScriptHandlers> {
let row = match resource_type {
ResourceType::Package => ScriptHandlers {
check: resources::package_check::check_script,
apply: resources::package::apply_script,
state_query: resources::package::state_query_script,
},
ResourceType::File => from_module!(resources::file),
ResourceType::Service => from_module!(resources::service),
ResourceType::Mount => from_module!(resources::mount),
ResourceType::User => from_module!(resources::user),
ResourceType::Cron => from_module!(resources::cron),
ResourceType::Network => from_module!(resources::network),
ResourceType::Docker
| ResourceType::Pepita
| ResourceType::Model
| ResourceType::Gpu
| ResourceType::Task
| ResourceType::WasmBundle
| ResourceType::Image
| ResourceType::Build
| ResourceType::GithubRelease
| ResourceType::OverlayInterface
| ResourceType::DiskBudget
| ResourceType::BackupSync
| ResourceType::NasArchive
| ResourceType::Recipe => return None,
};
Some(row)
}
fn handlers_workload(resource_type: &ResourceType) -> Option<ScriptHandlers> {
let row = match resource_type {
ResourceType::Docker => from_module!(resources::docker),
ResourceType::Pepita => from_module!(resources::pepita),
ResourceType::Model => from_module!(resources::model),
ResourceType::Gpu => from_module!(resources::gpu),
ResourceType::Task => from_module!(resources::task),
ResourceType::WasmBundle => from_module!(resources::wasm_bundle),
ResourceType::Image => from_module!(resources::file),
ResourceType::Package
| ResourceType::File
| ResourceType::Service
| ResourceType::Mount
| ResourceType::User
| ResourceType::Cron
| ResourceType::Network
| ResourceType::Build
| ResourceType::GithubRelease
| ResourceType::OverlayInterface
| ResourceType::DiskBudget
| ResourceType::BackupSync
| ResourceType::NasArchive
| ResourceType::Recipe => return None,
};
Some(row)
}
fn handlers_fleet(resource_type: &ResourceType) -> Option<ScriptHandlers> {
let row = match resource_type {
ResourceType::Build => from_module!(resources::build),
ResourceType::GithubRelease => from_module!(resources::github_release),
ResourceType::OverlayInterface => from_module!(resources::overlay_interface),
ResourceType::DiskBudget => from_module!(resources::disk_budget),
ResourceType::BackupSync => from_module!(resources::backup_sync),
ResourceType::NasArchive => from_module!(resources::nas_archive),
ResourceType::Package
| ResourceType::File
| ResourceType::Service
| ResourceType::Mount
| ResourceType::User
| ResourceType::Cron
| ResourceType::Network
| ResourceType::Docker
| ResourceType::Pepita
| ResourceType::Model
| ResourceType::Gpu
| ResourceType::Task
| ResourceType::WasmBundle
| ResourceType::Image
| ResourceType::Recipe => return None,
};
Some(row)
}
#[contract("codegen-dispatch-v1", equation = "check_script")]
pub fn check_script(resource: &Resource) -> Result<String, String> {
contract_pre_check_script!(resource);
let row = handlers(&resource.resource_type).ok_or_else(|| NOT_DISPATCHABLE.to_string())?;
Ok(in_declared_privilege_context(
resource,
(row.check)(resource),
))
}
#[contract("codegen-dispatch-v1", equation = "apply_script")]
pub fn apply_script(resource: &Resource) -> Result<String, String> {
contract_pre_apply_script!(resource);
if crate::core::resolver::has_unresolved_secret(resource) {
return Err(format!(
"resource carries an unresolved secret template — the secrets provider \
did not supply it, and applying would write the placeholder as if it \
were the credential (type: {})",
resource.resource_type
));
}
if resource.state.as_deref() == Some("absent")
&& matches!(
resource.resource_type,
ResourceType::Task | ResourceType::Build | ResourceType::WasmBundle
)
{
return Ok(format!(
"echo 'forjar: {} resources have no absent form — nothing to remove'",
resource.resource_type
));
}
let row = handlers(&resource.resource_type).ok_or_else(|| NOT_DISPATCHABLE.to_string())?;
let script = (row.apply)(resource);
Ok(in_declared_privilege_context(resource, script))
}
fn in_declared_privilege_context(resource: &Resource, script: String) -> String {
if !resource.sudo {
return script;
}
let delim = crate::resources::task::heredoc_delimiter("FORJAR_SUDO", &script);
format!(
"if [ \"$(id -u)\" -eq 0 ]; then\n{script}\nelse\n\
forjar_sudo_script=\"$(mktemp \"${{TMPDIR:-/tmp}}/forjar-sudo.XXXXXX\")\" || exit 1\n\
trap 'rm -f \"$forjar_sudo_script\"' EXIT INT TERM\n\
cat >\"$forjar_sudo_script\" <<'{delim}' || exit 1\n{script}\n{delim}\n\
sudo bash \"$forjar_sudo_script\"\nfi"
)
}
#[contract("codegen-dispatch-v1", equation = "state_query_script")]
pub fn state_query_script(resource: &Resource) -> Result<String, String> {
contract_pre_state_query_script!(resource);
let row = handlers(&resource.resource_type).ok_or_else(|| NOT_DISPATCHABLE.to_string())?;
Ok(in_declared_privilege_context(
resource,
(row.state_query)(resource),
))
}