feat: all-in-one nodes for cloud engines - #11349
Conversation
|
✅ No security or compliance issues detected. Reviewed everything up to 918cb72. Security Overview
Detected Code Changes
|
basvandijk
left a comment
There was a problem hiding this comment.
Approve for the files under rs/tests/driver/src/driver/.
A PR description would be nice for a >2k diff ;)
|
✅ No security or compliance issues detected. Reviewed everything up to 918cb72. Security Overview
Detected Code Changes
|
pierugo-dfinity
left a comment
There was a problem hiding this comment.
Are we confident launching this without a system test?
Would there be a way to test that the different arguments to different methods of different canisters do not drift away from production?
| pub struct Config { | ||
| /// Principal of the engine management canister, which an all-in-one node | ||
| /// needs to discover the operator canister of its own engine. When unset, | ||
| /// the node cannot find its operator and therefore does not run `ic-gateway`. | ||
| #[serde(default)] | ||
| pub engine_management_canister_id: Option<CanisterId>, | ||
| } |
There was a problem hiding this comment.
Did we consider putting that information to the registry instead of the config? Personally, having a hard-coded canister ID inside the config feels weird + the registry can be modified if we need to + I think it would be easier to mock in tests.
And the orchestrator would know whether to launch the CloudEngineManager or not based on its node reward type, the same way it tells it is a cloud-engine node for the registry replicator & firewall.
But this is not a strong opinion, just interested in what you think
| let engine_management_canister_id = match guestos_config | ||
| .guestos_settings | ||
| .engine_management_canister_id | ||
| .as_deref() | ||
| .or(matches!( | ||
| guestos_config.icos_settings.deployment_environment, | ||
| DeploymentEnvironment::Mainnet | ||
| ) | ||
| .then_some(ic_config::cloud_engine::MAINNET_ENGINE_MANAGEMENT_CANISTER_ID)) | ||
| { | ||
| Some(id) => serde_json::to_string(id) | ||
| .context("Failed to encode the engine management canister id")?, | ||
| None => "null".to_string(), | ||
| }; |
There was a problem hiding this comment.
| let engine_management_canister_id = match guestos_config | |
| .guestos_settings | |
| .engine_management_canister_id | |
| .as_deref() | |
| .or(matches!( | |
| guestos_config.icos_settings.deployment_environment, | |
| DeploymentEnvironment::Mainnet | |
| ) | |
| .then_some(ic_config::cloud_engine::MAINNET_ENGINE_MANAGEMENT_CANISTER_ID)) | |
| { | |
| Some(id) => serde_json::to_string(id) | |
| .context("Failed to encode the engine management canister id")?, | |
| None => "null".to_string(), | |
| }; | |
| let engine_management_canister_id = match guestos_config | |
| .guestos_settings | |
| .engine_management_canister_id | |
| .as_deref() { | |
| Some(id) => id, | |
| None if guestos_config.icos_settings.deployment_environment == DeploymentEnvironment::Mainnet => ic_config::cloud_engine::MAINNET_ENGINE_MANAGEMENT_CANISTER_ID, | |
| None => "null", | |
| } | |
| .to_string(); |
Maybe something like this would clarify the control flow. I'm not sure why use serde_json::to_string either
| DomainNameMissingError(NodeId, RegistryVersion), | ||
|
|
||
| /// A step of the cloud engine configuration lookup failed. | ||
| CloudEngineError(String), |
There was a problem hiding this comment.
There are a lot of different Error enums (FetchError, OperatorError, ConfigError), yet not complete (AgentFactory and Discovery return OrchestratorErrors).
- Would it be feasible to flatten the existing errors into one big
CloudEngineManagementErrorenum holding every possible variant? Maybe in its ownorchestrator/src/cloud_engine/error.rs - I don't think we need a new variant in
OrchestratorError. Existing callers ofcloud_engine_errorcan have their own enums (AgentFactory/Discovery) or use a different variant (inregistry_helper.rsand infn env_overlay.
| registry_helper::RegistryHelper, | ||
| }; | ||
| use config::ConfigError; | ||
| pub(crate) use config::GatewayConfig; |
There was a problem hiding this comment.
No need to re-export, I think callers can import the submodule directly
| /// needs to discover the operator canister of its own engine. When unset, | ||
| /// the node cannot find its operator and therefore does not run `ic-gateway`. | ||
| #[serde(default)] | ||
| pub engine_management_canister_id: Option<CanisterId>, |
There was a problem hiding this comment.
Note that despite the very misleading documentation, CanisterId's implementation of Deserialize does not check that it is a valid canister ID (it could be any principal) because CanisterId::try_from does not actually try, it just casts without validating.
ic/rs/types/base_types/src/canister_id.rs
Lines 351 to 363 in c2cebaf
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.debug_struct("GatewayConfig") | ||
| .field("base_domains", &self.base_domains) | ||
| .field("dns_api_url", &self.dns_api_url.as_str()) |
There was a problem hiding this comment.
Just to make sure: the DNS API URL doesn't contain anything sensitive (like query parameters or whatnot)?
There was a problem hiding this comment.
This is all as intended: the DNS API config consists of the URL and the key (below). The key is sensitive, the URL not.
| } | ||
|
|
||
| #[test] | ||
| fn every_missing_field_is_incomplete() { |
There was a problem hiding this comment.
style nit: we could use rstest here (but this is fine too)
| assert_eq!( | ||
| discovery.validated_from_memory(SUBNET_1, VERSION), | ||
| Some(operator()) | ||
| ); |
There was a problem hiding this comment.
assert_eq!(discovery.resolved, Some(operator()));
| assert_matches!( | ||
| discovery.validate(operator().get(), SUBNET_1, VERSION), | ||
| Ok(accepted) if accepted == operator() | ||
| ); |
There was a problem hiding this comment.
assert_eq!(discovery.resolved, Some(operator()));
| SystemTime::now() | ||
| .duration_since(SystemTime::UNIX_EPOCH) | ||
| .map_or(0, |since_epoch| since_epoch.as_secs() as i64) | ||
| } |
There was a problem hiding this comment.
Some unit tests for this module would be more than welcome 🙏 (especially the invalidation logic)
Cloud engines nodes will be "all-in-one" nodes. This means they don't just run the replica, but also
ic-gatewayand potentially alsoic-boundary. The orchestrator already supports running multiple processes, but we can't just startic-gateway, we also need to provide it with the right, engine-specific config. That's what this PR is doing:This PR adds an additional service to the orchestrator that fetches the HTTP Gateway config from the engine operator canister and uses it to run
ic-gateway.The config includes the domain names, DNS API URLs and API key (for the DNS-01 ACME challenge to obtain the certificates), and the ACME account. That's the bare minimum for
ic-gatewayto run. In the future, the config will be extended.The config is stored in the engine operator canister, which resides on the engine itself. The orchestrator first has to discover this canister using the engine management canister. Then, it needs to use the node's identity to make the call as the config is protected and can only be accessed by the nodes making up the engine.