2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 02:02:14 +00:00

added delete and update generated zone

This commit is contained in:
nspadaccino 2025-06-02 23:26:55 -04:00
parent 477ae80a3a
commit 06cd54e257
No known key found for this signature in database
GPG Key ID: AB060C9A2C68918E
8 changed files with 200 additions and 105 deletions

View File

@ -136,12 +136,6 @@ vinyldns {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "BIND Delete Zone Request", "title": "BIND Delete Zone Request",
"type": "object", "type": "object",
"required": ["zoneName"],
"properties": {
"zoneName": {
"type": "string"
}
},
"additionalProperties": false "additionalProperties": false
} }
""" """
@ -219,34 +213,16 @@ vinyldns {
"additionalProperties": false "additionalProperties": false
} }
""" """
delete-zone = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "PowerDNS Delete Zone",
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" }
},
"additionalProperties": false
}
"""
update-zone = """ update-zone = """
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "PowerDNS Update Zone", "title": "PowerDNS Update Zone",
"type": "object", "type": "object",
"required": ["kind", "nameservers"],
"properties": { "properties": {
"kind": { "kind": {
"type": "string", "type": "string",
"enum": ["Native", "Master"] "enum": ["Native", "Master"]
}, },
"nameservers": {
"type": "array",
"minItems": 1,
"items": { "type": "string", "pattern": "^[a-zA-Z0-9.-]+\\.$" }
},
"masters": { "masters": {
"type": "array", "type": "array",
"items": { "type": "string" } "items": { "type": "string" }
@ -265,11 +241,6 @@ vinyldns {
"nameservers": "{{nameservers}}" "nameservers": "{{nameservers}}"
} }
""" """
delete-zone = """
{
"name": "{{zoneName}}"
}
"""
update-zone = """ update-zone = """
{ {
"name": "{{zoneName}}", "name": "{{zoneName}}",

View File

@ -175,28 +175,7 @@ class ZoneService(
case None => result(()) 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,
request: ZoneGenerationInput, request: ZoneGenerationInput,
auth: AuthPrincipal auth: AuthPrincipal
): Result[ZoneGenerationResponse] = { ): Result[ZoneGenerationResponse] = {
@ -204,22 +183,22 @@ 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) _ <- schemaValidationResult(providerConfig, "create-zone", request.providerParams)
_ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult _ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult
// Build request and endpoint // Build request and endpoint
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request) endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("create-zone"), request)
requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get(operation), request) requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get("create-zone"), request)
// Authorization and existence checks // Authorization and existence checks
_ <- canChangeZone(auth, request.zoneName, request.groupId).toResult _ <- canChangeZone(auth, request.zoneName, request.groupId).toResult
_ <- existenceCheck(operation, request.zoneName) _ <- generateZoneDoesNotExist(request.zoneName).toResult
// Send request // Send request
_ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult _ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult
dnsProviderConn <- createConnection(endpoint).toResult dnsProviderConn <- createConnection(endpoint).toResult
dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, operation, requestJsonOpt, dnsProviderConn).toResult dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "create-zone", requestJsonOpt, dnsProviderConn).toResult
// Process response // Process response
responseCode = dnsConnResponse.getResponseCode responseCode = dnsConnResponse.getResponseCode
@ -240,7 +219,107 @@ class ZoneService(
) )
_ <- logger.info(s"response: $zoneGenerateResponse").toResult _ <- logger.info(s"response: $zoneGenerateResponse").toResult
_ <- executeRepoAction(operation, request, zoneGenerateResponse) zoneToGenerate = GenerateZone(request)
_ <- generateZoneRepository.save(zoneToGenerate.copy(response = Some(zoneGenerateResponse))).toResult[GenerateZone]
} yield zoneGenerateResponse
}
def handleUpdateGeneratedZoneRequest(
request: ZoneGenerationInput,
auth: AuthPrincipal
): Result[ZoneGenerationResponse] = {
for {
existingGeneratedZone <- getGenerateZoneByName(request.zoneName, auth)
_ <- canChangeZone(auth, existingGeneratedZone.zoneName, existingGeneratedZone.groupId).toResult
// Validate input
providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult
_ <- validateZoneName(request.zoneName).toResult
_ <- schemaValidationResult(providerConfig, "update-zone", request.providerParams)
_ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult
// Build request and endpoint
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("update-zone"), request)
requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get("update-zone"), request)
// Send request
_ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult
dnsProviderConn <- createConnection(endpoint).toResult
dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "update-zone", requestJsonOpt, dnsProviderConn).toResult
// Process response
responseCode = dnsConnResponse.getResponseCode
_ <- logger.info(s"response code: $responseCode").toResult
inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream
responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString
_ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult
// Only parse JSON if the response is non-empty
responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing
// Create response object
zoneGenerateResponse = ZoneGenerationResponse(
provider = request.provider,
responseCode = responseCode,
status = dnsConnResponse.getResponseMessage,
message = responseJson
)
_ <- logger.info(s"response: $zoneGenerateResponse").toResult
updatedZone = existingGeneratedZone.copy(
email = request.email,
groupId = request.groupId,
providerParams = request.providerParams,
response = Some(zoneGenerateResponse)
)
_ <- generateZoneRepository.save(updatedZone).toResult[GenerateZone]
} yield zoneGenerateResponse
}
def handleDeleteGeneratedZoneRequest(
generatedZoneId: String,
auth: AuthPrincipal
): Result[ZoneGenerationResponse] = {
for {
generatedZone <- getGeneratedZoneOrFail(generatedZoneId)
_ <- canChangeZone(auth, generatedZone.zoneName, generatedZone.groupId).toResult
providerConfig <- validateProvider(generatedZone.provider, dnsProviderApiConnection.providers).toResult
request = ZoneGenerationInput(
zoneName = generatedZone.zoneName,
provider = generatedZone.provider,
groupId = generatedZone.groupId,
email = generatedZone.email,
providerParams = generatedZone.providerParams
)
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("delete-zone"), request)
dnsProviderConn <- createConnection(endpoint).toResult
dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "delete-zone", None, dnsProviderConn).toResult
// Process response
responseCode = dnsConnResponse.getResponseCode
_ <- logger.info(s"response code: $responseCode").toResult
inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream
responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString
_ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult
// Only parse JSON if the response is non-empty
responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing
// Create response object
zoneGenerateResponse = ZoneGenerationResponse(
provider = generatedZone.provider,
responseCode = responseCode,
status = dnsConnResponse.getResponseMessage,
message = responseJson
)
_ <- logger.info(s"response: $zoneGenerateResponse").toResult
_ <- generateZoneRepository.delete(generatedZone).toResult[GenerateZone]
} yield zoneGenerateResponse } yield zoneGenerateResponse
} }
@ -771,19 +850,19 @@ class ZoneService(
} }
} }
private def generateZoneExists(zoneName: String): Either[Throwable, Unit] = { // private def generateZoneExists(zoneName: String): Either[Throwable, Unit] = {
val existingZoneOpt: Option[GenerateZone] = // val existingZoneOpt: Option[GenerateZone] =
generateZoneRepository.getGenerateZoneByName(zoneName).unsafeRunSync() // generateZoneRepository.getGenerateZoneByName(zoneName).unsafeRunSync()
//
existingZoneOpt match { // existingZoneOpt match {
case Some(_) => // case Some(_) =>
Right(()) // Right(())
case None => // case None =>
Left(ZoneNotFoundError( // Left(ZoneNotFoundError(
s"Zone with name $zoneName does not exist." // s"Zone with name $zoneName does not exist."
)) // ))
} // }
} // }
def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] = def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
ensuring( ensuring(
@ -826,6 +905,12 @@ class ZoneService(
.orFail(ZoneNotFoundError(s"Zone with name $zoneName does not exists")) .orFail(ZoneNotFoundError(s"Zone with name $zoneName does not exists"))
.toResult[GenerateZone] .toResult[GenerateZone]
def getGeneratedZoneOrFail(generatedZoneId: String): Result[GenerateZone] =
generateZoneRepository
.getGenerateZoneById(generatedZoneId)
.orFail(ZoneNotFoundError(s"Generated zone with id $generatedZoneId does not exists"))
.toResult[GenerateZone]
def validateZoneConnectionIfChanged(newZone: Zone, existingZone: Zone): Result[Unit] = def validateZoneConnectionIfChanged(newZone: Zone, existingZone: Zone): Result[Unit] =
if (newZone.connection != existingZone.connection if (newZone.connection != existingZone.connection
|| newZone.transferConnection != existingZone.transferConnection) { || newZone.transferConnection != existingZone.transferConnection) {

View File

@ -16,8 +16,6 @@
package vinyldns.api.domain.zone package vinyldns.api.domain.zone
import cats.data.EitherT
import cats.effect.IO
import vinyldns.api.Interfaces.Result import vinyldns.api.Interfaces.Result
import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.zone._ import vinyldns.core.domain.zone._
@ -29,18 +27,34 @@ trait ZoneServiceAlgebra {
auth: AuthPrincipal auth: AuthPrincipal
): Result[ZoneCommandResult] ): Result[ZoneCommandResult]
def handleGenerateZoneRequest(operation: String, request: ZoneGenerationInput, auth: AuthPrincipal): EitherT[IO, Throwable, ZoneGenerationResponse] def handleGenerateZoneRequest(
request: ZoneGenerationInput,
auth: AuthPrincipal
): Result[ZoneGenerationResponse]
def getGenerateZoneByName(zoneName: String, auth: AuthPrincipal): Result[GenerateZone] def handleUpdateGeneratedZoneRequest(
request: ZoneGenerationInput,
auth: AuthPrincipal
): Result[ZoneGenerationResponse]
def handleDeleteGeneratedZoneRequest(
generatedZoneId: String,
auth: AuthPrincipal
): Result[ZoneGenerationResponse]
def getGenerateZoneByName(
zoneName: String,
auth: AuthPrincipal
): Result[GenerateZone]
def listGeneratedZones( def listGeneratedZones(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,
nameFilter: Option[String], nameFilter: Option[String],
startFrom: Option[String], startFrom: Option[String],
maxItems: Int, maxItems: Int,
searchByAdminGroup: Boolean, searchByAdminGroup: Boolean,
ignoreAccess: Boolean ignoreAccess: Boolean
): Result[ListGeneratedZonesResponse] ): Result[ListGeneratedZonesResponse]
def allowedDNSProviders(): Result[List[String]] def allowedDNSProviders(): Result[List[String]]
@ -69,12 +83,12 @@ trait ZoneServiceAlgebra {
): Result[ListZonesResponse] ): Result[ListZonesResponse]
def listDeletedZones( def listDeletedZones(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,
nameFilter: Option[String], nameFilter: Option[String],
startFrom: Option[String], startFrom: Option[String],
maxItems: Int, maxItems: Int,
ignoreAccess: Boolean ignoreAccess: Boolean
): Result[ListDeletedZoneChangesResponse] ): Result[ListDeletedZoneChangesResponse]
def listZoneChanges( def listZoneChanges(
zoneId: String, zoneId: String,
@ -98,8 +112,8 @@ trait ZoneServiceAlgebra {
def getBackendIds(): Result[List[String]] def getBackendIds(): Result[List[String]]
def listFailedZoneChanges( def listFailedZoneChanges(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,
startFrom: Int, startFrom: Int,
maxItems: Int maxItems: Int
): Result[ListFailedZoneChangesResponse] ): Result[ListFailedZoneChangesResponse]
} }

View File

@ -146,28 +146,27 @@ class ZoneRoute(
(post & monitor("Endpoint.generateZone")) { (post & monitor("Endpoint.generateZone")) {
authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput]( authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput](
(authPrincipal, generateZone) => (authPrincipal, generateZone) =>
zoneService.handleGenerateZoneRequest("create-zone", generateZone, authPrincipal) zoneService.handleGenerateZoneRequest(generateZone, authPrincipal)
) { response => ) { response =>
complete(StatusCodes.Accepted -> response) complete(StatusCodes.Accepted -> response)
} }
} ~ } ~
(delete & monitor("Endpoint.deleteGeneratedZone")) {
authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput](
(authPrincipal, generateZone) =>
zoneService.handleGenerateZoneRequest("delete-zone", generateZone, authPrincipal)
) { response =>
complete(StatusCodes.Accepted, response)
}
} ~
(put & monitor("Endpoint.updateGeneratedZone")) { (put & monitor("Endpoint.updateGeneratedZone")) {
authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput]( authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput](
(authPrincipal, generateZone) => (authPrincipal, generateZone) =>
zoneService.handleGenerateZoneRequest("update-zone", generateZone, authPrincipal) zoneService.handleUpdateGeneratedZoneRequest(generateZone, authPrincipal)
) { response => ) { response =>
complete(StatusCodes.Accepted, response) complete(StatusCodes.Accepted, response)
} }
} }
} ~ } ~
path("zones" / "generate" / Segment) { id =>
(delete & monitor("Endpoint.deleteGeneratedZone")) {
authenticateAndExecute(zoneService.handleDeleteGeneratedZoneRequest(id, _)) { response =>
complete(StatusCodes.Accepted, response)
}
}
} ~
path("zones" / "generate" / "info") { path("zones" / "generate" / "info") {
(get & monitor("Endpoint.listGeneratedZones")) { (get & monitor("Endpoint.listGeneratedZones")) {
parameters( parameters(

View File

@ -26,7 +26,9 @@ 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 getGenerateZoneById(id: String): IO[Option[GenerateZone]]
def delete(generateZone: GenerateZone): IO[GenerateZone]
def listGenerateZones( def listGenerateZones(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,

View File

@ -224,7 +224,7 @@ trait ProtobufConversions {
zgr.getProvider, zgr.getProvider,
zgr.getResponseCode.toInt, zgr.getResponseCode.toInt,
zgr.getStatus, zgr.getStatus,
parse(zgr.getMessage) if (Option(zgr.getMessage).exists(_.trim.nonEmpty)) parse(zgr.getMessage) else JNothing
) )
def fromPB(rd: VinylDNSProto.RecordData, rt: RecordType): RecordData = def fromPB(rd: VinylDNSProto.RecordData, rt: RecordType): RecordData =

View File

@ -8,6 +8,7 @@ Create the Generate Zone table
CREATE TABLE generate_zone ( CREATE TABLE generate_zone (
id CHAR(36) NOT NULL, id CHAR(36) NOT NULL,
name VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL,
provider VARCHAR(256) NOT NULL,
admin_group_id CHAR(36) NOT NULL, admin_group_id CHAR(36) NOT NULL,
response BLOB NOT NULL, response BLOB NOT NULL,
data BLOB NOT NULL, data BLOB NOT NULL,

View File

@ -38,9 +38,10 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
*/ */
private final val PUT_GENERATE_ZONE = private final val PUT_GENERATE_ZONE =
sql""" sql"""
|INSERT INTO generate_zone(id, name, admin_group_id, response, data) |INSERT INTO generate_zone(id, name, provider, admin_group_id, response, data)
| VALUES ({id}, {name}, {adminGroupId}, {response}, {data}) ON DUPLICATE KEY | VALUES ({id}, {name}, {provider}, {adminGroupId}, {response}, {data}) ON DUPLICATE KEY
| UPDATE name=VALUES(name), | UPDATE name=VALUES(name),
| provider=VALUES(provider),
| admin_group_id=VALUES(admin_group_id), | admin_group_id=VALUES(admin_group_id),
| response=VALUES(response), | response=VALUES(response),
| data=VALUES(data); | data=VALUES(data);
@ -61,6 +62,13 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
| WHERE name = ? | WHERE name = ?
""".stripMargin """.stripMargin
private final val GET_GENERATED_ZONE_BY_ID =
sql"""
|SELECT data
| FROM generate_zone
| WHERE id = ?
""".stripMargin
private final val BASE_GENERATE_ZONE_SEARCH_SQL = private final val BASE_GENERATE_ZONE_SEARCH_SQL =
""" """
|SELECT gz.data |SELECT gz.data
@ -75,6 +83,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
.bindByName( .bindByName(
'id -> generateZone.id, 'id -> generateZone.id,
'name -> generateZone.zoneName, 'name -> generateZone.zoneName,
'provider -> generateZone.provider,
'adminGroupId -> generateZone.groupId, 'adminGroupId -> generateZone.groupId,
'response -> toPB(generateZone.response.get).toByteArray, 'response -> toPB(generateZone.response.get).toByteArray,
'data -> toPB(generateZone).toByteArray 'data -> toPB(generateZone).toByteArray
@ -96,11 +105,13 @@ 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[Unit] = def delete(generateZone: GenerateZone): IO[GenerateZone] =
monitor("repo.ZoneJDBC.generateZoneDelete") { monitor("repo.ZoneJDBC.generateZoneDelete") {
IO { IO {
DB.localTx { implicit s => DB.localTx { implicit s =>
deleteGeneratedZone(generateZone) deleteGeneratedZone(generateZone)
generateZone
} }
} }
} }
@ -108,6 +119,9 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
private def getGenerateZoneByNameInSession(zoneName: String)(implicit session: DBSession): Option[GenerateZone] = private def getGenerateZoneByNameInSession(zoneName: String)(implicit session: DBSession): Option[GenerateZone] =
GET_GENERATED_ZONE_BY_NAME.bind(zoneName).map(extractGenerateZone(1)).first().apply() GET_GENERATED_ZONE_BY_NAME.bind(zoneName).map(extractGenerateZone(1)).first().apply()
private def getGenerateZoneByIdInSession(zoneId: String)(implicit session: DBSession): Option[GenerateZone] =
GET_GENERATED_ZONE_BY_ID.bind(zoneId).map(extractGenerateZone(1)).first().apply()
def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]] = def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]] =
monitor("repo.ZoneJDBC.getGenerateZoneByName") { monitor("repo.ZoneJDBC.getGenerateZoneByName") {
IO { IO {
@ -117,6 +131,15 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
} }
} }
def getGenerateZoneById(id: String): IO[Option[GenerateZone]] =
monitor("repo.ZoneJDBC.getGenerateZoneById") {
IO {
DB.readOnly { implicit s =>
getGenerateZoneByIdInSession(id)
}
}
}
def listGenerateZones( def listGenerateZones(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,
zoneNameFilter: Option[String] = None, zoneNameFilter: Option[String] = None,