add powerdns client codes.

This commit is contained in:
dalbodeule 2024-06-05 20:50:43 +09:00
parent 07aa50cd3a
commit 3dd8fc69c1
No known key found for this signature in database
GPG Key ID: EFA860D069C9FA65
15 changed files with 372 additions and 176 deletions

View File

@ -31,6 +31,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2")
implementation("com.google.code.gson:gson:2.11.0")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")

7
docker_build.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# 현재 날짜와 시간을 YYMMDD(AM/PM)HHMM 형식으로 설정
current_time=$(date +'%y%m%d%p%H%M')
# Docker 이미지 빌드 명령 실행
docker build --no-cache -t dalbodeule/dnsapi:$current_time -t dalbodeule/dnsapi:latest .

View File

@ -3,6 +3,10 @@ package space.mori.dnsapi
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import io.github.cdimascio.dotenv.dotenv
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
@SpringBootApplication
class DnsapiApplication
@ -13,7 +17,9 @@ fun main(args: Array<String>) {
"DB_PORT" to dotenv["DB_PORT"],
"DB_NAME" to dotenv["DB_NAME"],
"DB_USER" to dotenv["DB_USER"],
"DB_PASSWORD" to dotenv["DB_PASSWORD"]
"DB_PASSWORD" to dotenv["DB_PASSWORD"],
"PDNS_API_KEY" to dotenv["PDNS_API_KEY"],
"PDNS_API_URL" to dotenv["PDNS_API_URL"],
)
runApplication<DnsapiApplication>(*args) {
@ -24,3 +30,8 @@ fun main(args: Array<String>) {
val dotenv = dotenv {
ignoreIfMissing = true
}
fun Date.getISOFormat(): String {
val offsetDateTime = OffsetDateTime.ofInstant(this.toInstant(), ZoneOffset.UTC)
return offsetDateTime.format(DateTimeFormatter.ISO_DATE_TIME)
}

View File

@ -0,0 +1,82 @@
package space.mori.dnsapi
import com.google.gson.Gson
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.*
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import space.mori.dnsapi.dto.RecordRequestDTO
@Service
class PowerDNSApiClient {
@Value("\${pdns.api.url}")
private lateinit var apiUrl: String
@Value("\${pdns.api.key}")
private lateinit var apiKey: String
private val restTemplate = RestTemplate()
private val gson = Gson()
private fun createHeaders(): HttpHeaders {
val headers = HttpHeaders()
headers.set("X-API-Key", apiKey)
headers.contentType = MediaType.APPLICATION_JSON
return headers
}
fun createDomain(name: String): ResponseEntity<String> {
val url = "$apiUrl/servers/localhost/zones"
val headers = createHeaders()
val domainRequest = DomainRequest("$name.", "Master", arrayOf(), arrayOf())
val body = gson.toJson(domainRequest)
val entity = HttpEntity(body, headers)
return restTemplate.exchange(url, HttpMethod.POST, entity, String::class.java)
}
fun createRecord(domainName: String, recordRequest: RecordRequestDTO): ResponseEntity<String> {
val url = "$apiUrl/servers/localhost/zones/$domainName."
val headers = createHeaders()
val record = RecordRequest(
name = "${recordRequest.name}.$domainName.",
type = recordRequest.type,
ttl = recordRequest.ttl,
changetype = "REPLACE",
records = arrayOf(RecordContent(recordRequest.content, false))
)
val body = gson.toJson(RecordRequestWrapper(arrayOf(record)))
val entity = HttpEntity(body, headers)
return restTemplate.exchange(url, HttpMethod.PATCH, entity, String::class.java)
}
fun deleteDomain(name: String): ResponseEntity<String> {
val url = "$apiUrl/servers/localhost/zones/$name."
val headers = createHeaders()
val entity = HttpEntity<String>(headers)
return restTemplate.exchange(url, HttpMethod.DELETE, entity, String::class.java)
}
private data class DomainRequest(
val name: String,
val kind: String,
val masters: Array<String>,
val nameservers: Array<String>
)
private data class RecordRequestWrapper(
val rrsets: Array<RecordRequest>
)
private data class RecordRequest(
val name: String,
val type: String,
val ttl: Int,
val changetype: String,
val records: Array<RecordContent>
)
private data class RecordContent(
val content: String,
val disabled: Boolean
)
}

View File

@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.ErrorResponse
import org.springframework.web.bind.annotation.*
import space.mori.dnsapi.db.Domain
import space.mori.dnsapi.dto.DomainResponseDTO
@ -68,5 +67,5 @@ class DomainController {
domainService!!.deleteDomain(cfid!!)
}
private fun Domain.toDTO() = DomainResponseDTO(cfid = cfid!!, domainName = domainName!!)
private fun Domain.toDTO() = DomainResponseDTO(id = cfid, name = name)
}

View File

@ -7,20 +7,23 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.jpa.domain.AbstractPersistable_.id
import org.springframework.web.bind.annotation.*
import space.mori.dnsapi.service.RecordService
import space.mori.dnsapi.db.Record
import space.mori.dnsapi.db.Domain
import space.mori.dnsapi.db.Record as DomainRecord
import space.mori.dnsapi.dto.RecordRequestDTO
import space.mori.dnsapi.dto.RecordResponseDTO
import space.mori.dnsapi.getISOFormat
import space.mori.dnsapi.service.RecordService
import java.util.*
@RestController
@RequestMapping("/record")
class RecordController {
@RequestMapping("/zones")
class RecordController(
@Autowired
private val recordService: RecordService? = null
@GetMapping
private val recordService: RecordService,
) {
@GetMapping("{zone_id}/dns_records")
@Operation(summary = "Get all records", tags=["record"])
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "Return All Records",
@ -28,11 +31,11 @@ class RecordController {
ApiResponse(responseCode = "400", description = "Bad request",
content = [Content(schema = Schema(implementation = Void::class))]),
])
fun allRecords(@PathVariable cfid: String?): List<RecordResponseDTO?> {
return recordService!!.getAllRecords(cfid!!).map{ it?.toDTO() }
fun allRecords(@PathVariable zone_id: String): List<RecordResponseDTO> {
return recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf()
}
@GetMapping("/{cfid}")
@GetMapping("{zone_id}/dns_records/{dns_record_id}")
@Operation(summary = "Get Record by ID", tags=["record"])
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "Return Record",
@ -40,11 +43,11 @@ class RecordController {
ApiResponse(responseCode = "400", description = "Bad request",
content = [Content(schema = Schema(implementation = Void::class))])
])
fun getRecordByCfid(@PathVariable cfid: String?): Optional<RecordResponseDTO> {
return recordService!!.getRecordById(cfid!!).map { it.toDTO() }
fun getRecordByCfid(@PathVariable zone_id: String, @PathVariable dns_record_id: String): RecordResponseDTO {
return recordService.getRecord(zone_id, dns_record_id)
}
@PostMapping
@PostMapping("{zone_id}/dns_records")
@Operation(summary = "Add Record by ID", tags=["record"])
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "Return Record",
@ -52,11 +55,11 @@ class RecordController {
ApiResponse(responseCode = "400", description = "Bad request",
content = [Content(schema = Schema(implementation = Void::class))]),
])
fun createRecord(@RequestBody record: RecordRequestDTO): RecordResponseDTO {
return recordService!!.createRecord(record).toDTO()
fun createRecord(@PathVariable zone_id: String, @RequestBody record: RecordRequestDTO): RecordResponseDTO {
return recordService.createRecord(zone_id, record)
}
@DeleteMapping("/{cfid}")
@DeleteMapping("{zone_id}/dns_records/{dns_record_id}")
@Operation(summary = "Remove Record by ID", tags=["record"])
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "Return Record",
@ -64,19 +67,33 @@ class RecordController {
ApiResponse(responseCode = "400", description = "Bad request",
content = [Content(schema = Schema(implementation = Void::class))]),
])
fun deleteRecord(@PathVariable cfid: String?) {
recordService!!.deleteRecord(cfid!!)
fun deleteRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String) {
recordService.deleteRecord(zone_id, dns_record_id)
}
private fun Record.toDTO() = RecordResponseDTO(
cfid = cfid!!,
name = name!!,
type = type!!,
content = content!!,
prio = prio!!,
ttl = ttl!!,
changeDate = changeDate!!,
auth = auth,
disabled = disabled
@PatchMapping("{zone_id}/dns_records/{dns_record_id}")
@Operation(summary = "Update Record by ID", tags=["record"])
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "Return Record",
content = [Content(schema = Schema(implementation = RecordResponseDTO::class))]),
ApiResponse(responseCode = "400", description = "Bad request",
content = [Content(schema = Schema(implementation = Void::class))]),
])
fun updateRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String, @RequestBody record: RecordRequestDTO): RecordResponseDTO {
return recordService.updateRecord(zone_id, dns_record_id, record)
}
private fun DomainRecord.toDTO() = RecordResponseDTO(
id = cfid,
type = type,
name = name,
content = content,
zoneId = domain.cfid,
zoneName = domain.name,
priority = prio,
ttl = ttl,
createdOn = createdOn.getISOFormat(),
modifiedOn = modifiedOn.getISOFormat(),
comment = comment
)
}

View File

@ -7,37 +7,17 @@ import java.util.*
@Entity
@Table(name = "domains")
class Domain {
data class Domain(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null
var id: Long? = null,
@Column(nullable = false, length = 255)
val domainName: String? = null
@Column(nullable = false, unique = true)
var name: String,
@Column(nullable = true, length = 128)
val master: String? = null
@Column(nullable = true, name = "last_check")
val lastCheck: Int? = null
@Column(nullable = false, length = 6)
val type: String? = null
@Column(nullable = true, name = "notified_serial")
val notifiedSerial: Int? = null
@Column(nullable = false, length = 128)
val account: String? = null
@Column(unique = true, nullable = false, length = 32)
var cfid: String? = null
@OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
private val records: Set<Record>? = null
@PrePersist
protected fun onCreate() {
this.cfid = UUID.randomUUID().toString().replace("-", "")
} // Getters and setters
@Column(nullable = false, unique = true)
var cfid: String = UUID.randomUUID().toString().replace("-", "")
) {
@OneToMany(mappedBy = "domain", cascade = [CascadeType.ALL], orphanRemoval = true)
var records: List<Record> = mutableListOf()
}

View File

@ -6,68 +6,28 @@ import java.util.*
@Entity
@Table(name = "records")
class Record {
data class Record(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null
var id: Long? = null,
@ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "domain_id", nullable = false)
var domain: Domain? = null
var domain: Domain,
@Column(nullable = false, length = 255)
var name: String? = null
var name: String,
var type: String,
var content: String,
var ttl: Int,
var prio: Int,
var disabled: Boolean,
var auth: Boolean,
@Column(nullable = false, length = 10)
var type: String? = null
var createdOn: Date,
var modifiedOn: Date,
@Column(nullable = false, length = 64000)
var content: String? = null
var comment: String,
@Column(nullable = true)
var ttl: Int?
@Column(nullable = true)
var prio: Int?
@Column(nullable = true)
var changeDate: Int?
var disabled: Boolean = false
var auth: Boolean = true
@Column(unique = true, nullable = false, length = 32)
var cfid: String? = null
@Column(nullable = true, length = 64)
var comment: String? = null
@PrePersist
private fun onCreate() {
this.cfid = UUID.randomUUID().toString().replace("-", "")
} // Getters and setters
constructor(
name: String,
type: String,
content: String,
changeDate: Int?,
disabled: Boolean,
domain: Domain,
comment: String?,
auth: Boolean = true,
ttl: Int? = 300,
prio: Int? = 0,
) {
this.name = name
this.type = type
this.content = content
this.ttl = ttl
this.prio = prio
this.changeDate = changeDate
this.disabled = disabled
this.auth = auth
this.domain = domain
this.comment = comment
}
}
@Column(nullable = false, unique = true)
var cfid: String = UUID.randomUUID().toString().replace("-", "")
)

View File

@ -12,8 +12,11 @@ interface RecordRepository : JpaRepository<Record?, Long?> {
fun findByCfid(cfid: String): Optional<Record>
@Transactional
fun deleteByCfid(cfid: String)
fun findByDomainIdAndCfid(domainId: Long, cfid: String): Optional<Record>
@Query("SELECT r FROM Record r WHERE r.domain.cfid = :domainCfid")
fun findByDomainCfid(@Param("domainCfid") cfid: String): List<Record>
@Transactional
fun deleteByCfid(cfid: String): Int
@Transactional
fun deleteByDomainIdAndCfid(domain_id: Long, cfid: String): Int
}

View File

@ -5,5 +5,5 @@ import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "Request DTO for Domain")
data class DomainRequestDTO(
@Schema(description = "Domain name(TLD)", example = "example.com")
val domainName: String
val name: String
)

View File

@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "Response DTO for Domain")
data class DomainResponseDTO(
@Schema(description = "Domain CFID", example = "123e4567e89b12d3a456426655440000")
val cfid: String,
val id: String,
@Schema(description = "Domain name(TLD)", example = "example.com")
val domainName: String
val name: String
)

View File

@ -4,20 +4,23 @@ import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "Request DTO for Record")
data class RecordRequestDTO(
@Schema(description = "Host name", example = "www")
val host: String,
@Schema(description = "Record type", example = "A")
val type: String,
@Schema(description = "Host name", example = "www.example.com.")
val name: String,
@Schema(description = "Record data", example = "192.0.2.1")
val data: String,
val content: String,
@Schema(description = "TTL (Time to Live)", example = "3600")
val ttl: Int,
val ttl: Int = 300,
@Schema(description = "Domain CFID", example = "123e4567e89b12d3a456426655440000")
val cfid: String,
@Schema(description = "Priority", example = "0")
val priority: Int? = null,
@Schema(description = "Proxied: cloudflare api compatibility", example = "false")
val proxied: Boolean = false,
@Schema(description = "comment", example="")
val comment: String

View File

@ -5,30 +5,45 @@ import java.util.*
@Schema(description = "Response DTO for Record")
data class RecordResponseDTO(
@Schema(description = "Record CFID", example = "123e4567e89b12d3a456426655440001")
val cfid: String,
@Schema(description = "Host name", example = "www.domain.tld")
val name: String,
@Schema(description = "Record ID", example = "123e4567e89b12d3a456426655440001")
val id: String,
@Schema(description = "Record type", example = "A")
val type: String,
var type: String,
@Schema(description = "Record data", example = "192.0.2.1")
val content: String,
@Schema(description = "Record name", example = "test.example.com")
var name: String,
@Schema(description = "TTL (Time to Live)", example = "3600")
val ttl: Int,
@Schema(description = "Record content", example = "1.1.1.1")
var content: String,
@Schema(description = "TTL per second", example = "300s")
val prio: Int,
@Schema(description = "Zone(TLD) ID", example = "123e4567e89b12d3a456426655440001")
val zoneId: String,
@Schema(description = "Changed date with Unix Timestamp")
val changeDate: Int,
@Schema(description = "Zone name(TLD)", example = "example.com")
val zoneName: String,
@Schema(description = "is disabled?", example = "false")
val disabled: Boolean,
@Schema(description = "Record creation time", example = "2014-01-01T05:20:00.12345Z")
val createdOn: String,
@Schema(description = "is authed", example = "true")
val auth: Boolean,
@Schema(description = "Record modification time", example = "2014-01-01T05:20:00.12345Z")
val modifiedOn: String,
@Schema(description = "Record priority", example = "0")
val priority: Int? = 0,
@Schema(description = "is proxyable: must false", example = "false")
val proxiable: Boolean = false,
@Schema(description = "is proxied: must false", example = "false")
val proxied: Boolean = false,
@Schema(description = "Record TTL", example = "300")
val ttl: Int = 300,
@Schema(description = "Record is locked: must false", example = "false")
val locked: Boolean = false,
@Schema(description = "Record comments", example = "")
val comment: String? = null,
)

View File

@ -2,45 +2,160 @@ package space.mori.dnsapi.service
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.mori.dnsapi.PowerDNSApiClient
import space.mori.dnsapi.db.DomainRepository
import space.mori.dnsapi.db.RecordRepository
import space.mori.dnsapi.db.Record as DomainRecord
import space.mori.dnsapi.db.RecordRepository
import space.mori.dnsapi.dto.DomainRequestDTO
import space.mori.dnsapi.dto.RecordRequestDTO
import space.mori.dnsapi.dto.RecordResponseDTO
import space.mori.dnsapi.getISOFormat
import java.util.*
@Service
class RecordService {
class RecordService(
@Autowired
private lateinit var domainRepository: DomainRepository
private val powerDNSApiClient: PowerDNSApiClient,
@Autowired
private val recordRepository: RecordRepository? = null
private val domainRepository: DomainRepository,
@Autowired
private val recordRepository: RecordRepository
) {
fun createRecord(domain_id: String, recordRequest: RecordRequestDTO): RecordResponseDTO {
val domain = domainRepository.findByCfid(domain_id)
if(domain.isEmpty) throw RuntimeException("Failed to find domain in API: $domain_id")
fun getAllRecords(cfid: String): List<DomainRecord?> {
return recordRepository!!.findByDomainCfid(cfid)
val response = powerDNSApiClient.createRecord(domain.get().name, recordRequest)
if (!response.statusCode.is2xxSuccessful) {
throw RuntimeException("Failed to create record in PowerDNS: ${response.body}")
}
fun getRecordById(cfid: String): Optional<DomainRecord> {
return recordRepository!!.findByCfid(cfid)
}
fun createRecord(record: RecordRequestDTO): DomainRecord {
val domain = domainRepository.findByCfid(record.cfid)
.orElseThrow { IllegalArgumentException("Invalid domain CFID") }
val r = DomainRecord(
name = record.host,
type = record.type,
content = record.data,
ttl = record.ttl,
domain = domain,
comment = record.comment,
changeDate = java.util.Date().time.toInt(),
disabled = false
val record = DomainRecord(
domain = domain.get(),
name = recordRequest.name,
type = recordRequest.type,
content = recordRequest.content,
ttl = recordRequest.ttl,
prio = recordRequest.priority ?: 0,
disabled = false,
auth = true,
createdOn = Date(),
modifiedOn = Date(),
comment = recordRequest.comment,
)
return RecordResponseDTO(
id = record.cfid,
type = record.type,
name = record.name,
content = record.content,
proxiable = false,
proxied = false,
ttl = record.ttl,
locked = false,
zoneId = record.cfid,
zoneName = domain.get().name,
createdOn = record.createdOn.getISOFormat(),
modifiedOn = record.modifiedOn.getISOFormat(),
priority = record.prio,
comment = record.comment
)
return recordRepository!!.save(r)
}
fun deleteRecord(cfid: String) {
recordRepository!!.deleteByCfid(cfid)
fun getRecord(domain_id: String, record_id: String): RecordResponseDTO {
val domain = domainRepository.findByCfid(domain_id)
if(domain.isEmpty) throw RuntimeException("Failed to find domain in API: $domain_id")
val record = domain.get().records.find { it.cfid == record_id }
if(record == null) throw RuntimeException("Failed to find record in API: $record_id")
return RecordResponseDTO(
id = record.cfid,
type = record.type,
name = record.name,
content = record.content,
ttl = record.ttl,
zoneId = record.domain.cfid,
zoneName = record.domain.name,
createdOn = record.createdOn.getISOFormat(),
modifiedOn = record.modifiedOn.getISOFormat(),
comment = record.comment,
)
}
fun getRecordsByDomain(domain_id: String): List<RecordResponseDTO>? {
val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") }
return domain?.records?.map { RecordResponseDTO(
id = it.cfid,
type = it.type,
name = it.name,
content = it.content,
zoneId = it.domain.cfid,
zoneName = it.domain.name,
priority = it.prio,
ttl = it.ttl,
createdOn = it.createdOn.getISOFormat(),
modifiedOn = it.modifiedOn.getISOFormat(),
comment = it.comment,
)}
}
@Transactional
fun updateRecord(domainId: String, cfid: String, updatedRecord: RecordRequestDTO): RecordResponseDTO {
// 도메인 조회
val domain = domainRepository.findByCfid(domainId)
.orElseThrow { RuntimeException("Domain not found") }
// 레코드 조회
val record = recordRepository.findByDomainIdAndCfid(domain.id!!, cfid)
.orElseThrow { RuntimeException("Record not found") }
// 레코드 업데이트
record.name = updatedRecord.name
record.type = updatedRecord.type
record.content = updatedRecord.content
record.ttl = updatedRecord.ttl
record.prio = updatedRecord.priority ?: 0
record.comment = updatedRecord.comment
record.modifiedOn = Date()
val response = powerDNSApiClient.createRecord(domain!!.name, updatedRecord)
if (!response.statusCode.is2xxSuccessful) {
throw RuntimeException("Failed to update record in PowerDNS: ${response.body}")
}
// 저장
val savedRecord = recordRepository.save(record)
return RecordResponseDTO(
id = savedRecord.cfid,
type = savedRecord.type,
name = savedRecord.name,
content = savedRecord.content,
proxiable = true,
proxied = false,
ttl = savedRecord.ttl,
locked = false,
zoneId = domain.cfid,
zoneName = domain.name,
createdOn = savedRecord.createdOn.getISOFormat(),
modifiedOn = savedRecord.modifiedOn.getISOFormat(),
priority = savedRecord.prio
)
}
fun deleteRecord(domain_id: String, record_id: String) {
val domain = domainRepository.findByCfid(domain_id).orElseThrow { RuntimeException("Failed to find domain in API: $domain_id") }
val deletedCount = recordRepository.deleteByDomainIdAndCfid(domain.id!!, record_id)
if(deletedCount == 0) throw RuntimeException("Failed to find record in API: $record_id")
}
fun deleteDomain(name: String) {
val response = powerDNSApiClient.deleteDomain(name)
if (!response.statusCode.is2xxSuccessful) {
throw RuntimeException("Failed to delete domain in PowerDNS: ${response.body}")
}
}
}

View File

@ -12,3 +12,5 @@ springdoc.api-docs.path=/api-docs
springdoc.default-consumes-media-type= application/json
springdoc.default-produces-media-type= application/json
springdoc.version= '@project.version@'
pdns.api.key=${PDNS_API_KEY}
pdns.api.url=${PDNS_API_URL}