Compare commits

..

7 commits

8 changed files with 461 additions and 243 deletions

View file

@ -2,7 +2,7 @@ name: test
on:
push:
branches:
- "main"
- master
pull_request:
branches:
- "*"

524
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,12 @@
[package]
name = "gandi-live-dns"
version = "1.4.0"
description = "Automatically updates your IP address in Gandi's Live DNS. Makes it possible to use Gandi as a dynamic DNS system."
version = "1.5.0"
edition = "2021"
authors = ["Kaan Barmore-Genç <kaan@bgenc.net>"]
license = "MIT"
readme = "Readme.md"
repository = "https://github.com/SeriousBug/gandi-live-dns-rust"
[profile.release]
strip = "symbols"
@ -17,7 +21,7 @@ toml = "0.5"
json = "0.12"
serde = { version = "1.0", features = ["derive"] }
directories = "4.0"
clap = { version = "3.2", features = [
clap = { version = "4.0", features = [
"derive",
"cargo",
"unicode",
@ -26,11 +30,14 @@ clap = { version = "3.2", features = [
tokio = { version = "1.20", features = ["full"] }
futures = "0.3"
anyhow = "1.0"
governor = "0.4"
governor = "0.5"
async-trait = "0.1"
# TODO: Relies on a yet-unreleased interface. Switch to an actual crate release once available
die-exit = { git = "https://github.com/Xavientois/die.git", rev = "31d3801f4e21654b0b28430987b1e21fc7728676" }
die-exit-2 = "0.4"
[dev-dependencies]
httpmock = "0.6"
regex = "1.6"
[dev-dependencies.die-exit-2]
version = "0.4"
features = ["test"]

View file

@ -1,6 +1,6 @@
## gandi-live-dns-rust
[![tests](https://img.shields.io/github/workflow/status/SeriousBug/gandi-live-dns-rust/test?label=tests)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/test.yml) [![lint checks](https://img.shields.io/github/workflow/status/SeriousBug/gandi-live-dns-rust/lint%20checks?label=lints)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/lint.yml) [![Releases](https://img.shields.io/github/v/release/SeriousBug/gandi-live-dns-rust?include_prereleases)](https://github.com/SeriousBug/gandi-live-dns-rust/releases) [![Docker Image Size](https://img.shields.io/docker/image-size/seriousbug/gandi-live-dns-rust)](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust) [![MIT license](https://img.shields.io/github/license/SeriousBug/gandi-live-dns-rust)](https://github.com/SeriousBug/gandi-live-dns-rust/blob/master/LICENSE.txt)
[![tests](https://img.shields.io/github/workflow/status/SeriousBug/gandi-live-dns-rust/test?label=tests)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/test.yml) [![Test coverage report](https://img.shields.io/codecov/c/github/SeriousBug/gandi-live-dns-rust)](https://codecov.io/gh/SeriousBug/gandi-live-dns-rust) [![lint checks](https://img.shields.io/github/workflow/status/SeriousBug/gandi-live-dns-rust/lint%20checks?label=lints)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/lint.yml) [![Releases](https://img.shields.io/github/v/release/SeriousBug/gandi-live-dns-rust?include_prereleases)](https://github.com/SeriousBug/gandi-live-dns-rust/releases) [![Docker Image Size](https://img.shields.io/docker/image-size/seriousbug/gandi-live-dns-rust)](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust) [![MIT license](https://img.shields.io/github/license/SeriousBug/gandi-live-dns-rust)](https://github.com/SeriousBug/gandi-live-dns-rust/blob/master/LICENSE.txt)
A program that can set the IP addresses for configured DNS entries in
[Gandi](https://gandi.net)'s domain configuration. Thanks to Gandi's
@ -59,6 +59,12 @@ arm64, armv6, and armv7 platforms. Follow the steps below to use these images.
> has a full path to the config file (`$(pwd)/gandi.toml` part). Otherwise
> Docker will create a directory.
### From source
This package is also published on `crates.io` as
[gandi-live-dns](https://crates.io/crates/gandi-live-dns). If you would like to
build it from source and you have a working rust install, you can use `cargo install gandi-live-dns` to build and install it.
## Automation
The `Packaging` folder contains a Systemd service and timer, which you can use

View file

@ -44,25 +44,24 @@ declare -A DOCKER_TARGETS=(
)
# Get the version number
VERSION=$(sed -nr 's/^version *= *"([0-9.]+)"/\1/p' Cargo.toml)
VERSION=$(sed -nr 's/^version *= *"([0-9.]+)"/\1/p' Cargo.toml | head --lines=1)
# Make the builds
for target in "${!TARGETS[@]}"; do
echo Building "${target}"
cross build -j $(($(nproc) / 2)) --release --target "${target}"
if [[ "${target}" =~ .*"windows".* ]] ; then
if [[ "${target}" =~ .*"windows".* ]]; then
zip -j "gandi-live-dns.${VERSION}.${TARGETS[${target}]}.zip" target/"${target}"/release/gandi-live-dns.exe 1>/dev/null
else
tar -acf "gandi-live-dns.${VERSION}.${TARGETS[${target}]}.tar.xz" -C "target/${target}/release/" "gandi-live-dns"
fi
done
if [[ "$#" -ge 2 && "$1" = "--no-docker" ]] ; then
if [[ "$#" -ge 2 && "$1" = "--no-docker" ]]; then
echo "Exiting without releasing to docker"
exit 0
fi
# Copy files into place so Docker can get them easily
cd Docker
echo Building Docker images

View file

@ -5,10 +5,15 @@ use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
fn default_types() -> Vec<String> {
DEFAULT_TYPES.iter().map(|v| v.to_string()).collect()
}
#[derive(Deserialize, Debug)]
pub struct Entry {
pub name: String,
types: Option<Vec<String>>,
#[serde(default = "default_types")]
types: Vec<String>,
fqdn: Option<String>,
ttl: Option<u32>,
}
@ -54,11 +59,7 @@ impl Config {
}
pub fn types<'e>(entry: &'e Entry) -> Vec<&'e str> {
entry
.types
.as_ref()
.and_then(|ts| Some(ts.iter().map(|t| t.as_str()).collect()))
.unwrap_or_else(|| DEFAULT_TYPES.to_vec())
entry.types.iter().map(|t| t.as_str()).collect()
}
}
@ -68,7 +69,7 @@ fn load_config_from<P: std::convert::AsRef<std::path::Path>>(path: P) -> anyhow:
}
pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
match &opts.config {
let mut config = match &opts.config {
Some(config_path) => load_config_from(&config_path),
None => {
let confpath = ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns")
@ -85,7 +86,23 @@ pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
load_config_from(path)
})
}
}?;
// Filter out any types skipped in CLI opts
if opts.skip_ipv4 || opts.skip_ipv6 {
config.entry = config
.entry
.into_iter()
.map(|mut entry| {
entry.types = entry
.types
.into_iter()
.filter(|v| (v == "A" && !opts.skip_ipv4) || (v == "AAAA" && !opts.skip_ipv6))
.collect();
entry
})
.collect();
}
Ok(config)
}
pub fn validate_config(config: &Config) -> anyhow::Result<()> {
@ -117,6 +134,9 @@ fqdn = "example.com"
api_key = "xxx"
ttl = 300
[[entry]]
name = "www"
[[entry]]
name = "@"
"#,
@ -125,14 +145,18 @@ name = "@"
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
..Opts::default()
};
let conf = load_config(&opts).expect("Failed to load config file");
assert_eq!(conf.fqdn, "example.com");
assert_eq!(conf.api_key, "xxx");
assert_eq!(conf.ttl, 300);
assert_eq!(conf.entry.len(), 1);
assert_eq!(conf.entry[0].name, "@");
assert_eq!(conf.entry.len(), 2);
assert_eq!(conf.entry[0].name, "www");
assert_eq!(conf.entry[0].types, vec!["A".to_string()]);
assert_eq!(conf.entry[1].name, "@");
assert_eq!(conf.entry[1].types, vec!["A".to_string()]);
// default
assert_eq!(conf.ip_source, IPSourceName::Ipify);
}
@ -161,6 +185,7 @@ name = "@"
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
..Opts::default()
};
let conf = load_config(&opts).expect("Failed to load config file");
@ -172,4 +197,80 @@ name = "@"
assert_eq!(conf.entry[1].name, "@");
assert_eq!(conf.ip_source, IPSourceName::Icanhazip);
}
#[test]
fn load_config_skip_ipv4_with_opts() {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp).expect("Failed to create test dir");
temp.push("test-3.toml");
fs::write(
&temp,
r#"
fqdn = "example.com"
api_key = "yyy"
[[entry]]
name = "www"
types = ["A", "AAAA"]
[[entry]]
name = "@"
types = ["A", "AAAA"]
"#,
)
.expect("Failed to write test config file");
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
skip_ipv4: true,
..Opts::default()
};
let conf = load_config(&opts).expect("Failed to load config file");
assert_eq!(conf.fqdn, "example.com");
assert_eq!(conf.api_key, "yyy");
assert_eq!(conf.entry.len(), 2);
assert_eq!(conf.entry[0].name, "www");
assert_eq!(conf.entry[0].types, vec!["AAAA".to_string()]);
assert_eq!(conf.entry[1].name, "@");
assert_eq!(conf.entry[1].types, vec!["AAAA".to_string()]);
}
#[test]
fn load_config_skip_ipv6_with_opts() {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp).expect("Failed to create test dir");
temp.push("test-4.toml");
fs::write(
&temp,
r#"
fqdn = "example.com"
api_key = "yyy"
[[entry]]
name = "www"
types = ["A", "AAAA"]
[[entry]]
name = "@"
types = ["A", "AAAA"]
"#,
)
.expect("Failed to write test config file");
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
skip_ipv6: true,
..Opts::default()
};
let conf = load_config(&opts).expect("Failed to load config file");
assert_eq!(conf.fqdn, "example.com");
assert_eq!(conf.api_key, "yyy");
assert_eq!(conf.entry.len(), 2);
assert_eq!(conf.entry[0].name, "www");
assert_eq!(conf.entry[0].types, vec!["A".to_string()]);
assert_eq!(conf.entry[1].name, "@");
assert_eq!(conf.entry[1].types, vec!["A".to_string()]);
}
}

View file

@ -14,7 +14,7 @@ mod config;
mod gandi;
mod ip_source;
mod opts;
use die_exit::*;
use die_exit_2::*;
use governor;
/// 30 requests per minute, see https://api.gandi.net/docs/reference/
@ -179,6 +179,7 @@ mod tests {
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
..Opts::default()
};
let conf = config::load_config(&opts).expect("Failed to load config");
run::<IPSourceMock>(server.base_url().as_str(), conf)

View file

@ -1,10 +1,20 @@
use clap::Parser;
/// A tool to automatically update DNS entries on Gandi, using it as a dynamic DNS system.
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Default)]
#[clap(author, version, about, long_about = None, name = "gandi-live-dns")]
pub struct Opts {
/// The path to the configuration file.
#[clap(long)]
pub config: Option<String>,
/// Skip IPv4 updates.
///
/// If enabled, any IPv4 (A) records in the configuration file are ignored.
#[clap(action, long)]
pub skip_ipv4: bool,
/// Skip IPv4 updates.
///
/// If enabled, any IPv6 (AAAA) records in the configuration file are ignored.
#[clap(action, long)]
pub skip_ipv6: bool,
}