Skip to content

Commit 6400cb9

Browse files
committed
Implement Container Service
Signed-off-by: Guvenc Gulce <[email protected]>
1 parent 9cf35b3 commit 6400cb9

File tree

26 files changed

+1655
-6
lines changed

26 files changed

+1655
-6
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"feos/services/host-service",
66
"feos/services/image-service",
77
"feos/services/task-service",
8+
"feos/services/container-service",
89
"cli",
910
"feos/proto",
1011
"feos/utils",

cli/src/container_commands.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use anyhow::{Context, Result};
5+
use clap::{Args, Subcommand};
6+
use feos_proto::container_service::{
7+
container_service_client::ContainerServiceClient, ContainerConfig, ContainerState,
8+
CreateContainerRequest, DeleteContainerRequest, GetContainerRequest, ListContainersRequest,
9+
StartContainerRequest, StopContainerRequest,
10+
};
11+
use tonic::transport::Channel;
12+
13+
#[derive(Args, Debug)]
14+
pub struct ContainerArgs {
15+
#[arg(
16+
short,
17+
long,
18+
global = true,
19+
env = "FEOS_ADDRESS",
20+
default_value = "http://[::1]:1337"
21+
)]
22+
pub address: String,
23+
24+
#[command(subcommand)]
25+
command: ContainerCommand,
26+
}
27+
28+
#[derive(Subcommand, Debug)]
29+
pub enum ContainerCommand {
30+
/// Create a new container
31+
Create {
32+
#[arg(
33+
long,
34+
required = true,
35+
help = "Container image reference (e.g., docker.io/library/alpine:latest)"
36+
)]
37+
image_ref: String,
38+
39+
#[arg(long, help = "Optional custom container identifier (UUID)")]
40+
id: Option<String>,
41+
42+
#[arg(
43+
long,
44+
help = "Override the default command of the image",
45+
num_args = 1..,
46+
value_delimiter = ' '
47+
)]
48+
cmd: Vec<String>,
49+
50+
#[arg(
51+
long,
52+
help = "Set environment variables (e.g., --env KEY1=VALUE1 --env KEY2=VALUE2)",
53+
value_parser = parse_key_val
54+
)]
55+
env: Vec<(String, String)>,
56+
},
57+
/// Start a created container
58+
Start {
59+
#[arg(required = true, help = "Container identifier")]
60+
id: String,
61+
},
62+
/// Stop a running container
63+
Stop {
64+
#[arg(required = true, help = "Container identifier")]
65+
id: String,
66+
},
67+
/// Get detailed information about a container
68+
Info {
69+
#[arg(required = true, help = "Container identifier")]
70+
id: String,
71+
},
72+
/// List all containers
73+
List,
74+
/// Delete a container
75+
Delete {
76+
#[arg(required = true, help = "Container identifier")]
77+
id: String,
78+
},
79+
}
80+
81+
fn parse_key_val(s: &str) -> Result<(String, String), String> {
82+
s.split_once('=')
83+
.map(|(key, value)| (key.to_string(), value.to_string()))
84+
.ok_or_else(|| format!("invalid KEY=value format: {}", s))
85+
}
86+
87+
pub async fn handle_container_command(args: ContainerArgs) -> Result<()> {
88+
let mut client = ContainerServiceClient::connect(args.address)
89+
.await
90+
.context("Failed to connect to container service")?;
91+
92+
match args.command {
93+
ContainerCommand::Create {
94+
image_ref,
95+
id,
96+
cmd,
97+
env,
98+
} => create_container(&mut client, image_ref, id, cmd, env).await?,
99+
ContainerCommand::Start { id } => start_container(&mut client, id).await?,
100+
ContainerCommand::Stop { id } => stop_container(&mut client, id).await?,
101+
ContainerCommand::Info { id } => get_container_info(&mut client, id).await?,
102+
ContainerCommand::List => list_containers(&mut client).await?,
103+
ContainerCommand::Delete { id } => delete_container(&mut client, id).await?,
104+
}
105+
106+
Ok(())
107+
}
108+
109+
async fn create_container(
110+
client: &mut ContainerServiceClient<Channel>,
111+
image_ref: String,
112+
id: Option<String>,
113+
cmd: Vec<String>,
114+
env: Vec<(String, String)>,
115+
) -> Result<()> {
116+
println!("Requesting container creation with image: {}...", image_ref);
117+
118+
let config = ContainerConfig {
119+
image_ref,
120+
command: cmd,
121+
env: env.into_iter().collect(),
122+
};
123+
124+
let request = CreateContainerRequest {
125+
config: Some(config),
126+
container_id: id,
127+
};
128+
129+
let response = client.create_container(request).await?.into_inner();
130+
println!(
131+
"Container creation initiated. Container ID: {}",
132+
response.container_id
133+
);
134+
println!(
135+
"Use 'feos-cli container list' to check its status and 'feos-cli container start {}' to run it.",
136+
response.container_id
137+
);
138+
139+
Ok(())
140+
}
141+
142+
async fn start_container(client: &mut ContainerServiceClient<Channel>, id: String) -> Result<()> {
143+
println!("Requesting to start container: {}...", id);
144+
let request = StartContainerRequest {
145+
container_id: id.clone(),
146+
};
147+
client.start_container(request).await?;
148+
println!("Start request sent for container: {}", id);
149+
Ok(())
150+
}
151+
152+
async fn stop_container(client: &mut ContainerServiceClient<Channel>, id: String) -> Result<()> {
153+
println!("Requesting to stop container: {}...", id);
154+
let request = StopContainerRequest {
155+
container_id: id.clone(),
156+
..Default::default()
157+
};
158+
client.stop_container(request).await?;
159+
println!("Stop request sent for container: {}", id);
160+
Ok(())
161+
}
162+
163+
async fn get_container_info(
164+
client: &mut ContainerServiceClient<Channel>,
165+
id: String,
166+
) -> Result<()> {
167+
let request = GetContainerRequest {
168+
container_id: id.clone(),
169+
};
170+
let response = client.get_container(request).await?.into_inner();
171+
172+
println!("Container Info for: {}", id);
173+
println!(
174+
" State: {:?}",
175+
ContainerState::try_from(response.state).unwrap_or(ContainerState::Unspecified)
176+
);
177+
if let Some(pid) = response.pid {
178+
println!(" PID: {}", pid);
179+
}
180+
if let Some(exit_code) = response.exit_code {
181+
println!(" Exit Code: {}", exit_code);
182+
}
183+
if let Some(config) = response.config {
184+
println!(" Config:");
185+
println!(" Image Ref: {}", config.image_ref);
186+
if !config.command.is_empty() {
187+
println!(" Command: {:?}", config.command);
188+
}
189+
if !config.env.is_empty() {
190+
println!(" Env: {:?}", config.env);
191+
}
192+
}
193+
194+
Ok(())
195+
}
196+
197+
async fn list_containers(client: &mut ContainerServiceClient<Channel>) -> Result<()> {
198+
let request = ListContainersRequest {};
199+
let response = client.list_containers(request).await?.into_inner();
200+
201+
if response.containers.is_empty() {
202+
println!("No containers found.");
203+
return Ok(());
204+
}
205+
206+
println!("{:<38} {:<15} IMAGE_REF", "CONTAINER_ID", "STATE");
207+
println!("{:-<38} {:-<15} {:-<40}", "", "", "");
208+
for container in response.containers {
209+
let state =
210+
ContainerState::try_from(container.state).unwrap_or(ContainerState::Unspecified);
211+
let image_ref = container
212+
.config
213+
.map(|c| c.image_ref)
214+
.unwrap_or_else(|| "N/A".to_string());
215+
println!(
216+
"{:<38} {:<15} {}",
217+
container.container_id,
218+
format!("{:?}", state),
219+
image_ref
220+
);
221+
}
222+
Ok(())
223+
}
224+
225+
async fn delete_container(client: &mut ContainerServiceClient<Channel>, id: String) -> Result<()> {
226+
println!("Requesting to delete container: {}...", id);
227+
let request = DeleteContainerRequest {
228+
container_id: id.clone(),
229+
};
230+
client.delete_container(request).await?;
231+
println!("Successfully deleted container: {}", id);
232+
Ok(())
233+
}

cli/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use anyhow::Result;
55
use clap::{Parser, Subcommand};
66

7+
mod container_commands;
78
mod host_commands;
89
mod image_commands;
910
mod vm_commands;
@@ -20,6 +21,7 @@ enum Service {
2021
Vm(vm_commands::VmArgs),
2122
Host(host_commands::HostArgs),
2223
Image(image_commands::ImageArgs),
24+
Container(container_commands::ContainerArgs),
2325
}
2426

2527
#[tokio::main]
@@ -35,6 +37,7 @@ async fn main() -> Result<()> {
3537
Service::Vm(args) => vm_commands::handle_vm_command(args).await?,
3638
Service::Host(args) => host_commands::handle_host_command(args).await?,
3739
Service::Image(args) => image_commands::handle_image_command(args).await?,
40+
Service::Container(args) => container_commands::handle_container_command(args).await?,
3841
}
3942

4043
Ok(())

feos/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vm-service = { path = "services/vm-service" }
1717
host-service = { path = "services/host-service" }
1818
image-service = { path = "services/image-service" }
1919
task-service = { path = "services/task-service" }
20+
container-service = { path = "services/container-service" }
2021
feos-proto = { workspace = true }
2122

2223
# Workspace dependencies

feos/proto/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ pub mod image_service {
1313
pub mod task_service {
1414
tonic::include_proto!("feos.task.v1");
1515
}
16+
pub mod container_service {
17+
tonic::include_proto!("feos.container.v1");
18+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "container-service"
3+
version.workspace = true
4+
edition.workspace = true
5+
6+
[dependencies]
7+
feos-proto = { workspace = true }
8+
image-service = { path = "../image-service" }
9+
task-service = { path = "../task-service" }
10+
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "macros", "uuid"] }
11+
serde = { workspace = true }
12+
serde_json = { workspace = true }
13+
hyper-util = { workspace = true }
14+
tower = { workspace = true }
15+
16+
# Workspace dependencies
17+
tokio = { workspace = true }
18+
tokio-stream = { workspace = true }
19+
tonic = { workspace = true }
20+
anyhow = { workspace = true }
21+
prost = { workspace = true }
22+
prost-types = { workspace = true }
23+
nix = { workspace = true }
24+
uuid = { workspace = true }
25+
log = { workspace = true }
26+
thiserror = { workspace = true }
27+
hyper = { workspace = true }
28+
29+
[build-dependencies]
30+
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "macros", "uuid"] }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
fn main() -> Result<(), Box<dyn std::error::Error>> {
5+
println!("cargo:rustc-env=SQLX_OFFLINE=true");
6+
Ok(())
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors
2+
-- SPDX-License-Identifier: Apache-2.0
3+
4+
CREATE TABLE IF NOT EXISTS containers (
5+
container_id TEXT PRIMARY KEY NOT NULL,
6+
image_uuid TEXT NOT NULL,
7+
state TEXT NOT NULL,
8+
pid INTEGER,
9+
config_blob BLOB NOT NULL,
10+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
11+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
12+
);
13+
14+
CREATE TRIGGER IF NOT EXISTS trigger_containers_updated_at
15+
AFTER UPDATE ON containers
16+
FOR EACH ROW
17+
BEGIN
18+
UPDATE containers SET updated_at = CURRENT_TIMESTAMP WHERE container_id = OLD.container_id;
19+
END;

0 commit comments

Comments
 (0)