feat: add servers description
This commit is contained in:
@@ -45,20 +45,22 @@ Deploy successful! (total 2.3s)
|
||||
Схема:
|
||||
|
||||
```toml
|
||||
[projects.my-node-app]
|
||||
workdir = "~/code/my-node-app" # где выполнять локальные команды (~ разворачивается)
|
||||
host = "my-vps" # алиас из ~/.ssh/config или IP
|
||||
user = "deploy" # опционально; иначе из ~/.ssh/config или юзер ОС
|
||||
port = 2222 # опционально; иначе из ~/.ssh/config, иначе 22
|
||||
key_path = "~/.ssh/id_ed25519" # опционально; иначе identity_file из ssh-config,
|
||||
# иначе ~/.ssh/id_rsa
|
||||
[servers.my-vps] # профиль сервера: описывается один раз
|
||||
host = "195.0.2.10" # алиас из ~/.ssh/config или IP
|
||||
port = 2222
|
||||
user = "deploy"
|
||||
key_path = "~/.ssh/id_ed25519"
|
||||
|
||||
[projects.my-node-app.local] # шаги сборки на вашей машине
|
||||
commands = ["npm ci", "npm run build"]
|
||||
[projects.my-node-app]
|
||||
server = "my-vps" # ссылка на профиль
|
||||
workdir = "~/code/my-node-app" # где выполнять локальные команды (~ разворачивается)
|
||||
|
||||
[projects.my-node-app.env] # переменные для локальных команд сборки
|
||||
PUBLIC_BASE_PATH = "/my-node-app"
|
||||
|
||||
[projects.my-node-app.local] # шаги сборки на вашей машине
|
||||
commands = ["npm ci", "npm run build"]
|
||||
|
||||
[[projects.my-node-app.sync]] # что заливать на сервер
|
||||
source = "dist" # файл или папка (относительно workdir)
|
||||
target = "/var/www/pages"
|
||||
@@ -67,7 +69,10 @@ target = "/var/www/pages"
|
||||
commands = ["pm2 restart pages"]
|
||||
```
|
||||
|
||||
Приоритет параметров SSH: deploy.toml → `~/.ssh/config` → дефолт.
|
||||
Проект может задать параметры подключения и напрямую (`host`, `user`, `port`,
|
||||
`key_path` вместо `server`) — тогда они имеют приоритет над профилем.
|
||||
|
||||
Приоритет параметров SSH: проект → `[servers.<name>]` → `~/.ssh/config` → дефолт.
|
||||
|
||||
### Windows-нюанс TOML
|
||||
|
||||
|
||||
+102
-3
@@ -11,14 +11,26 @@ pub const GLOBAL_DIR: &str = "xboctploy";
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub servers: BTreeMap<String, Server>,
|
||||
pub projects: BTreeMap<String, Project>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Server {
|
||||
pub host: String,
|
||||
pub user: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub key_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Project {
|
||||
pub workdir: PathBuf,
|
||||
pub host: String,
|
||||
pub server: Option<String>,
|
||||
pub host: Option<String>,
|
||||
pub user: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub key_path: Option<PathBuf>,
|
||||
@@ -50,8 +62,38 @@ impl Config {
|
||||
if self.projects.is_empty() {
|
||||
bail!("no [projects.*] sections defined");
|
||||
}
|
||||
for (name, project) in &mut self.projects {
|
||||
if project.host.trim().is_empty() {
|
||||
for (server_name, server) in &mut self.servers {
|
||||
if let Some(kp) = &mut server.key_path {
|
||||
*kp = expand_tilde(kp)
|
||||
.with_context(|| format!("server '{server_name}': invalid key_path"))?;
|
||||
}
|
||||
}
|
||||
let Self { servers, projects } = &mut self;
|
||||
for (name, project) in projects.iter_mut() {
|
||||
if let Some(server_name) = &project.server {
|
||||
let Some(server) = servers.get(server_name) else {
|
||||
let known = servers.keys().cloned().collect::<Vec<_>>().join(", ");
|
||||
bail!("project '{name}': unknown server '{server_name}' (defined: {known})");
|
||||
};
|
||||
if project.host.is_none() {
|
||||
project.host = Some(server.host.clone());
|
||||
}
|
||||
if project.user.is_none() {
|
||||
project.user = server.user.clone();
|
||||
}
|
||||
if project.port.is_none() {
|
||||
project.port = server.port;
|
||||
}
|
||||
if project.key_path.is_none() {
|
||||
project.key_path = server.key_path.clone();
|
||||
}
|
||||
}
|
||||
let Some(host) = &project.host else {
|
||||
bail!(
|
||||
"project '{name}': no host (set 'host' or reference a '[servers.*]' profile via 'server')"
|
||||
);
|
||||
};
|
||||
if host.trim().is_empty() {
|
||||
bail!("project '{name}': host must not be empty");
|
||||
}
|
||||
for (phase, cmds) in [
|
||||
@@ -193,6 +235,63 @@ hostt = "typo"
|
||||
assert!(parse(s).is_err());
|
||||
}
|
||||
|
||||
const SERVERS: &str = r#"
|
||||
[servers.main]
|
||||
host = "10.0.0.9"
|
||||
port = 2327
|
||||
user = "deploy"
|
||||
key_path = "~/.ssh/xbp_key"
|
||||
|
||||
[projects.web]
|
||||
workdir = "."
|
||||
server = "main"
|
||||
|
||||
[projects.api]
|
||||
workdir = "."
|
||||
server = "main"
|
||||
port = 2222
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn server_profile_fills_connection_params() {
|
||||
let cfg = parse(SERVERS).unwrap();
|
||||
let web = cfg.projects.get("web").unwrap();
|
||||
assert_eq!(web.host.as_deref(), Some("10.0.0.9"));
|
||||
assert_eq!(web.port, Some(2327));
|
||||
assert_eq!(web.user.as_deref(), Some("deploy"));
|
||||
let key = web.key_path.as_ref().unwrap();
|
||||
assert!(key.ends_with("xbp_key"), "{}", key.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_fields_override_server_profile() {
|
||||
let cfg = parse(SERVERS).unwrap();
|
||||
let api = cfg.projects.get("api").unwrap();
|
||||
assert_eq!(api.port, Some(2222));
|
||||
assert_eq!(api.user.as_deref(), Some("deploy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_server_is_clear_error() {
|
||||
let s = r#"
|
||||
[projects.web]
|
||||
workdir = "."
|
||||
server = "nope"
|
||||
"#;
|
||||
let err = parse(s).unwrap_err();
|
||||
assert!(err.to_string().contains("unknown server 'nope'"), "{err:#}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_host_without_server_is_error() {
|
||||
let s = r#"
|
||||
[projects.web]
|
||||
workdir = "."
|
||||
"#;
|
||||
let err = parse(s).unwrap_err();
|
||||
assert!(err.to_string().contains("no host"), "{err:#}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_commands_section_is_rejected() {
|
||||
let s = r#"
|
||||
|
||||
+4
-1
@@ -114,7 +114,10 @@ fn print_plan(project: &Project, local_cmds: &[String], remote_cmds: &[String])
|
||||
|
||||
fn connect(project: &Project) -> Result<ssh::Session> {
|
||||
let target = ssh::Target {
|
||||
host_alias: project.host.clone(),
|
||||
host_alias: project
|
||||
.host
|
||||
.clone()
|
||||
.expect("host is guaranteed by config validation"),
|
||||
user: project.user.clone(),
|
||||
port: project.port,
|
||||
key_path: project.key_path.clone(),
|
||||
|
||||
Reference in New Issue
Block a user