mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-22 10:10:12 +00:00
update zone generation handler, repository methods
This commit is contained in:
parent
244f6895f3
commit
477ae80a3a
@ -34,7 +34,6 @@ import com.cronutils.parser.CronParser
|
|||||||
import com.cronutils.model.CronType
|
import com.cronutils.model.CronType
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import vinyldns.api.domain.membership.MembershipService
|
import vinyldns.api.domain.membership.MembershipService
|
||||||
import vinyldns.core.Messages
|
|
||||||
import org.json4s._
|
import org.json4s._
|
||||||
import org.json4s.jackson.JsonMethods._
|
import org.json4s.jackson.JsonMethods._
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
@ -45,8 +44,6 @@ import scala.util.Try
|
|||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import java.io.{ByteArrayInputStream, InputStream, OutputStream}
|
|
||||||
import java.net.{HttpURLConnection, URL}
|
import java.net.{HttpURLConnection, URL}
|
||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
|
|
||||||
@ -169,6 +166,35 @@ class ZoneService(
|
|||||||
new URL(apiUrl).openConnection().asInstanceOf[HttpURLConnection]
|
new URL(apiUrl).openConnection().asInstanceOf[HttpURLConnection]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def schemaValidationResult(
|
||||||
|
providerConfig: DnsProviderConfig,
|
||||||
|
operation: String,
|
||||||
|
params: Map[String, JValue]
|
||||||
|
): Result[Unit] = providerConfig.schemas.get(operation) match {
|
||||||
|
case Some(schema) => JsonSchemaValidator.validate(schema, params).toResult
|
||||||
|
case None => result(())
|
||||||
|
}
|
||||||
|
|
||||||
|
private def existenceCheck(operation: String, zoneName: String): Result[Unit] = operation match {
|
||||||
|
case "create-zone" => generateZoneDoesNotExist(zoneName).toResult
|
||||||
|
case "delete-zone" => generateZoneExists(zoneName).toResult
|
||||||
|
case "update-zone" => generateZoneExists(zoneName).toResult
|
||||||
|
case _ => Left(InvalidRequest(s"Unsupported operation: $operation")).toResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private def executeRepoAction(
|
||||||
|
operation: String,
|
||||||
|
request: ZoneGenerationInput,
|
||||||
|
response: ZoneGenerationResponse
|
||||||
|
): Result[Unit] = {
|
||||||
|
val zoneToGenerate = GenerateZone(request)
|
||||||
|
operation match {
|
||||||
|
case "delete-zone" => generateZoneRepository.deleteTx(zoneToGenerate).toResult
|
||||||
|
case "create-zone" => generateZoneRepository.save(zoneToGenerate.copy(response = Some(response))).toResult
|
||||||
|
case "update-zone" => generateZoneRepository.save(zoneToGenerate.copy(response = Some(response))).toResult
|
||||||
|
case _ => Left(InvalidRequest(s"Unsupported operation: $operation")).toResult
|
||||||
|
}
|
||||||
|
}
|
||||||
def handleGenerateZoneRequest(
|
def handleGenerateZoneRequest(
|
||||||
operation: String,
|
operation: String,
|
||||||
request: ZoneGenerationInput,
|
request: ZoneGenerationInput,
|
||||||
@ -178,45 +204,32 @@ class ZoneService(
|
|||||||
// Validate input
|
// Validate input
|
||||||
providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult
|
providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult
|
||||||
_ <- validateZoneName(request.zoneName).toResult
|
_ <- validateZoneName(request.zoneName).toResult
|
||||||
|
_ <- schemaValidationResult(providerConfig, operation, request.providerParams)
|
||||||
// JSON Schema validation for providerParams
|
|
||||||
_ <- JsonSchemaValidator
|
|
||||||
.validate(
|
|
||||||
providerConfig.schemas(operation),
|
|
||||||
request.providerParams
|
|
||||||
).toResult
|
|
||||||
|
|
||||||
_ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult
|
_ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult
|
||||||
|
|
||||||
// Build JSON request
|
// Build request and endpoint
|
||||||
generateZoneRequestJson = buildGenerateZoneRequestJson(
|
|
||||||
providerConfig.requestTemplates(operation),
|
|
||||||
request
|
|
||||||
)
|
|
||||||
|
|
||||||
// Authorization checks
|
|
||||||
_ <- canChangeZone(auth, request.zoneName, request.groupId).toResult
|
|
||||||
_ <- generateZoneDoesNotExist(request.zoneName).toResult
|
|
||||||
|
|
||||||
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request)
|
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request)
|
||||||
|
requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get(operation), request)
|
||||||
|
|
||||||
_ = logger.info(s"Request: provider=${request.provider}, path=${endpoint}, request=$generateZoneRequestJson").toResult
|
// Authorization and existence checks
|
||||||
|
_ <- canChangeZone(auth, request.zoneName, request.groupId).toResult
|
||||||
|
_ <- existenceCheck(operation, request.zoneName)
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
_ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult
|
||||||
dnsProviderConn <- createConnection(endpoint).toResult
|
dnsProviderConn <- createConnection(endpoint).toResult
|
||||||
dnsConnResponse <- createDnsZoneService(
|
dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, operation, requestJsonOpt, dnsProviderConn).toResult
|
||||||
endpoint,
|
|
||||||
providerConfig.apiKey,
|
|
||||||
generateZoneRequestJson,
|
|
||||||
dnsProviderConn
|
|
||||||
).toResult
|
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
responseCode = dnsConnResponse.getResponseCode
|
responseCode = dnsConnResponse.getResponseCode
|
||||||
|
_ <- logger.info(s"response code: $responseCode").toResult
|
||||||
inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream
|
inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream
|
||||||
responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString
|
responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString
|
||||||
_ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult
|
_ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult
|
||||||
|
|
||||||
// Parse response
|
// Only parse JSON if the response is non-empty
|
||||||
responseJson = parse(responseMessage)
|
responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing
|
||||||
|
|
||||||
// Create response object
|
// Create response object
|
||||||
zoneGenerateResponse = ZoneGenerationResponse(
|
zoneGenerateResponse = ZoneGenerationResponse(
|
||||||
@ -225,32 +238,28 @@ class ZoneService(
|
|||||||
status = dnsConnResponse.getResponseMessage,
|
status = dnsConnResponse.getResponseMessage,
|
||||||
message = responseJson
|
message = responseJson
|
||||||
)
|
)
|
||||||
|
_ <- logger.info(s"response: $zoneGenerateResponse").toResult
|
||||||
|
|
||||||
// Save to repository
|
_ <- executeRepoAction(operation, request, zoneGenerateResponse)
|
||||||
zoneToGenerate = GenerateZone(request)
|
|
||||||
_ <- generateZoneRepository.save(zoneToGenerate.copy(response = Some(zoneGenerateResponse))).toResult[GenerateZone]
|
|
||||||
|
|
||||||
} yield {
|
} yield zoneGenerateResponse
|
||||||
// Cleanup resources
|
|
||||||
Option(inputStream).foreach(_.close())
|
|
||||||
Option(dnsConnResponse).foreach(_.disconnect())
|
|
||||||
zoneGenerateResponse
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a Generate Zone JSON request using template engine
|
// Build a Generate Zone JSON request using template engine
|
||||||
private def buildGenerateZoneRequestJson(
|
private def buildGenerateZoneRequestJson(
|
||||||
requestTemplate: String,
|
maybeRequestTemplate: Option[String],
|
||||||
zoneGenerationInput: ZoneGenerationInput
|
zoneGenerationInput: ZoneGenerationInput
|
||||||
): String = {
|
): Option[String] = {
|
||||||
val baseParams = Map(
|
val baseParams = Map(
|
||||||
"zoneName" -> JString(zoneGenerationInput.zoneName),
|
"zoneName" -> JString(zoneGenerationInput.zoneName),
|
||||||
"provider" -> JString(zoneGenerationInput.provider),
|
"provider" -> JString(zoneGenerationInput.provider),
|
||||||
"groupId" -> JString(zoneGenerationInput.groupId),
|
"groupId" -> JString(zoneGenerationInput.groupId),
|
||||||
"email" -> JString(zoneGenerationInput.email)
|
"email" -> JString(zoneGenerationInput.email)
|
||||||
)
|
)
|
||||||
|
|
||||||
TemplateEngine.renderTemplate(requestTemplate, baseParams ++ zoneGenerationInput.providerParams)
|
maybeRequestTemplate.map { requestTemplate =>
|
||||||
|
TemplateEngine.renderTemplate(requestTemplate, baseParams ++ zoneGenerationInput.providerParams)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def buildGenerateZoneEndpoint(
|
private def buildGenerateZoneEndpoint(
|
||||||
@ -354,36 +363,41 @@ class ZoneService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def createDnsZoneService(dnsApiUrl: String, dnsApiKey: String, request: String, connection: HttpURLConnection): Either[Throwable, HttpURLConnection] =
|
def createDnsZoneService(
|
||||||
{
|
dnsApiKey: String,
|
||||||
|
operation: String,
|
||||||
|
request: Option[String],
|
||||||
|
connection: HttpURLConnection
|
||||||
|
): Either[Throwable, HttpURLConnection] = {
|
||||||
try {
|
try {
|
||||||
//val connection = new URL(dnsApiUrl).openConnection().asInstanceOf[HttpURLConnection]
|
// Map operation to HTTP method
|
||||||
connection.setRequestMethod("POST")
|
val method = operation match {
|
||||||
|
case "create-zone" => "POST"
|
||||||
|
case "update-zone" => "PUT"
|
||||||
|
case "delete-zone" => "DELETE"
|
||||||
|
case other => throw new IllegalArgumentException(s"Unsupported operation: $other")
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestMethod(method)
|
||||||
connection.setRequestProperty("Content-Type", "application/json")
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
connection.setRequestProperty("X-API-Key", dnsApiKey)
|
connection.setRequestProperty("X-API-Key", dnsApiKey)
|
||||||
connection.setDoOutput(true)
|
|
||||||
|
|
||||||
val outputStream: OutputStream = connection.getOutputStream
|
|
||||||
outputStream.write(request.getBytes("UTF-8"))
|
|
||||||
outputStream.close()
|
|
||||||
|
|
||||||
|
// Only send a body if the HTTP method and request are appropriate
|
||||||
|
val methodsWithBody = Set("POST", "PUT", "PATCH")
|
||||||
|
if (methodsWithBody.contains(method) && request.isDefined) {
|
||||||
|
connection.setDoOutput(true)
|
||||||
|
val outputStream = connection.getOutputStream
|
||||||
|
try {
|
||||||
|
outputStream.write(request.get.getBytes("UTF-8"))
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Right(connection)
|
Right(connection)
|
||||||
} catch {
|
} catch {
|
||||||
case e: Exception =>
|
case e: Exception =>
|
||||||
val errorConnection = new HttpURLConnection(new URL(dnsApiUrl)) {
|
Left(e)
|
||||||
private val errorJson = Messages.dnsProviderConnRefusedMessage(e, dnsApiUrl)
|
|
||||||
private val errorBytes = errorJson.getBytes("UTF-8")
|
|
||||||
private val errorByteStream = new ByteArrayInputStream(errorBytes)
|
|
||||||
|
|
||||||
override def disconnect(): Unit = {}
|
|
||||||
override def usingProxy(): Boolean = false
|
|
||||||
override def connect(): Unit = {}
|
|
||||||
|
|
||||||
override def getResponseCode: Int = 500
|
|
||||||
override def getErrorStream: InputStream = errorByteStream
|
|
||||||
}
|
|
||||||
Right(errorConnection)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -757,6 +771,20 @@ class ZoneService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def generateZoneExists(zoneName: String): Either[Throwable, Unit] = {
|
||||||
|
val existingZoneOpt: Option[GenerateZone] =
|
||||||
|
generateZoneRepository.getGenerateZoneByName(zoneName).unsafeRunSync()
|
||||||
|
|
||||||
|
existingZoneOpt match {
|
||||||
|
case Some(_) =>
|
||||||
|
Right(())
|
||||||
|
case None =>
|
||||||
|
Left(ZoneNotFoundError(
|
||||||
|
s"Zone with name $zoneName does not exist."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
|
def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
|
||||||
ensuring(
|
ensuring(
|
||||||
NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.")
|
NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.")
|
||||||
|
@ -24,7 +24,6 @@ import vinyldns.api.Interfaces.ensuring
|
|||||||
import vinyldns.core.domain.membership.User
|
import vinyldns.core.domain.membership.User
|
||||||
import vinyldns.core.domain.record.RecordType
|
import vinyldns.core.domain.record.RecordType
|
||||||
import vinyldns.core.domain.zone.{ACLRule, Zone, ZoneACL, DnsProviderConfig}
|
import vinyldns.core.domain.zone.{ACLRule, Zone, ZoneACL, DnsProviderConfig}
|
||||||
import org.json4s._
|
|
||||||
|
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
@ -105,17 +104,6 @@ class ZoneValidations(syncDelayMillis: Int) {
|
|||||||
case None => Left(InvalidRequest(s"Unsupported DNS provider: $provider"))
|
case None => Left(InvalidRequest(s"Unsupported DNS provider: $provider"))
|
||||||
}
|
}
|
||||||
|
|
||||||
def validateRequiredFields(
|
|
||||||
requiredFields: List[String],
|
|
||||||
providedFields: Map[String, JValue],
|
|
||||||
providerName: String
|
|
||||||
): Either[Throwable, Unit] = {
|
|
||||||
val missing = requiredFields.filterNot(providedFields.contains)
|
|
||||||
ensuring(InvalidRequest(
|
|
||||||
s"Missing required fields for $providerName: ${missing.mkString(", ")}"
|
|
||||||
))(missing.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
def validateZoneName(zoneName: String): Either[Throwable, Unit] =
|
def validateZoneName(zoneName: String): Either[Throwable, Unit] =
|
||||||
ensuring(InvalidRequest(s"Invalid zone name: $zoneName")) {
|
ensuring(InvalidRequest(s"Invalid zone name: $zoneName")) {
|
||||||
zoneName.matches("""^[a-zA-Z0-9.-]+\.$""")
|
zoneName.matches("""^[a-zA-Z0-9.-]+\.$""")
|
||||||
|
@ -26,6 +26,8 @@ trait GenerateZoneRepository extends Repository {
|
|||||||
|
|
||||||
def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]]
|
def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]]
|
||||||
|
|
||||||
|
def deleteTx(generateZone: GenerateZone): IO[Unit]
|
||||||
|
|
||||||
def listGenerateZones(
|
def listGenerateZones(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
zoneNameFilter: Option[String] = None,
|
zoneNameFilter: Option[String] = None,
|
||||||
|
@ -82,8 +82,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
|
|||||||
.update()
|
.update()
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
generateZone
|
generateZone
|
||||||
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@ -97,7 +96,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
|
|||||||
fromPB(VinylDNSProto.GenerateZone.parseFrom(res.bytes(columnIndex)))
|
fromPB(VinylDNSProto.GenerateZone.parseFrom(res.bytes(columnIndex)))
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteTx(generateZone: GenerateZone): IO[GenerateZone] =
|
def deleteTx(generateZone: GenerateZone): IO[Unit] =
|
||||||
monitor("repo.ZoneJDBC.generateZoneDelete") {
|
monitor("repo.ZoneJDBC.generateZoneDelete") {
|
||||||
IO {
|
IO {
|
||||||
DB.localTx { implicit s =>
|
DB.localTx { implicit s =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user