diff --git a/docs/2-podman-docker.md b/docs/2-podman-docker.md index 79bf774..0234900 100644 --- a/docs/2-podman-docker.md +++ b/docs/2-podman-docker.md @@ -159,7 +159,7 @@ if the VM uses Ignition. ## SSH'ing into the VM Assuming the VM supports cloud-init or Ignition and exposes an SSH server on -port 22, you can `ssh` into it using podman-exec as the VMs default user: +port 22, you can `ssh` into it as root using podman-exec: > For this command to work with Docker, you must replace the `--latest` flag > with the container's name or ID. @@ -172,27 +172,43 @@ $ podman run \ "" 8068a2c180e0f4bf494f5e0baa37d9f13a9810f76b361c0771b73666e47ec383 -$ podman exec --latest fedora whoami +$ podman exec --latest whoami +Please login as the user "fedora" rather than the user "root". +``` + +This particular VM image does not allow logging in as root. To `ssh` into the VM +as a different user, specify its username using the `--as` option immediately +before the command (if any). You may need to pass in `--` before this option to +prevent podman-exec from trying to interpret it: + +```console +$ podman exec --latest -- --as fedora whoami fedora +``` -$ podman exec -it --latest fedora -[fedora@8068a2c180e0 ~]$ +If you just want a login shell, pass in an empty string as the command. The +following would be the output if this VM image allowed logging in as root: + +``` +$ podman exec -it --latest "" +[root@8068a2c180e0 ~]$ ``` -With cloud-init, the default user can vary between VM images. With Ignition, -`core` is considered to be the default user. In both cases, if the SSH server -allows password authentication, you should also be able to log in as any other -user. +You can also log in as a specific user: + +``` +$ podman exec -it --latest -- --as fedora +[fedora@8068a2c180e0 ~]$ +``` -The `fedora` argument to podman-exec above, which would typically correspond to -the command to be executed, determines instead the name of the user to `ssh` -into the VM as. A command can optionally be specified with further arguments. If -no command is specified, a login shell is initiated. In this case, you probably -also want to pass flags `-it` to podman-exec. +When the VM supports cloud-init, `authorized_keys` is automatically set up to +allow SSH access by podman-exec for users `root` and the default user as set in +the image's cloud-init configuration. With Ignition, this is set up for users +`root` and `core`. -If you actually just want to exec into the container in which the VM is running -(probably to debug some problem with crun-vm itself), pass in `-` as the -username. +> If you want to exec into the container in which the VM is running (probably to +> debug some problem with crun-vm itself), pass in the `--container` flag +> immediately before the command (if any). ## Port forwarding diff --git a/docs/3-kubernetes.md b/docs/3-kubernetes.md index e6045b2..83439d7 100644 --- a/docs/3-kubernetes.md +++ b/docs/3-kubernetes.md @@ -67,15 +67,13 @@ $ kubectl logs my-vm ### SSH'ing into the pod/VM Assuming the VM supports cloud-init or Ignition, you can also SSH into it using -`kubectl exec`, with the caveat that the user to SSH as is passed in place of -the command (this is the same behavior as with `podman exec` or `docker exec`; -see [SSH'ing into the VM]): +`kubectl exec`: ```console -$ kubectl exec my-vm -- fedora whoami +$ kubectl exec my-vm -- --as fedora whoami fedora -$ kubectl exec -it my-vm -- fedora +$ kubectl exec -it my-vm -- --as fedora bash [fedora@my-vm ~]$ ``` diff --git a/src/commands/create/first_boot.rs b/src/commands/create/first_boot.rs index 0bc6f64..941ba4c 100644 --- a/src/commands/create/first_boot.rs +++ b/src/commands/create/first_boot.rs @@ -17,7 +17,6 @@ pub struct FirstBootConfig<'a> { } impl FirstBootConfig<'_> { - /// Returns `true` if a cloud-init config should be passed to the VM. pub fn apply_to_cloud_init_config( &self, in_config_dir_path: Option>, @@ -132,11 +131,38 @@ impl FirstBootConfig<'_> { ssh_authorized_keys.push(self.container_public_key.into()); + let write_files = match user_data_mapping + .entry("write_files".into()) + .or_insert_with(|| serde_yaml::Value::Sequence(vec![])) + { + serde_yaml::Value::Sequence(v) => v, + _ => bail!("invalid user-data file"), + }; + + write_files.push({ + let mut m = serde_yaml::Mapping::new(); + m.insert("path".into(), "/root/.ssh/authorized_keys".into()); + m.insert( + "content".into(), + ("\n".to_string() + self.container_public_key).into(), + ); + m.insert("append".into(), true.into()); + m.into() + }); + // create block device symlinks and udev rules let block_device_symlinks = self.get_block_device_symlinks(); let block_device_udev_rules = self.get_block_device_udev_rules(); + if let Some(rules) = &block_device_udev_rules { + let mut mapping = serde_yaml::Mapping::new(); + mapping.insert("path".into(), "/etc/udev/rules.d/99-crun-vm.rules".into()); + mapping.insert("content".into(), rules.to_string().into()); + + write_files.push(mapping.into()); + } + if !block_device_symlinks.is_empty() || block_device_udev_rules.is_some() { let runcmd = match user_data_mapping .entry("runcmd".into()) @@ -166,22 +192,6 @@ impl FirstBootConfig<'_> { } } - if let Some(rules) = block_device_udev_rules { - let write_files = match user_data_mapping - .entry("write_files".into()) - .or_insert_with(|| serde_yaml::Value::Sequence(vec![])) - { - serde_yaml::Value::Sequence(v) => v, - _ => bail!("invalid user-data file"), - }; - - let mut mapping = serde_yaml::Mapping::new(); - mapping.insert("path".into(), "/etc/udev/rules.d/99-crun-vm.rules".into()); - mapping.insert("content".into(), rules.into()); - - write_files.push(mapping.into()); - } - // generate iso { @@ -263,15 +273,17 @@ impl FirstBootConfig<'_> { _ => bail!("invalid config file"), }; - let users_contains_core = users.iter().any(|u| match u { - serde_json::Value::Object(m) => m.get("name") == Some(&"core".into()), - _ => false, - }); + for user in ["root", "core"] { + let user_exists = users.iter().any(|u| match u { + serde_json::Value::Object(m) => m.get("name") == Some(&user.into()), + _ => false, + }); - if !users_contains_core { - users.push(serde_json::json!({ - "name": "core", - })); + if !user_exists { + users.push(serde_json::json!({ + "name": user, + })); + } } for user in users { @@ -280,7 +292,9 @@ impl FirstBootConfig<'_> { _ => bail!("invalid config file"), }; - if map.get("name") == Some(&"core".into()) { + let name = map.get("name"); + + if name == Some(&"root".into()) || name == Some(&"core".into()) { let keys = match map .entry("sshAuthorizedKeys") .or_insert_with(|| serde_json::json!([])) @@ -290,8 +304,6 @@ impl FirstBootConfig<'_> { }; keys.push(self.container_public_key.into()); - - break; } } diff --git a/src/commands/exec.rs b/src/commands/exec.rs index bac3246..e18ecac 100644 --- a/src/commands/exec.rs +++ b/src/commands/exec.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::Result; +use clap::Parser; use crate::crun::crun_exec; @@ -18,22 +19,7 @@ pub fn exec(global_args: &liboci_cli::GlobalOpts, args: &liboci_cli::Exec) -> Re let command = process.args().as_ref().expect("command specified"); - let ssh_user = command - .first() - .expect("first command argument is user to ssh as into the vm"); - - let mut new_command = vec![]; - - if ssh_user != "-" { - new_command.extend(["/crun-vm/exec.sh".to_string(), ssh_user.clone()]); - } - - new_command.extend(command.iter().skip(1).cloned()); - - if ssh_user == "-" && new_command.is_empty() { - new_command.push("/bin/bash".to_string()); - } - + let new_command = build_command(command); process.set_args(Some(new_command)); serde_json::to_writer( @@ -45,3 +31,37 @@ pub fn exec(global_args: &liboci_cli::GlobalOpts, args: &liboci_cli::Exec) -> Re Ok(()) } + +#[derive(Parser, Debug)] +#[clap(no_binary_name = true, disable_help_flag = true)] +struct ExecArgs { + #[clap(long = "as", default_value = "root")] + user: String, + + #[clap(long, conflicts_with = "user")] + container: bool, + + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + command: Vec, +} + +fn build_command(original_command: &Vec) -> Vec { + let mut args: ExecArgs = ExecArgs::parse_from(original_command); + + if args.command.starts_with(&["".to_string()]) { + args.command.remove(0); + } + + if args.container { + if args.command.is_empty() { + vec!["/bin/bash".to_string()] + } else { + args.command + } + } else { + ["/crun-vm/exec.sh".to_string(), args.user] + .into_iter() + .chain(args.command) + .collect() + } +}