mirror of
https://github.com/dalbodeule/sh0rt.kr-pdns.git
synced 2026-11-05 22:29:34 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029d1275b9 | ||
|
|
47461c505f |
No files matched your search
+44
-5
@@ -1,5 +1,5 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/gradle,java,kotlin,macos,intellij
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,java,kotlin,macos,intellij
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/gradle,java,kotlin,macos,linux,windows,intellij
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,java,kotlin,macos,linux,windows,intellij
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
@@ -153,6 +153,21 @@ replay_pid*
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
@@ -186,6 +201,32 @@ Temporary Items
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
**/build/
|
||||
@@ -213,6 +254,4 @@ gradle-app.setting
|
||||
# Java heap dump
|
||||
*.hprof
|
||||
|
||||
.env
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/gradle,java,kotlin,macos,intellij
|
||||
# End of https://www.toptal.com/developers/gitignore/api/gradle,java,kotlin,macos,linux,windows,intellij
|
||||
Generated
+9
@@ -3,4 +3,13 @@
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="dnsapi" options="-parameters" />
|
||||
<module name="dnsapi.aot" options="-parameters" />
|
||||
<module name="dnsapi.aotTest" options="-parameters" />
|
||||
<module name="dnsapi.test" options="-parameters" />
|
||||
<module name="space.mori.dnsapi.main" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-forest-configuration">
|
||||
<data version="2">.
|
||||
----------------------------------------
|
||||
1:0:97ab7754-c9e9-4e6f-b775-574d260344a3
|
||||
2:0:1b4583fc-5364-493b-be71-9d4c536f6902
|
||||
.</data>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+2
-1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.24" />
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.4.21" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SwiftToolchain">
|
||||
<option name="toolchain" value="xcode-/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+71
-35
@@ -1,26 +1,34 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("org.springframework.boot") version "3.3.0"
|
||||
id("io.spring.dependency-management") version "1.1.5"
|
||||
id("org.hibernate.orm") version "6.5.2.Final"
|
||||
id("org.graalvm.buildtools.native") version "0.10.2"
|
||||
kotlin("jvm") version "1.9.24"
|
||||
kotlin("plugin.spring") version "1.9.24"
|
||||
kotlin("plugin.jpa") version "1.9.24"
|
||||
id("org.springframework.boot") version "4.1.2"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
id("org.hibernate.orm") version "7.1.4.Final"
|
||||
id("org.graalvm.buildtools.native") version "1.1.17"
|
||||
kotlin("jvm") version "2.4.21"
|
||||
kotlin("plugin.spring") version "2.4.21"
|
||||
kotlin("plugin.jpa") version "2.4.21"
|
||||
}
|
||||
|
||||
val datetimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm")
|
||||
val jvmVersion = 25
|
||||
|
||||
group = "space.mori"
|
||||
version = "0.0.1"
|
||||
version = "1.0.0"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
toolchain.languageVersion.set(JavaLanguageVersion.of(jvmVersion))
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xjsr305=strict")
|
||||
jvmTarget.set(JvmTarget.fromTarget(jvmVersion.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -31,66 +39,94 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||
implementation("tools.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.2.0")
|
||||
|
||||
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-bindings
|
||||
implementation("org.springframework.cloud:spring-cloud-bindings:2.0.3")
|
||||
implementation("org.springframework.cloud:spring-cloud-bindings:2.0.4")
|
||||
|
||||
implementation("com.google.code.gson:gson:2.11.0")
|
||||
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
|
||||
implementation("com.google.code.gson:gson:2.15.0")
|
||||
implementation("io.github.cdimascio:dotenv-kotlin:6.5.1")
|
||||
|
||||
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:5.5.1")
|
||||
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver3:5.5.1")
|
||||
testRuntimeOnly("com.h2database:h2")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-Xjsr305=strict"
|
||||
jvmTarget = "21"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.named<Test>("test") {
|
||||
useJUnitPlatform { excludeTags("openapi-generation") }
|
||||
}
|
||||
|
||||
tasks.register<Test>("generateOpenApi") {
|
||||
group = "documentation"
|
||||
description = "Generate the root openapi.json from the running Spring application"
|
||||
dependsOn(tasks.testClasses)
|
||||
testClassesDirs = sourceSets.test.get().output.classesDirs
|
||||
classpath = sourceSets.test.get().runtimeClasspath
|
||||
useJUnitPlatform { includeTags("openapi-generation") }
|
||||
val openApiFile = layout.projectDirectory.file("openapi.json")
|
||||
outputs.file(openApiFile)
|
||||
systemProperty("openapi.output", openApiFile.asFile.absolutePath)
|
||||
}
|
||||
|
||||
tasks.register<Exec>("updateReadme") {
|
||||
group = "documentation"
|
||||
description = "Update README.md from the generated OpenAPI document"
|
||||
dependsOn("generateOpenApi")
|
||||
commandLine("python3", "scripts/update_readme.py")
|
||||
}
|
||||
|
||||
tasks.register("updateDocs") {
|
||||
group = "documentation"
|
||||
description = "Refresh openapi.json and README.md"
|
||||
dependsOn("updateReadme")
|
||||
}
|
||||
|
||||
tasks.withType<BootBuildImage> {
|
||||
val dockerId = System.getenv("DOCKER_USERNAME") ?: "git.mori.space/dalbodeule"
|
||||
val dockerPw = System.getenv("DOCKER_PASSWORD") ?: ""
|
||||
val dockerId = System.getenv("DOCKER_USERNAME") ?: "dalbodeule"
|
||||
val dockerPw = System.getenv("DOCKER_PASSWORD")
|
||||
|
||||
docker {
|
||||
publish = true
|
||||
publishRegistry {
|
||||
username = dockerId
|
||||
password = dockerPw
|
||||
if (!dockerPw.isNullOrBlank()) {
|
||||
publishRegistry {
|
||||
username = dockerId
|
||||
password = dockerPw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageName.set("$dockerId/dnsapi")
|
||||
tags.set(setOf("$dockerId/dnsapi:latest", "$dockerId/dnsapi:${datetimeFormatter.format(LocalDateTime.now())}"))
|
||||
if (providers.gradleProperty("multiArchImage").orNull == "true") {
|
||||
tags.set(emptySet<String>())
|
||||
} else {
|
||||
tags.set(setOf("$dockerId/dnsapi:latest", "$dockerId/dnsapi:${datetimeFormatter.format(LocalDateTime.now())}"))
|
||||
}
|
||||
buildpacks.set(setOf("docker.io/paketobuildpacks/oracle", "urn:cnb:builder:paketo-buildpacks/java-native-image"))
|
||||
|
||||
environment = mapOf(
|
||||
"BP_NATIVE_IMAGE" to "true",
|
||||
"BP_NATIVE_IMAGE_BUILD_ARGUMENTS" to "-H:+UnlockExperimentalVMOptions",
|
||||
"BP_JVM_TYPE" to "JDK",
|
||||
"BP_JVM_VERSION" to "21",
|
||||
"BP_JVM_VERSION" to jvmVersion.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
hibernate {
|
||||
enhancement {
|
||||
enableAssociationManagement.set(true)
|
||||
}
|
||||
enhancement {}
|
||||
}
|
||||
|
||||
tasks.named<org.springframework.boot.gradle.tasks.run.BootRun>("bootRun") {
|
||||
@@ -101,4 +137,4 @@ graalvmNative {
|
||||
binaries.all {
|
||||
buildArgs.add("--initialize-at-build-time=org.slf4j.helpers")
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+291
-322
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Cloudflare compatible PowerDNS API Server",
|
||||
"version": "v1.0.0"
|
||||
@@ -16,6 +16,159 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/zones/{zone_id}/dns_records/{dns_record_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Get Record by ID",
|
||||
"operationId": "getRecordByCfid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTORecordResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Update Record by ID",
|
||||
"operationId": "updateRecord",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RecordRequestDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTORecordResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Remove Record by ID",
|
||||
"operationId": "deleteRecord",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTODeleteResponseWithId"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/zones": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -176,159 +329,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/zones/{zone_id}/dns_records/{dns_record_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Get Record by ID",
|
||||
"operationId": "getRecordByCfid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTORecordResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Remove Record by ID",
|
||||
"operationId": "deleteRecord",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTODeleteResponseWithId"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"record"
|
||||
],
|
||||
"summary": "Update Record by ID",
|
||||
"operationId": "updateRecord",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "zone_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dns_record_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RecordRequestDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Return Record",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTORecordResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/zones/{cfid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -414,122 +414,9 @@
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"DomainRequestDTO": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name(TLD)",
|
||||
"example": "example.com"
|
||||
}
|
||||
},
|
||||
"description": "Request DTO for Domain"
|
||||
},
|
||||
"ApiResponseDTO": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorOrMessage": {
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResponseDTODomainResponseDTO": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/components/schemas/DomainResponseDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DomainResponseDTO": {
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Domain CFID",
|
||||
"example": "123e4567e89b12d3a456426655440000"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name(TLD)",
|
||||
"example": "example.com"
|
||||
}
|
||||
},
|
||||
"description": "Response DTO for Domain"
|
||||
},
|
||||
"RecordRequestDTO": {
|
||||
"required": [
|
||||
"comment",
|
||||
"content",
|
||||
"name",
|
||||
"proxied",
|
||||
"ttl",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"description": "Request DTO for Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
@@ -541,21 +428,24 @@
|
||||
"description": "Host name",
|
||||
"example": "www"
|
||||
},
|
||||
"content": {
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Record data",
|
||||
"example": "192.0.2.1"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "TTL (Time to Live)",
|
||||
"format": "int32",
|
||||
"description": "TTL (Time to Live)",
|
||||
"example": 3600
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32",
|
||||
"description": "Priority",
|
||||
"example": 0
|
||||
},
|
||||
"proxied": {
|
||||
@@ -567,15 +457,42 @@
|
||||
"type": "string",
|
||||
"description": "comment"
|
||||
}
|
||||
},
|
||||
"description": "Request DTO for Record"
|
||||
}
|
||||
},
|
||||
"ApiResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"result": {}
|
||||
}
|
||||
},
|
||||
"ErrorOrMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResponseDTORecordResponseDTO": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
@@ -594,26 +511,20 @@
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/components/schemas/RecordResponseDTO"
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/RecordResponseDTO"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecordResponseDTO": {
|
||||
"required": [
|
||||
"content",
|
||||
"createdOn",
|
||||
"id",
|
||||
"locked",
|
||||
"modifiedOn",
|
||||
"name",
|
||||
"proxiable",
|
||||
"proxied",
|
||||
"ttl",
|
||||
"type",
|
||||
"zoneId",
|
||||
"zoneName"
|
||||
],
|
||||
"type": "object",
|
||||
"description": "Response DTO for Record",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
@@ -656,9 +567,12 @@
|
||||
"example": "2014-01-01T05:20:00.12345Z"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Record priority",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32",
|
||||
"description": "Record priority",
|
||||
"example": 0
|
||||
},
|
||||
"proxiable": {
|
||||
@@ -673,8 +587,8 @@
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Record TTL",
|
||||
"format": "int32",
|
||||
"description": "Record TTL",
|
||||
"example": 300
|
||||
},
|
||||
"locked": {
|
||||
@@ -683,18 +597,26 @@
|
||||
"example": false
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Record comments"
|
||||
}
|
||||
},
|
||||
"description": "Response DTO for Record"
|
||||
}
|
||||
},
|
||||
"ApiResponseDTOListDomainResponseDTO": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"DomainRequestDTO": {
|
||||
"type": "object",
|
||||
"description": "Request DTO for Domain",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name(TLD)",
|
||||
"example": "example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResponseDTODomainResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
@@ -713,7 +635,56 @@
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DomainResponseDTO"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"DomainResponseDTO": {
|
||||
"type": "object",
|
||||
"description": "Response DTO for Domain",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Domain CFID",
|
||||
"example": "123e4567e89b12d3a456426655440000"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name(TLD)",
|
||||
"example": "example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResponseDTOListDomainResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ErrorOrMessage"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DomainResponseDTO"
|
||||
}
|
||||
@@ -721,11 +692,6 @@
|
||||
}
|
||||
},
|
||||
"ApiResponseDTOListRecordResponseDTO": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
@@ -744,7 +710,10 @@
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"type": "array",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RecordResponseDTO"
|
||||
}
|
||||
@@ -752,11 +721,6 @@
|
||||
}
|
||||
},
|
||||
"ApiResponseDTODeleteResponseWithId": {
|
||||
"required": [
|
||||
"errors",
|
||||
"messages",
|
||||
"success"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
@@ -775,14 +739,18 @@
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/components/schemas/DeleteResponseWithId"
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DeleteResponseWithId"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeleteResponseWithId": {
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -794,8 +762,9 @@
|
||||
"securitySchemes": {
|
||||
"api token": {
|
||||
"type": "http",
|
||||
"name": "api token",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <image-tag>" >&2
|
||||
echo "Set IMAGE_REPOSITORY to override the default repository." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
tag=$1
|
||||
if [[ ! $tag =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,121}$ ]]; then
|
||||
echo "Invalid Docker image tag: $tag" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
repository=${IMAGE_REPOSITORY:-${DOCKER_USERNAME:-dalbodeule}/dnsapi}
|
||||
publish_latest=${PUBLISH_LATEST:-1}
|
||||
dry_run=${DRY_RUN:-0}
|
||||
resume=${RESUME:-0}
|
||||
root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
||||
if [[ $publish_latest != 0 && $publish_latest != 1 ]]; then
|
||||
echo "PUBLISH_LATEST must be 0 or 1" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ $dry_run != 0 && $dry_run != 1 ]]; then
|
||||
echo "DRY_RUN must be 0 or 1" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ $resume != 0 && $resume != 1 ]]; then
|
||||
echo "RESUME must be 0 or 1" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ $dry_run == 0 ]]; then
|
||||
command -v docker >/dev/null || { echo "Docker is required" >&2; exit 1; }
|
||||
docker buildx version >/dev/null
|
||||
fi
|
||||
|
||||
run() {
|
||||
if [[ $dry_run == 1 ]]; then
|
||||
printf 'DRY RUN:'
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
cd "$root_dir"
|
||||
for arch in amd64 arm64; do
|
||||
image="$repository:$tag-$arch"
|
||||
if [[ $resume == 1 && $dry_run == 0 ]] && docker image inspect "$image" >/dev/null 2>&1; then
|
||||
actual_platform=$(docker image inspect "$image" --format '{{.Os}}/{{.Architecture}}')
|
||||
if [[ $actual_platform != "linux/$arch" ]]; then
|
||||
echo "$image has platform $actual_platform, expected linux/$arch" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Reusing local image $image"
|
||||
else
|
||||
run ./gradlew bootBuildImage \
|
||||
-PmultiArchImage=true \
|
||||
"--imagePlatform=linux/$arch" \
|
||||
"--imageName=$image" \
|
||||
--no-publishImage
|
||||
fi
|
||||
run docker push "$image"
|
||||
done
|
||||
|
||||
manifest_args=(docker buildx imagetools create --tag "$repository:$tag")
|
||||
if [[ $publish_latest == 1 ]]; then
|
||||
manifest_args+=(--tag "$repository:latest")
|
||||
fi
|
||||
manifest_args+=("$repository:$tag-amd64" "$repository:$tag-arm64")
|
||||
run "${manifest_args[@]}"
|
||||
run docker buildx imagetools inspect "$repository:$tag"
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refresh the generated README section from the root OpenAPI document."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
START = "<!-- BEGIN GENERATED API -->"
|
||||
END = "<!-- END GENERATED API -->"
|
||||
METHODS = ("get", "post", "put", "patch", "delete")
|
||||
|
||||
|
||||
def schema_type(schema):
|
||||
if not schema:
|
||||
return "—"
|
||||
if "$ref" in schema:
|
||||
name = schema["$ref"].rsplit("/", 1)[-1]
|
||||
return f"[{name}](#{name.lower()})"
|
||||
if "oneOf" in schema:
|
||||
return " or ".join(schema_type(part) for part in schema["oneOf"])
|
||||
kind = schema.get("type", "object")
|
||||
if isinstance(kind, list):
|
||||
return " or ".join(
|
||||
f"array of {schema_type(schema.get('items'))}" if part == "array" else part
|
||||
for part in kind
|
||||
)
|
||||
if kind == "array":
|
||||
return f"array of {schema_type(schema.get('items'))}"
|
||||
return kind
|
||||
|
||||
|
||||
def media_schema(content):
|
||||
return schema_type(content.get("application/json", {}).get("schema"))
|
||||
|
||||
|
||||
def cell(value):
|
||||
return str(value or "—").replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
|
||||
def generate(spec):
|
||||
lines = [
|
||||
"Generated from [openapi.json](openapi.json). Do not edit this section by hand.",
|
||||
"",
|
||||
"### Endpoints",
|
||||
"",
|
||||
"| Method | Path | Summary | Request body | Success response |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
|
||||
for path, operations in sorted(spec["paths"].items()):
|
||||
for method in METHODS:
|
||||
operation = operations.get(method)
|
||||
if not operation:
|
||||
continue
|
||||
request = media_schema(operation.get("requestBody", {}).get("content", {}))
|
||||
responses = operation.get("responses", {})
|
||||
success_code = next((code for code in sorted(responses) if code.startswith("2")), None)
|
||||
response = responses.get(success_code, {})
|
||||
result = media_schema(response.get("content", {}))
|
||||
lines.append(
|
||||
f"| `{method.upper()}` | `{path}` | {cell(operation.get('summary'))} | "
|
||||
f"{cell(request)} | {cell(success_code)}: {cell(result)} |"
|
||||
)
|
||||
|
||||
lines.extend(["", "### Schemas", ""])
|
||||
for name, schema in sorted(spec.get("components", {}).get("schemas", {}).items()):
|
||||
lines.extend([f"#### {name}", ""])
|
||||
if schema.get("description"):
|
||||
lines.extend([schema["description"], ""])
|
||||
properties = schema.get("properties", {})
|
||||
if not properties:
|
||||
lines.extend([f"Type: {schema_type(schema)}", ""])
|
||||
continue
|
||||
required = set(schema.get("required", []))
|
||||
lines.extend(["| Field | Type | Required | Description |", "| --- | --- | --- | --- |"])
|
||||
for field, definition in properties.items():
|
||||
lines.append(
|
||||
f"| `{field}` | {cell(schema_type(definition))} | "
|
||||
f"{'Yes' if field in required else 'No'} | {cell(definition.get('description'))} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
spec = json.loads((ROOT / "openapi.json").read_text(encoding="utf-8"))
|
||||
readme_path = ROOT / "README.md"
|
||||
readme = readme_path.read_text(encoding="utf-8")
|
||||
if readme.count(START) != 1 or readme.count(END) != 1:
|
||||
raise ValueError("README.md must contain one pair of generated API markers")
|
||||
before, remainder = readme.split(START, 1)
|
||||
_, after = remainder.split(END, 1)
|
||||
readme_path.write_text(before + START + "\n" + generate(spec) + END + after, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,10 +6,10 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import java.lang.reflect.Type
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@Service
|
||||
@@ -21,40 +21,46 @@ class PowerDNSAPIClient() {
|
||||
private lateinit var apiKey: String
|
||||
|
||||
private val gson = Gson()
|
||||
private val client = OkHttpClient()
|
||||
private val client = OkHttpClient.Builder()
|
||||
.callTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun createZone(zoneName: String): Response {
|
||||
try {
|
||||
val body = gson.toJson(
|
||||
mapOf(
|
||||
"name" to "$zoneName.",
|
||||
"kind" to "Primary"
|
||||
)
|
||||
).toRequestBody()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/api/v1/servers/localhost/zones")
|
||||
.addHeader("X-API-Key", apiKey)
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build()
|
||||
private fun execute(request: Request) {
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) return
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
val str = response.body?.string()
|
||||
val error = gson.fromJson(str, PowerDNSAPIError::class.java)
|
||||
throw PowerDNSAPIException(error)
|
||||
}
|
||||
return response
|
||||
} catch(ex: Exception) {
|
||||
println(ex)
|
||||
throw ex
|
||||
val error = runCatching {
|
||||
gson.fromJson(response.peekBody(64 * 1024L).string(), PowerDNSAPIError::class.java)
|
||||
}.getOrNull()
|
||||
throw PowerDNSAPIException(
|
||||
if (error?.error.isNullOrBlank())
|
||||
PowerDNSAPIError("PowerDNS API returned HTTP ${response.code}", null)
|
||||
else error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun deleteZone(zoneName: String): Response {
|
||||
fun createZone(zoneName: String) {
|
||||
val body = gson.toJson(
|
||||
mapOf(
|
||||
"name" to "$zoneName.",
|
||||
"kind" to "Primary"
|
||||
)
|
||||
).toRequestBody()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/api/v1/servers/localhost/zones")
|
||||
.addHeader("X-API-Key", apiKey)
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
execute(request)
|
||||
}
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun deleteZone(zoneName: String) {
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/api/v1/servers/localhost/zones/$zoneName.")
|
||||
.addHeader("X-API-Key", apiKey)
|
||||
@@ -63,17 +69,11 @@ class PowerDNSAPIClient() {
|
||||
.delete()
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if(!response.isSuccessful) {
|
||||
val str = response.body?.string()
|
||||
val error = gson.fromJson(str, PowerDNSAPIError::class.java)
|
||||
throw PowerDNSAPIException(error)
|
||||
}
|
||||
return response
|
||||
execute(request)
|
||||
}
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun createRecord(zoneName: String, recordName: String, recordType: String, recordContent: String, ttl: Int = 300, priority: Int = 0): Response {
|
||||
fun createRecord(zoneName: String, recordName: String, recordType: String, recordContent: String, ttl: Int = 300, priority: Int = 0) {
|
||||
val rrset = mapOf(
|
||||
"name" to recordName,
|
||||
"type" to recordType,
|
||||
@@ -95,17 +95,11 @@ class PowerDNSAPIClient() {
|
||||
.patch(body)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if(!response.isSuccessful) {
|
||||
val str = response.body?.string()
|
||||
val error = gson.fromJson(str, PowerDNSAPIError::class.java)
|
||||
throw PowerDNSAPIException(error)
|
||||
}
|
||||
return response
|
||||
execute(request)
|
||||
}
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun updateRecord(zoneName: String, recordName: String, recordType: String, recordContent: String, ttl: Int = 300, priority: Int = 0): Response {
|
||||
fun updateRecord(zoneName: String, recordName: String, recordType: String, recordContent: String, ttl: Int = 300, priority: Int = 0) {
|
||||
val rrset = mapOf(
|
||||
"name" to recordName,
|
||||
"type" to recordType,
|
||||
@@ -127,17 +121,11 @@ class PowerDNSAPIClient() {
|
||||
.patch(body)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if(!response.isSuccessful) {
|
||||
val str = response.body?.string()
|
||||
val error = gson.fromJson(str, PowerDNSAPIError::class.java)
|
||||
throw PowerDNSAPIException(error)
|
||||
}
|
||||
return response
|
||||
execute(request)
|
||||
}
|
||||
|
||||
@Throws(PowerDNSAPIException::class)
|
||||
fun deleteRecord(zoneName: String, recordName: String, recordType: String): Response {
|
||||
fun deleteRecord(zoneName: String, recordName: String, recordType: String) {
|
||||
val rrset = mapOf(
|
||||
"name" to recordName,
|
||||
"type" to recordType,
|
||||
@@ -153,13 +141,7 @@ class PowerDNSAPIClient() {
|
||||
.patch(body)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if(!response.isSuccessful) {
|
||||
val str = response.body?.string()
|
||||
val error = gson.fromJson(str, PowerDNSAPIError::class.java)
|
||||
throw PowerDNSAPIException(error)
|
||||
}
|
||||
return response
|
||||
execute(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,4 +174,4 @@ class PowerDNSAPIException(private val error: PowerDNSAPIError): RuntimeExceptio
|
||||
get() = error.errors
|
||||
|
||||
override fun toString(): String = "PowerDNSAPIException(${error.error} ${errors?.joinToString(", ") ?: ""})"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class RecordController(
|
||||
content = [Content(schema = Schema(implementation = ApiResponseDTO::class))]),
|
||||
])
|
||||
fun allRecords(@PathVariable zone_id: String): ApiResponseDTO<List<RecordResponseDTO>> {
|
||||
return ApiResponseDTO(result = recordService.getRecordsByDomain(zone_id)?.map{ it } ?: listOf())
|
||||
return ApiResponseDTO(result = recordService.getRecordsByDomain(zone_id))
|
||||
}
|
||||
|
||||
@GetMapping("{zone_id}/dns_records/{dns_record_id}")
|
||||
@@ -71,4 +71,4 @@ class RecordController(
|
||||
fun updateRecord(@PathVariable zone_id: String, @PathVariable dns_record_id: String, @RequestBody record: RecordRequestDTO): ApiResponseDTO<RecordResponseDTO> {
|
||||
return ApiResponseDTO(result = recordService.updateRecord(zone_id, dns_record_id, record))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.*
|
||||
|
||||
|
||||
interface DomainRepository : JpaRepository<Domain?, Long?> {
|
||||
interface DomainRepository : JpaRepository<Domain, Long> {
|
||||
@Transactional
|
||||
fun findByCfid(cfid: String): Optional<Domain>
|
||||
|
||||
@@ -14,4 +14,4 @@ interface DomainRepository : JpaRepository<Domain?, Long?> {
|
||||
|
||||
@Transactional
|
||||
fun deleteByCfid(cfid: String): Int
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,18 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.*
|
||||
|
||||
|
||||
interface RecordRepository : JpaRepository<Record?, Long?> {
|
||||
interface RecordRepository : JpaRepository<Record, Long> {
|
||||
@Transactional
|
||||
fun findByCfid(cfid: String): Optional<Record>
|
||||
|
||||
@Transactional
|
||||
fun findByDomainIdAndCfid(domainId: Long, cfid: String): Optional<Record>
|
||||
|
||||
fun findAllByDomainId(domainId: Long): List<Record>
|
||||
|
||||
@Transactional
|
||||
fun deleteByCfid(cfid: String): Int
|
||||
|
||||
@Transactional
|
||||
fun deleteByDomainIdAndCfid(domain_id: Long, cfid: String): Int
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ data class User(
|
||||
@Column(nullable = false, unique = false, length = 20)
|
||||
val name: String,
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
val domains: List<Domain> = mutableListOf(),
|
||||
) {
|
||||
override fun toString(): String = "User(id=$id, name='$name')"
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,5 @@ class UserFilter(
|
||||
}
|
||||
|
||||
fun getCurrentUser(): User =
|
||||
SecurityContextHolder.getContext().authentication.principal as User
|
||||
SecurityContextHolder.getContext().authentication?.principal as? User
|
||||
?: throw IllegalStateException("Authenticated user is required")
|
||||
@@ -20,7 +20,6 @@ class DomainService(
|
||||
try {
|
||||
val user = getCurrentUser()
|
||||
val domain = domainRepository.findAllByUser(user)
|
||||
if (domain.isEmpty()) throw RuntimeException("Unauthorized")
|
||||
|
||||
return domain
|
||||
} catch(ex: Error) {
|
||||
@@ -62,14 +61,17 @@ class DomainService(
|
||||
val domain = domainRepository.findByCfid(domain_id).orElseThrow {
|
||||
throw RuntimeException("Domain with CFID $domain_id not found")
|
||||
}
|
||||
val user = getCurrentUser()
|
||||
if (domain.user.id != user.id)
|
||||
throw RuntimeException("Unauthorized to delete domain in API: $domain_id")
|
||||
|
||||
powerDNSApiClient.deleteZone(domain.name)
|
||||
val count = domainRepository.deleteByCfid(domain_id)
|
||||
|
||||
if (count > 0) throw RuntimeException("Domain with CFID $domain_id not found")
|
||||
if (count == 0) throw RuntimeException("Domain with CFID $domain_id not found")
|
||||
return domain_id
|
||||
} catch(ex: Error) {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class RecordService(
|
||||
proxied = false,
|
||||
ttl = record.ttl,
|
||||
locked = false,
|
||||
zoneId = record.domain.cfid,
|
||||
zoneId = domain.cfid,
|
||||
zoneName = domain.name,
|
||||
createdOn = record.createdOn.getISOFormat(),
|
||||
modifiedOn = record.modifiedOn.getISOFormat(),
|
||||
@@ -82,8 +82,9 @@ class RecordService(
|
||||
if(domain.user.id != user.id)
|
||||
throw RuntimeException("Unauthorized to get record in API: $domain_id")
|
||||
|
||||
val record = domain.records.find { it.cfid == record_id }
|
||||
if(record == null) throw RuntimeException("Failed to find record in API: $record_id")
|
||||
val record = recordRepository.findByDomainIdAndCfid(domain.id!!, record_id).orElseThrow {
|
||||
RuntimeException("Failed to find record in API: $record_id")
|
||||
}
|
||||
|
||||
return RecordResponseDTO(
|
||||
id = record.cfid,
|
||||
@@ -91,15 +92,15 @@ class RecordService(
|
||||
name = record.name,
|
||||
content = record.content,
|
||||
ttl = record.ttl,
|
||||
zoneId = record.domain.cfid,
|
||||
zoneName = record.domain.name,
|
||||
zoneId = domain.cfid,
|
||||
zoneName = domain.name,
|
||||
createdOn = record.createdOn.getISOFormat(),
|
||||
modifiedOn = record.modifiedOn.getISOFormat(),
|
||||
comment = record.comment,
|
||||
)
|
||||
}
|
||||
|
||||
fun getRecordsByDomain(domain_id: String): List<RecordResponseDTO>? {
|
||||
fun getRecordsByDomain(domain_id: String): List<RecordResponseDTO> {
|
||||
val domain = domainRepository.findByCfid(domain_id).orElseThrow {
|
||||
RuntimeException("Failed to find domain in API: $domain_id")
|
||||
}
|
||||
@@ -108,13 +109,13 @@ class RecordService(
|
||||
if(domain.user.id != user.id)
|
||||
throw RuntimeException("Unauthorized to create record in API: $domain_id")
|
||||
|
||||
return domain?.records?.map { RecordResponseDTO(
|
||||
return recordRepository.findAllByDomainId(domain.id!!).map { RecordResponseDTO(
|
||||
id = it.cfid,
|
||||
type = it.type,
|
||||
name = it.name,
|
||||
content = it.content,
|
||||
zoneId = it.domain.cfid,
|
||||
zoneName = it.domain.name,
|
||||
zoneId = domain.cfid,
|
||||
zoneName = domain.name,
|
||||
priority = it.prio,
|
||||
ttl = it.ttl,
|
||||
createdOn = it.createdOn.getISOFormat(),
|
||||
@@ -153,7 +154,7 @@ class RecordService(
|
||||
else -> record.ttl
|
||||
}
|
||||
|
||||
powerDNSApiClient.updateRecord(domain!!.name, updatedRecord.name, updatedRecord.type, updatedRecord.value)
|
||||
powerDNSApiClient.updateRecord(domain.name, updatedRecord.name, updatedRecord.type, updatedRecord.value, record.ttl, record.prio)
|
||||
|
||||
// 저장
|
||||
val savedRecord = recordRepository.save(record)
|
||||
@@ -192,4 +193,4 @@ class RecordService(
|
||||
|
||||
return record_id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package space.mori.dnsapi
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class DnsapiApplicationTests {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package space.mori.dnsapi
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import org.junit.jupiter.api.Tag
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
@Tag("openapi-generation")
|
||||
@SpringBootTest(properties = ["springdoc.api-docs.enabled=true"])
|
||||
@ActiveProfiles("test")
|
||||
class OpenApiGenerationTest(@Autowired private val context: WebApplicationContext) {
|
||||
@Test
|
||||
fun generateOpenApi() {
|
||||
val response = MockMvcBuilders.webAppContextSetup(context).build()
|
||||
.perform(get("/api-docs").with { request ->
|
||||
request.serverName = "localhost"
|
||||
request.serverPort = 8080
|
||||
request
|
||||
})
|
||||
.andReturn().response
|
||||
check(response.status == 200) { "OpenAPI endpoint returned HTTP ${response.status}: ${response.contentAsString}" }
|
||||
|
||||
val output = Path.of(System.getProperty("openapi.output"))
|
||||
val json = GsonBuilder().setPrettyPrinting().create()
|
||||
.toJson(JsonParser.parseString(response.contentAsString))
|
||||
Files.writeString(output, json + "\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package space.mori.dnsapi
|
||||
|
||||
import mockwebserver3.MockResponse
|
||||
import mockwebserver3.MockWebServer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
|
||||
class PowerDNSAPIClientTest {
|
||||
@Test
|
||||
fun successfulCallsUseTheExpectedHttpMethods() {
|
||||
MockWebServer().use { server ->
|
||||
server.start()
|
||||
repeat(5) { server.enqueue(MockResponse.Builder().body("ok").build()) }
|
||||
val client = clientFor(server)
|
||||
|
||||
client.createZone("example.com")
|
||||
client.deleteZone("example.com")
|
||||
client.createRecord("example.com", "www.example.com.", "A", "192.0.2.1")
|
||||
client.updateRecord("example.com", "www.example.com.", "A", "192.0.2.2")
|
||||
client.deleteRecord("example.com", "www.example.com.", "A")
|
||||
|
||||
assertEquals("POST", server.takeRequest().method)
|
||||
assertEquals("DELETE", server.takeRequest().method)
|
||||
repeat(3) { assertEquals("PATCH", server.takeRequest().method) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorResponsesKeepThePowerDnsMessageAndHandleInvalidJson() {
|
||||
MockWebServer().use { server ->
|
||||
server.start()
|
||||
server.enqueue(MockResponse.Builder().code(400)
|
||||
.body("{\"error\":\"invalid zone\",\"errors\":[\"bad name\"]}").build())
|
||||
server.enqueue(MockResponse.Builder().code(500).body("not json").build())
|
||||
val client = clientFor(server)
|
||||
|
||||
val error = assertThrows(PowerDNSAPIException::class.java) { client.deleteZone("example.com") }
|
||||
assertEquals("invalid zone", error.message)
|
||||
assertEquals(listOf("bad name"), error.errors)
|
||||
|
||||
val fallback = assertThrows(PowerDNSAPIException::class.java) { client.deleteZone("example.com") }
|
||||
assertEquals("PowerDNS API returned HTTP 500", fallback.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clientFor(server: MockWebServer): PowerDNSAPIClient = PowerDNSAPIClient().also {
|
||||
ReflectionTestUtils.setField(it, "apiUrl", server.url("/").toString().removeSuffix("/"))
|
||||
ReflectionTestUtils.setField(it, "apiKey", "test-key")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package space.mori.dnsapi.service
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import space.mori.dnsapi.PowerDNSAPIClient
|
||||
import space.mori.dnsapi.db.Domain
|
||||
import space.mori.dnsapi.db.DomainRepository
|
||||
import space.mori.dnsapi.db.User
|
||||
import java.util.Optional
|
||||
|
||||
class DomainServiceTest {
|
||||
private val repository = Mockito.mock(DomainRepository::class.java)
|
||||
private val powerDns = Mockito.mock(PowerDNSAPIClient::class.java)
|
||||
private val service = DomainService(repository, powerDns)
|
||||
|
||||
@AfterEach
|
||||
fun clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsDeletingAnotherUsersDomainBeforeCallingPowerDns() {
|
||||
val owner = User(id = 1, apiKey = "owner", name = "owner")
|
||||
val otherUser = User(id = 2, apiKey = "other", name = "other")
|
||||
val domain = Domain(name = "example.com", user = owner)
|
||||
Mockito.`when`(repository.findByCfid(domain.cfid)).thenReturn(Optional.of(domain))
|
||||
authenticate(otherUser)
|
||||
|
||||
assertThrows(RuntimeException::class.java) { service.deleteDomain(domain.cfid) }
|
||||
Mockito.verifyNoInteractions(powerDns)
|
||||
Mockito.verify(repository, Mockito.never()).deleteByCfid(domain.cfid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletingAnOwnedDomainSucceedsWhenOneRowWasRemoved() {
|
||||
val owner = User(id = 1, apiKey = "owner", name = "owner")
|
||||
val domain = Domain(name = "example.com", user = owner)
|
||||
Mockito.`when`(repository.findByCfid(domain.cfid)).thenReturn(Optional.of(domain))
|
||||
Mockito.`when`(repository.deleteByCfid(domain.cfid)).thenReturn(1)
|
||||
authenticate(owner)
|
||||
|
||||
assertEquals(domain.cfid, service.deleteDomain(domain.cfid))
|
||||
Mockito.verify(powerDns).deleteZone(domain.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aUserWithoutDomainsGetsAnEmptyList() {
|
||||
val user = User(id = 1, apiKey = "owner", name = "owner")
|
||||
Mockito.`when`(repository.findAllByUser(user)).thenReturn(emptyList())
|
||||
authenticate(user)
|
||||
|
||||
assertEquals(emptyList<Domain>(), service.getAllDomains())
|
||||
}
|
||||
|
||||
private fun authenticate(user: User) {
|
||||
SecurityContextHolder.getContext().authentication =
|
||||
UsernamePasswordAuthenticationToken(user, null, emptyList())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
spring.datasource.url=jdbc:h2:mem:dnsapi;DB_CLOSE_DELAY=-1
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
pdns.api.key=test
|
||||
pdns.api.url=http://localhost
|
||||
pdns.ns=ns.example
|
||||
Reference in new issue
Block a user