feat: add servers description
This commit is contained in:
@@ -45,20 +45,22 @@ Deploy successful! (total 2.3s)
|
|||||||
Схема:
|
Схема:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[projects.my-node-app]
|
[servers.my-vps] # профиль сервера: описывается один раз
|
||||||
workdir = "~/code/my-node-app" # где выполнять локальные команды (~ разворачивается)
|
host = "195.0.2.10" # алиас из ~/.ssh/config или IP
|
||||||
host = "my-vps" # алиас из ~/.ssh/config или IP
|
port = 2222
|
||||||
user = "deploy" # опционально; иначе из ~/.ssh/config или юзер ОС
|
user = "deploy"
|
||||||
port = 2222 # опционально; иначе из ~/.ssh/config, иначе 22
|
key_path = "~/.ssh/id_ed25519"
|
||||||
key_path = "~/.ssh/id_ed25519" # опционально; иначе identity_file из ssh-config,
|
|
||||||
# иначе ~/.ssh/id_rsa
|
|
||||||
|
|
||||||
[projects.my-node-app.local] # шаги сборки на вашей машине
|
[projects.my-node-app]
|
||||||
commands = ["npm ci", "npm run build"]
|
server = "my-vps" # ссылка на профиль
|
||||||
|
workdir = "~/code/my-node-app" # где выполнять локальные команды (~ разворачивается)
|
||||||
|
|
||||||
[projects.my-node-app.env] # переменные для локальных команд сборки
|
[projects.my-node-app.env] # переменные для локальных команд сборки
|
||||||
PUBLIC_BASE_PATH = "/my-node-app"
|
PUBLIC_BASE_PATH = "/my-node-app"
|
||||||
|
|
||||||
|
[projects.my-node-app.local] # шаги сборки на вашей машине
|
||||||
|
commands = ["npm ci", "npm run build"]
|
||||||
|
|
||||||
[[projects.my-node-app.sync]] # что заливать на сервер
|
[[projects.my-node-app.sync]] # что заливать на сервер
|
||||||
source = "dist" # файл или папка (относительно workdir)
|
source = "dist" # файл или папка (относительно workdir)
|
||||||
target = "/var/www/pages"
|
target = "/var/www/pages"
|
||||||
@@ -67,7 +69,10 @@ target = "/var/www/pages"
|
|||||||
commands = ["pm2 restart pages"]
|
commands = ["pm2 restart pages"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Приоритет параметров SSH: deploy.toml → `~/.ssh/config` → дефолт.
|
Проект может задать параметры подключения и напрямую (`host`, `user`, `port`,
|
||||||
|
`key_path` вместо `server`) — тогда они имеют приоритет над профилем.
|
||||||
|
|
||||||
|
Приоритет параметров SSH: проект → `[servers.<name>]` → `~/.ssh/config` → дефолт.
|
||||||
|
|
||||||
### Windows-нюанс TOML
|
### Windows-нюанс TOML
|
||||||
|
|
||||||
|
|||||||
+102
-3
@@ -11,14 +11,26 @@ pub const GLOBAL_DIR: &str = "xboctploy";
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
pub servers: BTreeMap<String, Server>,
|
||||||
pub projects: BTreeMap<String, Project>,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub workdir: PathBuf,
|
pub workdir: PathBuf,
|
||||||
pub host: String,
|
pub server: Option<String>,
|
||||||
|
pub host: Option<String>,
|
||||||
pub user: Option<String>,
|
pub user: Option<String>,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub key_path: Option<PathBuf>,
|
pub key_path: Option<PathBuf>,
|
||||||
@@ -50,8 +62,38 @@ impl Config {
|
|||||||
if self.projects.is_empty() {
|
if self.projects.is_empty() {
|
||||||
bail!("no [projects.*] sections defined");
|
bail!("no [projects.*] sections defined");
|
||||||
}
|
}
|
||||||
for (name, project) in &mut self.projects {
|
for (server_name, server) in &mut self.servers {
|
||||||
if project.host.trim().is_empty() {
|
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");
|
bail!("project '{name}': host must not be empty");
|
||||||
}
|
}
|
||||||
for (phase, cmds) in [
|
for (phase, cmds) in [
|
||||||
@@ -193,6 +235,63 @@ hostt = "typo"
|
|||||||
assert!(parse(s).is_err());
|
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]
|
#[test]
|
||||||
fn empty_commands_section_is_rejected() {
|
fn empty_commands_section_is_rejected() {
|
||||||
let s = r#"
|
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> {
|
fn connect(project: &Project) -> Result<ssh::Session> {
|
||||||
let target = ssh::Target {
|
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(),
|
user: project.user.clone(),
|
||||||
port: project.port,
|
port: project.port,
|
||||||
key_path: project.key_path.clone(),
|
key_path: project.key_path.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user