Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ee85a41
Initial plan
Copilot Oct 28, 2025
3287d6d
Initial plan
Copilot Oct 28, 2025
1282833
Automatically create NS record when creating a zone
Copilot Oct 28, 2025
1cfb448
Fix NS record mismatch by auto-creating NS record when zone is created
Copilot Oct 28, 2025
3a2dbe8
Remove hardcoded NS record from serializer and update tests
Copilot Oct 28, 2025
118bc37
Update serializer and tests to work with auto-created NS records
Copilot Oct 28, 2025
26d926e
Address code review feedback: improve comments and documentation
Copilot Oct 28, 2025
f70458f
chore: remove hardcoded nameserver record from zone serialization
kweonminsung Oct 29, 2025
e5c7f2d
Merge branch 'copilot/fix-ns-record-mismatch-again' into copilot/fix-…
kweonminsung Oct 29, 2025
30d29c4
test: update NS record value assertions to include trailing dot
kweonminsung Oct 29, 2025
9b76aa0
Merge pull request #44 from kweonminsung/copilot/fix-ns-record-mismatch
kweonminsung Oct 29, 2025
5e13f72
fix: update NS record value handling to remove trailing dot and impro…
kweonminsung Oct 29, 2025
edc6504
empty: fixing branch on issue
kweonminsung Oct 29, 2025
de3ea06
feat: add support for primary_ns_ipv6 and update related functionalities
kweonminsung Oct 29, 2025
e9e657f
chore: update openapi.yaml
kweonminsung Oct 29, 2025
b921714
chore: update openapi.yaml
kweonminsung Oct 29, 2025
1053746
fix: fixed conflicting option
kweonminsung Oct 29, 2025
29f1764
feat: prevent manual creation and updating of default records for pri…
kweonminsung Oct 30, 2025
c044670
refactor: simplify utility functions and improve code consistency
kweonminsung Oct 30, 2025
00dc68a
feat: add contraints for CNAME record of @
kweonminsung Oct 30, 2025
3061c37
fix: correct CNAME validation to use update_record_request name
kweonminsung Oct 30, 2025
72c2704
Merge pull request #50 from kweonminsung/48-feature-add-aaaa-support-…
kweonminsung Oct 30, 2025
15c3164
feat: added update openapi docs workflow
kweonminsung Oct 30, 2025
365bb40
chore(deps): update dependency node to v24
renovate[bot] Oct 30, 2025
6ff1e69
Merge pull request #51 from kweonminsung/renovate/node-24.x
kweonminsung Oct 30, 2025
6fbbdc1
feat: added err handler for unix socket busy problem
kweonminsung Oct 30, 2025
725c345
feat: refactor command handling to use DaemonCommandKind enum
kweonminsung Oct 30, 2025
19bb6a0
feat: simplify RecordType enum
kweonminsung Oct 30, 2025
9d139fe
Merge pull request #52 from kweonminsung/49-bug-fix-unix-socket-colli…
kweonminsung Oct 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[env]
RUST_TEST_THREADS = "1"
# RUST_BACKTRACE = "1"
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Checkout code
uses: actions/checkout@v5

- name: Build
run: cargo build --verbose

- name: Run tests
run: cargo test --verbose -- --test-threads=1
46 changes: 46 additions & 0 deletions .github/workflows/update-openapi-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Update OpenAPI Docs

on:
push:
branches: [ "main", "develop" ]
paths:
- "docs/openapi.yaml"
workflow_dispatch:

jobs:
build-docs:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"

- name: Install Redocly CLI
run: npm install -g @redocly/cli

- name: Build OpenAPI docs
working-directory: docs
run: |
npx @redocly/cli build-docs openapi.yaml -o index.html

- name: Commit & Push updated docs (only on push, not PR)
run: |
git config user.name "github-actions"
git config user.email "[email protected]"

git add docs/index.html

# Only commit and push if there are changes
if ! git diff --cached --quiet; then
git commit -m "chore: update openapi docs"
git push origin ${GITHUB_REF#refs/heads/}
else
echo "No changes to commit."
fi
24 changes: 17 additions & 7 deletions docs/index.html

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ components:
- name
- primary_ns
- primary_ns_ip
- primary_ns_ipv6
- admin_email
- ttl
- refresh
Expand All @@ -679,6 +680,10 @@ components:
type: string
format: ipv4
example: 127.0.0.1
primary_ns_ipv6:
type: string
format: ipv6
example: "::1"
admin_email:
type: string
format: email
Expand Down Expand Up @@ -724,10 +729,12 @@ components:
required:
- name
- primary_ns
- primary_ns_ip
- admin_email
- ttl
- serial
anyOf:
- required: [primary_ns_ip]
- required: [primary_ns_ipv6]
properties:
name:
type: string
Expand All @@ -739,6 +746,10 @@ components:
type: string
format: ipv4
example: 127.0.0.1
primary_ns_ipv6:
type: string
format: ipv6
example: "::1"
admin_email:
type: string
format: email
Expand Down
9 changes: 6 additions & 3 deletions src/api/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ pub struct GetZoneResponse {
pub id: i32,
pub name: String,
pub primary_ns: String,
pub primary_ns_ip: String,
pub primary_ns_ip: Option<String>,
pub primary_ns_ipv6: Option<String>,
pub admin_email: String,
pub ttl: i32,
pub serial: Option<i32>,
Expand All @@ -24,6 +25,7 @@ impl GetZoneResponse {
name: zone.name.clone(),
primary_ns: zone.primary_ns.clone(),
primary_ns_ip: zone.primary_ns_ip.clone(),
primary_ns_ipv6: zone.primary_ns_ipv6.clone(),
admin_email: zone.admin_email.clone(),
ttl: zone.ttl,
serial: Some(zone.serial),
Expand All @@ -50,7 +52,7 @@ impl GetRecordResponse {
GetRecordResponse {
id: record.id,
name: record.name.clone(),
record_type: record.record_type.to_str().to_string(),
record_type: record.record_type.to_string(),
value: record.value.clone(),
ttl: record.ttl,
priority: record.priority,
Expand All @@ -63,7 +65,8 @@ impl GetRecordResponse {
pub struct CreateZoneRequest {
pub name: String,
pub primary_ns: String,
pub primary_ns_ip: String,
pub primary_ns_ip: Option<String>,
pub primary_ns_ipv6: Option<String>,
pub admin_email: String,
pub ttl: i32,
pub serial: i32,
Expand Down
8 changes: 4 additions & 4 deletions src/api/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ impl AuthService {
};

// Check if the token is expired
if let Some(expires_at) = &stored_token.expires_at {
if Utc::now() > *expires_at {
return Err("Token has expired".to_string());
}
if let Some(expires_at) = &stored_token.expires_at
&& Utc::now() > *expires_at
{
return Err("Token has expired".to_string());
}

// Update last_used_at to current time
Expand Down
49 changes: 42 additions & 7 deletions src/api/service/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
record_history::RecordHistory,
},
},
serializer::utils::{to_relative_domain, to_fqdn},
log_error,
};
use chrono::Utc;
Expand Down Expand Up @@ -79,18 +80,23 @@ impl RecordService {
let record_type = RecordType::from_str(&create_record_request.record_type)
.map_err(|_| format!("Invalid record type: {}", create_record_request.record_type))?;

// CNAME validation for '@' name
if record_type == RecordType::CNAME && create_record_request.name == "@" {
return Err("CNAME record cannot have '@' as name".to_string());
}

// SOA validation
if record_type == RecordType::SOA {
log_error!("Cannot create SOA record manually");
return Err("Cannot create SOA record manually".to_string());
}

// Check if zone exists and fetch existing records in the zone for CNAME validation
match zone_repository
// Check if zone exists
let zone = match zone_repository
.get_by_id(create_record_request.zone_id)
.await
{
Ok(Some(_)) => {}
Ok(Some(zone)) => zone,
Ok(None) => {
return Err(format!(
"Zone with id {} not found",
Expand All @@ -103,7 +109,19 @@ impl RecordService {
}
};

// CNAME validation - fetch records from the same zone for efficient validation
// Prevent manual creation of records related to primary_ns
let primary_ns_relative_name = to_relative_domain(&to_fqdn(&zone.primary_ns), &zone.name);
if (record_type == RecordType::NS && create_record_request.name == "@")
|| (record_type == RecordType::A && create_record_request.name == primary_ns_relative_name)
|| (record_type == RecordType::AAAA && create_record_request.name == primary_ns_relative_name)
{
return Err(
"Cannot manually create records that are automatically generated for the primary NS"
.to_string(),
);
}

// CNAME validation
let existing_records_in_zone = match record_repository
.get_by_zone_id(create_record_request.zone_id)
.await
Expand Down Expand Up @@ -200,30 +218,47 @@ impl RecordService {
}?;

// Check if zone exists
match zone_repository
let zone = match zone_repository
.get_by_id(update_record_request.zone_id)
.await
{
Ok(Some(_)) => {}
Ok(Some(zone)) => zone,
Ok(None) => {
return Err("Zone not found".to_string());
}
Err(e) => {
log_error!("Failed to fetch zone: {}", e);
return Err("Failed to fetch zone".to_string());
}
}
};

// Validate record type
let record_type = RecordType::from_str(&update_record_request.record_type)
.map_err(|_| format!("Invalid record type: {}", update_record_request.record_type))?;

// CNAME validation for '@' name
if record_type == RecordType::CNAME && update_record_request.name == "@" {
return Err("CNAME record cannot have '@' as name".to_string());
}

// SOA validation
if record_type == RecordType::SOA {
log_error!("Cannot update to SOA record type");
return Err("Cannot update to SOA record type".to_string());
}

// Prevent manual update of records related to primary_ns
let primary_ns_relative_name = to_relative_domain(&to_fqdn(&zone.primary_ns), &zone.name);
if (record_type == RecordType::NS && update_record_request.name == "@")
|| (record_type == RecordType::A && update_record_request.name == primary_ns_relative_name)
|| (record_type == RecordType::AAAA && update_record_request.name == primary_ns_relative_name)
{
return Err(
"Cannot manually update records that are automatically generated for the primary NS"
.to_string(),
);
}

// CNAME validation
let existing_records = match record_repository
.get_by_name(&update_record_request.name)
Expand Down
20 changes: 20 additions & 0 deletions src/api/service/zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ impl ZoneService {
let zone_repository = get_zone_repository();
let zone_history_repository = get_zone_history_repository();

// Validate that at least one of primary_ns_ip or primary_ns_ipv6 is present
if create_zone_request.primary_ns_ip.is_none()
&& create_zone_request.primary_ns_ipv6.is_none()
{
return Err(
"At least one of primary_ns_ip or primary_ns_ipv6 must be provided".to_string(),
);
}

// Check if zone already exists
match zone_repository.get_by_name(&create_zone_request.name).await {
Ok(Some(_)) => {
Expand All @@ -61,6 +70,7 @@ impl ZoneService {
name: create_zone_request.name.clone(),
primary_ns: create_zone_request.primary_ns.clone(),
primary_ns_ip: create_zone_request.primary_ns_ip.clone(),
primary_ns_ipv6: create_zone_request.primary_ns_ipv6.clone(),
admin_email: create_zone_request.admin_email.clone(),
ttl: create_zone_request.ttl,
serial: create_zone_request.serial,
Expand Down Expand Up @@ -105,6 +115,15 @@ impl ZoneService {
let zone_repository = get_zone_repository();
let zone_history_repository = get_zone_history_repository();

// Validate that at least one of primary_ns_ip or primary_ns_ipv6 is present
if update_zone_request.primary_ns_ip.is_none()
&& update_zone_request.primary_ns_ipv6.is_none()
{
return Err(
"At least one of primary_ns_ip or primary_ns_ipv6 must be provided".to_string(),
);
}

// Check if zone exists
match zone_repository.get_by_id(zone_id).await {
Ok(Some(_)) => {}
Expand Down Expand Up @@ -139,6 +158,7 @@ impl ZoneService {
name: update_zone_request.name.clone(),
primary_ns: update_zone_request.primary_ns.clone(),
primary_ns_ip: update_zone_request.primary_ns_ip.clone(),
primary_ns_ipv6: update_zone_request.primary_ns_ipv6.clone(),
admin_email: update_zone_request.admin_email.clone(),
ttl: update_zone_request.ttl,
serial: update_zone_request.serial,
Expand Down
17 changes: 13 additions & 4 deletions src/cli/dns.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{log_debug, socket::client::DaemonSocketClient};
use crate::{
log_debug,
socket::{client::DaemonSocketClient, dto::DaemonCommandKind},
};
use clap::Subcommand;

#[derive(Subcommand, Debug)]
Expand All @@ -22,23 +25,29 @@ pub async fn handle_command(subcommand: DnsCommand) -> Result<(), String> {
}

async fn write_dns_config(client: &DaemonSocketClient) -> Result<(), String> {
let res = client.send_command("dns_write_config", None).await?;
let res = client
.send_command(DaemonCommandKind::DnsWriteConfig, None)
.await?;

log_debug!("DNS configuration write result: {:?}", res);

Ok(())
}

async fn reload_dns_config(client: &DaemonSocketClient) -> Result<(), String> {
let res = client.send_command("dns_reload", None).await?;
let res = client
.send_command(DaemonCommandKind::DnsReload, None)
.await?;

log_debug!("DNS configuration reload result: {:?}", res);

Ok(())
}

async fn get_dns_status(client: &DaemonSocketClient) -> Result<(), String> {
let res = client.send_command("dns_status", None).await?;
let res = client
.send_command(DaemonCommandKind::DnsStatus, None)
.await?;

log_debug!("DNS status result: {:?}", res);

Expand Down
9 changes: 7 additions & 2 deletions src/cli/status.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use crate::{
log_debug,
socket::{client::DaemonSocketClient, dto::DaemonStatusResponse},
socket::{
client::DaemonSocketClient,
dto::{DaemonCommandKind, DaemonStatusResponse},
},
};

pub async fn handle_command() -> Result<(), String> {
let client = DaemonSocketClient::new();

// Create socket request
let res = client.send_command("status", None).await?;
let res = client
.send_command(DaemonCommandKind::Status, None)
.await?;

log_debug!("Status command result: {:?}", res);

Expand Down
Loading