Skip to content
Draft
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
68 changes: 68 additions & 0 deletions crates/trident-acl-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ pub async fn run_omaha_only(config: &config::AgentConfig) -> Result<(), anyhow::
}
}

/// Checks that the Omaha/Nebraska server at `url` is reachable and speaking
/// the Omaha protocol, without treating any app-level result (including a
/// non-OK app/update-check status) as a failure. Unlike [`query_for_update`],
/// this only fails on network/transport problems or a response that isn't
/// well-formed Omaha XML -- it's meant for a pure "can we talk to this
/// server at all" check (e.g. `--validate-connection nebraska`), not for
/// deciding whether an update is available.
pub fn check_nebraska_reachable(
url: &Url,
app_id: &str,
track: &str,
machine_id_source: IdSource,
) -> Result<(), HarpoonError> {
let request = Request::default().with_app(
AppRequest::new(app_id, Version::new(0, 0, 0), track, machine_id_source)?
.with_update_check(),
);
omaha::send(url, &request)?;
Ok(())
}

/// Query the Omaha server at the given URL for the given app and track.
pub fn query_for_update(
url: &Url,
Expand Down Expand Up @@ -470,4 +491,51 @@ mod tests {
omaha_mock.assert();
assert!(matches!(response.result, QueryResult::NoUpdate));
}

#[test]
fn test_check_nebraska_reachable_succeeds_on_error_app_status() {
// check_nebraska_reachable() is meant to be a pure "can we reach
// this server and does it speak Omaha" check, unlike
// query_for_update() which also validates app-level semantics. A
// well-formed response with a non-OK app status should still count
// as "reachable" here, even though query_for_update() would reject
// the same response as a QueryError.
let mut server = mockito::Server::new();

let omaha_mock = server
.mock("POST", "/")
.with_status(200)
.match_body(Matcher::Regex(".*<updatecheck.*".to_string()))
.with_body(indoc::indoc! {r#"
<?xml version="1.0" encoding="UTF-8"?>
<response protocol="3.0" server="mock">
<daystart elapsed_seconds="0"/>
<app appid="test" status="error-unknownApplication">
<updatecheck status="error-internal"><urls></urls></updatecheck>
</app>
</response>"#})
.expect(2)
.create();

check_nebraska_reachable(
&Url::parse(&server.url()).unwrap(),
"test",
"track",
IdSource::MachineIdHashed,
)
.unwrap();

// Confirm the same response *would* be rejected by query_for_update(),
// to document the intentional behavior difference.
assert!(query_for_update(
&Url::parse(&server.url()).unwrap(),
"test",
"track",
&Version::new(0, 1, 0),
IdSource::MachineIdHashed,
)
.is_err());

omaha_mock.assert();
}
}
117 changes: 115 additions & 2 deletions crates/trident-acl-agent/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use std::path::PathBuf;

use anyhow::Context;
use clap::Parser;
use log::{LevelFilter, Log, Metadata, Record};

use trident_acl_agent::{
check_nebraska_reachable,
config::{AgentConfig, GoalSource, DEFAULT_CONFIG_PATH},
k8s::NodeClient,
orchestrator::Orchestrator,
run_omaha_only,
trident::TridentClient,
IdSource,
};

/// Module/target prefixes for the underlying HTTP/gRPC/watch client stack.
Expand Down Expand Up @@ -87,10 +92,114 @@ struct Args {
config: Option<PathBuf>,

/// Optional Omaha/Nebraska URL override. When omitted, Harpoon uses the
/// endpoint from config.toml. When both are missing, startup fails with a
/// clear error.
/// endpoint from trident-acl-agent.conf. When both are missing, startup
/// fails with a clear error.
#[arg()]
url: Option<url::Url>,

/// Validate connectivity to a single dependency and exit immediately,
/// instead of running the agent. Useful for troubleshooting one
/// connection in isolation (e.g. a systemd ExecStartPre check, or manual
/// diagnostics on-node) without running the full orchestrator loop.
/// Exits with status 0 if the connection could be established, non-zero
/// (with an error message) otherwise.
#[arg(long, value_enum)]
validate_connection: Option<ConnectionTarget>,
}

/// A single dependency `--validate-connection` can check.
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum ConnectionTarget {
/// Validates reachability of the Kubernetes API server by fetching this
/// node's own Node object (the same access the agent's reconcile loop
/// already requires).
Kubernetes,
/// Validates reachability of tridentd by connecting to its gRPC Unix
/// socket. Connecting is sufficient - no RPC call is needed, since the
/// connection itself fails immediately if nothing is listening.
Tridentd,
/// Validates reachability of the Nebraska/Omaha server by issuing a real
/// update-check query. Any well-formed Omaha response (including "no
/// update available") counts as success - only a network/transport
/// failure is treated as unreachable.
Comment thread
bfjelds marked this conversation as resolved.
Nebraska,
}

/// Checks connectivity to exactly one of `target`'s dependencies and returns
/// `Ok(())` on success. The caller (`main`) surfaces any `Err` the normal way
/// (`anyhow`'s `Termination` impl prints the error and exits non-zero), so
/// this function only needs to produce a descriptive error on failure - no
/// explicit `process::exit` is required.
async fn validate_connection(
target: ConnectionTarget,
config: &AgentConfig,
) -> Result<(), anyhow::Error> {
match target {
ConnectionTarget::Kubernetes => {
let client = NodeClient::new(&config.kubernetes)
.await
.context("failed to build Kubernetes client")?;
client
.get_node(&config.kubernetes.node_name)
.await
.with_context(|| {
format!(
"failed to reach Kubernetes API server at {} (get Node {:?})",
config.kubernetes.api_server, config.kubernetes.node_name
)
})?;
log::info!(
"kubernetes: reached API server at {} and fetched Node {:?}",
config.kubernetes.api_server,
config.kubernetes.node_name
);
}
ConnectionTarget::Tridentd => {
TridentClient::connect(&config.trident.socket)
.await
.with_context(|| {
format!("failed to reach tridentd at {}", config.trident.socket)
})?;
log::info!("tridentd: connected to {}", config.trident.socket);
}
ConnectionTarget::Nebraska => {
let endpoint = config.nebraska.endpoint.clone().ok_or_else(|| {
anyhow::anyhow!(
"nebraska.endpoint is not configured (set [nebraska].endpoint in trident-acl-agent.conf, or pass a URL on the CLI)"
)
})?;
let app_id = config.nebraska.app_id.clone();
// check_nebraska_reachable() is a blocking call (reqwest::blocking
// under the hood, see omaha::send) - calling it directly from this
// async fn can panic ("Cannot drop a runtime in a context where
// blocking is not allowed") because reqwest::blocking spins up
// its own inner Tokio runtime per call, which isn't safe to tear
// down from inside an already-running async task. Run it on a
// dedicated blocking thread instead.
//
// Deliberately uses check_nebraska_reachable() rather than
// query_for_update(): the latter also validates app-level
// semantics (app ID match, non-error app/update-check status),
// which would make this a "can we get a valid update check" test
// rather than the pure reachability check documented on
// ConnectionTarget::Nebraska above.
let endpoint_for_task = endpoint.clone();
let track = config.nebraska.track.clone();
tokio::task::spawn_blocking(move || {
check_nebraska_reachable(
&endpoint_for_task,
&app_id,
&track,
IdSource::MachineIdHashed,
)
})
.await
.context("Nebraska connectivity check task panicked")?
.with_context(|| format!("failed to reach Nebraska server at {endpoint}"))?;
log::info!("nebraska: reached server at {endpoint}");
}
}
Ok(())
}

#[tokio::main]
Expand Down Expand Up @@ -131,6 +240,10 @@ async fn main() -> Result<(), anyhow::Error> {
let config = AgentConfig::load(&config_path, explicit_config)?.unwrap_or_default();
let config = config.with_cli_endpoint(args.url.clone());

if let Some(target) = args.validate_connection {
return validate_connection(target, &config).await;
}

match config.orchestration.goal_source {
// Historical one-shot flow: query Nebraska once, apply an update if
// offered, and exit. No Kubernetes/annotation involvement.
Expand Down