Skip to content

Commit 1bbfa63

Browse files
committed
Implement container service test cases
Signed-off-by: Guvenc Gulce <[email protected]>
1 parent 4f02748 commit 1bbfa63

File tree

6 files changed

+215
-12
lines changed

6 files changed

+215
-12
lines changed

feos/services/container-service/src/persistence/repository.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ fn string_to_container_state(s: &str) -> Result<ContainerState, PersistenceError
3333
}
3434
}
3535

36+
fn container_state_to_string(state: ContainerState) -> &'static str {
37+
match state {
38+
ContainerState::PullingImage => "PULLING_IMAGE",
39+
ContainerState::Created => "CREATED",
40+
ContainerState::Running => "RUNNING",
41+
ContainerState::Stopped => "STOPPED",
42+
ContainerState::Unspecified => "CONTAINER_STATE_UNSPECIFIED",
43+
}
44+
}
45+
3646
impl ContainerRepository {
3747
pub async fn connect(db_url: &str) -> Result<Self, PersistenceError> {
3848
let pool = SqlitePoolOptions::new()
@@ -111,7 +121,7 @@ impl ContainerRepository {
111121
let mut config_blob = Vec::new();
112122
container.config.encode(&mut config_blob)?;
113123

114-
let state_str = format!("{:?}", container.status.state).to_uppercase();
124+
let state_str = container_state_to_string(container.status.state);
115125

116126
sqlx::query(
117127
r#"
@@ -135,7 +145,7 @@ impl ContainerRepository {
135145
container_id: Uuid,
136146
new_state: ContainerState,
137147
) -> Result<bool, PersistenceError> {
138-
let state_str = format!("{new_state:?}").to_uppercase();
148+
let state_str = container_state_to_string(new_state);
139149

140150
let result = sqlx::query(
141151
r#"

feos/services/vm-service/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub mod worker;
2424
pub const DEFAULT_VM_DB_URL: &str = "sqlite:/var/lib/feos/vms.db";
2525
pub const VM_API_SOCKET_DIR: &str = "/tmp/feos/vm_api_sockets";
2626
pub const VM_CH_BIN: &str = "cloud-hypervisor";
27+
pub const CONT_YOUKI_BIN: &str = "youki";
2728
pub const IMAGE_DIR: &str = "/var/lib/feos/images";
2829
pub const VM_CONSOLE_DIR: &str = "/tmp/feos/consoles";
2930

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use super::{
5+
ensure_server, get_public_clients, skip_if_youki_binary_missing, TEST_CONTAINER_IMAGE_REF,
6+
};
7+
use anyhow::Result;
8+
use feos_proto::container_service::{
9+
ContainerConfig, ContainerState, CreateContainerRequest, DeleteContainerRequest,
10+
GetContainerRequest, StartContainerRequest, StopContainerRequest,
11+
};
12+
use log::info;
13+
use std::time::Duration;
14+
use tokio::time::timeout;
15+
16+
async fn wait_for_container_state(
17+
client: &mut feos_proto::container_service::container_service_client::ContainerServiceClient<
18+
tonic::transport::Channel,
19+
>,
20+
container_id: &str,
21+
target_state: ContainerState,
22+
) -> Result<()> {
23+
info!(
24+
"Waiting for container {} to reach state {:?}",
25+
container_id, target_state
26+
);
27+
for _ in 0..180 {
28+
let response = client
29+
.get_container(GetContainerRequest {
30+
container_id: container_id.to_string(),
31+
})
32+
.await?
33+
.into_inner();
34+
35+
let current_state = ContainerState::try_from(response.state).unwrap_or_default();
36+
info!("Container {} is in state {:?}", container_id, current_state);
37+
38+
if current_state == target_state {
39+
return Ok(());
40+
}
41+
42+
tokio::time::sleep(Duration::from_secs(1)).await;
43+
}
44+
anyhow::bail!(
45+
"Timeout waiting for container {} to reach state {:?}",
46+
container_id,
47+
target_state
48+
);
49+
}
50+
51+
#[tokio::test]
52+
async fn test_create_and_start_container() -> Result<()> {
53+
if skip_if_youki_binary_missing() {
54+
return Ok(());
55+
}
56+
ensure_server().await;
57+
let (_, _, mut container_client) = get_public_clients().await?;
58+
59+
let image_ref = TEST_CONTAINER_IMAGE_REF.clone();
60+
let container_config = ContainerConfig {
61+
image_ref,
62+
command: vec![],
63+
env: Default::default(),
64+
};
65+
66+
let create_req = CreateContainerRequest {
67+
config: Some(container_config),
68+
container_id: None,
69+
};
70+
71+
info!("Sending CreateContainer request");
72+
let create_res = container_client
73+
.create_container(create_req)
74+
.await?
75+
.into_inner();
76+
let container_id = create_res.container_id;
77+
info!("Container creation initiated with ID: {}", container_id);
78+
79+
timeout(
80+
Duration::from_secs(180),
81+
wait_for_container_state(
82+
&mut container_client,
83+
&container_id,
84+
ContainerState::Created,
85+
),
86+
)
87+
.await
88+
.expect("Timed out waiting for container to become created")?;
89+
info!("Container is in CREATED state");
90+
91+
let start_req = StartContainerRequest {
92+
container_id: container_id.clone(),
93+
};
94+
info!("Sending StartContainer request for ID: {}", container_id);
95+
container_client.start_container(start_req).await?;
96+
97+
timeout(
98+
Duration::from_secs(30),
99+
wait_for_container_state(
100+
&mut container_client,
101+
&container_id,
102+
ContainerState::Running,
103+
),
104+
)
105+
.await
106+
.expect("Timed out waiting for container to become running")?;
107+
info!("Container is in RUNNING state");
108+
109+
let stop_req = StopContainerRequest {
110+
container_id: container_id.clone(),
111+
signal: None,
112+
timeout_seconds: None,
113+
};
114+
info!("Sending StopContainer request for ID: {}", container_id);
115+
container_client.stop_container(stop_req).await?;
116+
117+
timeout(
118+
Duration::from_secs(30),
119+
wait_for_container_state(
120+
&mut container_client,
121+
&container_id,
122+
ContainerState::Stopped,
123+
),
124+
)
125+
.await
126+
.expect("Timed out waiting for container to become stopped")?;
127+
info!("Container is in STOPPED state");
128+
129+
let delete_req = DeleteContainerRequest {
130+
container_id: container_id.clone(),
131+
};
132+
info!("Sending DeleteContainer request for ID: {}", container_id);
133+
container_client.delete_container(delete_req).await?;
134+
info!("DeleteContainer call successful");
135+
136+
let get_req = GetContainerRequest {
137+
container_id: container_id.clone(),
138+
};
139+
let result = container_client.get_container(get_req).await;
140+
assert!(result.is_err(), "GetContainer should fail after deletion");
141+
info!(
142+
"Verified that container {} is no longer found.",
143+
container_id
144+
);
145+
146+
Ok(())
147+
}

feos/tests/integration/host_tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::io::{BufRead, BufReader};
1414
#[tokio::test]
1515
async fn test_hostname_retrieval() -> Result<()> {
1616
ensure_server().await;
17-
let (_, mut host_client) = get_public_clients().await?;
17+
let (_, mut host_client, _) = get_public_clients().await?;
1818

1919
let response = host_client.hostname(HostnameRequest {}).await?;
2020
let remote_hostname = response.into_inner().hostname;
@@ -37,7 +37,7 @@ async fn test_hostname_retrieval() -> Result<()> {
3737
#[tokio::test]
3838
async fn test_get_memory_info() -> Result<()> {
3939
ensure_server().await;
40-
let (_, mut host_client) = get_public_clients().await?;
40+
let (_, mut host_client, _) = get_public_clients().await?;
4141

4242
let file = File::open("/proc/meminfo")?;
4343
let reader = BufReader::new(file);
@@ -85,7 +85,7 @@ async fn test_get_memory_info() -> Result<()> {
8585
#[tokio::test]
8686
async fn test_get_cpu_info() -> Result<()> {
8787
ensure_server().await;
88-
let (_, mut host_client) = get_public_clients().await?;
88+
let (_, mut host_client, _) = get_public_clients().await?;
8989

9090
let file = File::open("/proc/cpuinfo")?;
9191
let reader = BufReader::new(file);
@@ -155,7 +155,7 @@ async fn test_get_cpu_info() -> Result<()> {
155155
#[tokio::test]
156156
async fn test_get_network_info() -> Result<()> {
157157
ensure_server().await;
158-
let (_, mut host_client) = get_public_clients().await?;
158+
let (_, mut host_client, _) = get_public_clients().await?;
159159

160160
info!("Sending GetNetworkInfo request");
161161
let response = host_client

feos/tests/integration/mod.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
use anyhow::Result;
55
use feos_proto::{
6+
container_service::container_service_client::ContainerServiceClient,
67
host_service::host_service_client::HostServiceClient,
78
image_service::image_service_client::ImageServiceClient,
89
vm_service::vm_service_client::VmServiceClient,
910
};
1011
use hyper_util::rt::TokioIo;
1112
use image_service::IMAGE_SERVICE_SOCKET;
1213
use log::{error, info};
14+
use nix::sys::prctl;
1315
use once_cell::sync::{Lazy, OnceCell as SyncOnceCell};
1416
use std::env;
1517
use std::process::Command;
@@ -19,18 +21,24 @@ use tokio::net::UnixStream;
1921
use tokio::sync::OnceCell as TokioOnceCell;
2022
use tonic::transport::{Channel, Endpoint, Uri};
2123
use tower::service_fn;
22-
use vm_service::VM_CH_BIN;
24+
use vm_service::{CONT_YOUKI_BIN, VM_CH_BIN};
2325

26+
pub mod container_tests;
2427
pub mod fixtures;
2528
pub mod host_tests;
2629
pub mod image_tests;
2730
pub mod vm_tests;
2831

2932
pub const PUBLIC_SERVER_ADDRESS: &str = "http://[::1]:1337";
3033
pub const DEFAULT_TEST_IMAGE_REF: &str = "ghcr.io/ironcore-dev/os-images/gardenlinux-ch-dev";
34+
pub const DEFAULT_TEST_CONTAINER_IMAGE_REF: &str = "ghcr.io/appvia/hello-world/hello-world";
3135

3236
pub static TEST_IMAGE_REF: Lazy<String> =
3337
Lazy::new(|| env::var("TEST_IMAGE_REF").unwrap_or_else(|_| DEFAULT_TEST_IMAGE_REF.to_string()));
38+
pub static TEST_CONTAINER_IMAGE_REF: Lazy<String> = Lazy::new(|| {
39+
env::var("TEST_CONTAINER_IMAGE_REF")
40+
.unwrap_or_else(|_| DEFAULT_TEST_CONTAINER_IMAGE_REF.to_string())
41+
});
3442

3543
static SERVER_RUNTIME: TokioOnceCell<Arc<tokio::runtime::Runtime>> = TokioOnceCell::const_new();
3644
static TEMP_DIR_GUARD: SyncOnceCell<tempfile::TempDir> = SyncOnceCell::new();
@@ -51,9 +59,23 @@ async fn setup_server() -> Arc<tokio::runtime::Runtime> {
5159

5260
let db_path = temp_dir.path().join("vms.db");
5361
let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
62+
let container_db_path = temp_dir.path().join("containers.db");
63+
let container_db_url = format!("sqlite:{}", container_db_path.to_str().unwrap());
5464

5565
env::set_var("DATABASE_URL", &db_url);
66+
env::set_var("CONTAINER_DATABASE_URL", &container_db_url);
5667
info!("Using temporary database for tests: {}", db_url);
68+
info!(
69+
"Using temporary container database for tests: {}",
70+
container_db_url
71+
);
72+
73+
if let Err(e) = prctl::set_child_subreaper(true) {
74+
log::error!(
75+
"Failed to set as child subreaper, container tests will fail: {}",
76+
e
77+
);
78+
}
5779

5880
let runtime = tokio::runtime::Builder::new_multi_thread()
5981
.enable_all()
@@ -82,11 +104,15 @@ async fn setup_server() -> Arc<tokio::runtime::Runtime> {
82104
panic!("Server did not start in time.");
83105
}
84106

85-
pub async fn get_public_clients() -> Result<(VmServiceClient<Channel>, HostServiceClient<Channel>)>
86-
{
107+
pub async fn get_public_clients() -> Result<(
108+
VmServiceClient<Channel>,
109+
HostServiceClient<Channel>,
110+
ContainerServiceClient<Channel>,
111+
)> {
87112
let vm_client = VmServiceClient::connect(PUBLIC_SERVER_ADDRESS).await?;
88113
let host_client = HostServiceClient::connect(PUBLIC_SERVER_ADDRESS).await?;
89-
Ok((vm_client, host_client))
114+
let container_client = ContainerServiceClient::connect(PUBLIC_SERVER_ADDRESS).await?;
115+
Ok((vm_client, host_client, container_client))
90116
}
91117

92118
pub async fn get_image_service_client() -> Result<ImageServiceClient<Channel>> {
@@ -119,3 +145,22 @@ pub fn skip_if_ch_binary_missing() -> bool {
119145
}
120146
false
121147
}
148+
149+
pub fn check_youki_binary() -> bool {
150+
Command::new("which")
151+
.arg(CONT_YOUKI_BIN)
152+
.output()
153+
.map(|o| o.status.success())
154+
.unwrap_or(false)
155+
}
156+
157+
pub fn skip_if_youki_binary_missing() -> bool {
158+
if !check_youki_binary() {
159+
log::warn!(
160+
"Skipping test because '{}' binary was not found in PATH.",
161+
VM_CH_BIN
162+
);
163+
return true;
164+
}
165+
false
166+
}

feos/tests/integration/vm_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async fn test_create_and_start_vm() -> Result<()> {
2424
}
2525

2626
ensure_server().await;
27-
let (mut vm_client, _) = get_public_clients().await?;
27+
let (mut vm_client, _, _) = get_public_clients().await?;
2828

2929
let image_ref = TEST_IMAGE_REF.clone();
3030
let vm_config = VmConfig {
@@ -305,7 +305,7 @@ async fn test_vm_healthcheck_and_crash_recovery() -> Result<()> {
305305
}
306306

307307
ensure_server().await;
308-
let (mut vm_client, _) = get_public_clients().await?;
308+
let (mut vm_client, _, _) = get_public_clients().await?;
309309

310310
let image_ref = TEST_IMAGE_REF.clone();
311311
let vm_config = VmConfig {

0 commit comments

Comments
 (0)