|
| 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 | +} |
0 commit comments