mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-22 02:02:14 +00:00
Merge branch 'master' into records_ownership_transfer
This commit is contained in:
commit
d6fcd3d726
149
.github/workflows/release-beta.yml
vendored
Normal file
149
.github/workflows/release-beta.yml
vendored
Normal file
@ -0,0 +1,149 @@
|
||||
name: VinylDNS Beta Release
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: "release"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
verify-first:
|
||||
description: 'Verify First?'
|
||||
required: true
|
||||
default: 'true'
|
||||
create-gh-release:
|
||||
description: 'Create a GitHub Release?'
|
||||
required: true
|
||||
default: 'true'
|
||||
publish-images:
|
||||
description: 'Publish Docker Images?'
|
||||
required: true
|
||||
default: 'true'
|
||||
pre-release:
|
||||
description: 'Is this a pre-release?'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Tests
|
||||
id: verify
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
run: cd build/ && ./assemble_api.sh && ./run_all_tests.sh
|
||||
|
||||
create-gh-release:
|
||||
name: Create GitHub Release
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.create-gh-release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Artifacts
|
||||
id: build
|
||||
run: cd build/ && ./assemble_api.sh && ./assemble_portal.sh
|
||||
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(awk -F'"' '{print $2}' ./version.sbt)"
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
|
||||
generate_release_notes: true
|
||||
files: artifacts/*
|
||||
prerelease: ${{ github.event.inputs['pre-release'] == 'true' }}
|
||||
|
||||
docker-release-api:
|
||||
name: Release API Docker Image
|
||||
needs: [ verify, create-gh-release ]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.publish-images == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Import Content Trust Key
|
||||
run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
|
||||
env:
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
# This will publish the latest release
|
||||
- name: Publish API Docker Image
|
||||
run: make -C build/docker/api publish
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
docker-release-portal:
|
||||
name: Release Portal Docker Image
|
||||
needs: [ verify, create-gh-release ]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.publish-images == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Import Content Trust Key
|
||||
run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
|
||||
env:
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
# This will publish the latest release
|
||||
- name: Publish Portal Docker Image
|
||||
run: make -C build/docker/portal publish
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
|
||||
generate_release_notes: true
|
||||
@ -85,13 +85,13 @@ jobs:
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@ -120,13 +120,13 @@ jobs:
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
8
.github/workflows/verify.yml
vendored
8
.github/workflows/verify.yml
vendored
@ -31,28 +31,28 @@ jobs:
|
||||
|
||||
- name: Codecov
|
||||
id: codecov0
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry
|
||||
id: codecov1
|
||||
if: steps.codecov0.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry 2
|
||||
id: codecov2
|
||||
if: steps.codecov1.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry 3
|
||||
id: codecov3
|
||||
if: steps.codecov2.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
@ -21,6 +21,7 @@ in any way, but do not see your name here, please open a PR to add yourself (in
|
||||
- Joe Crowe
|
||||
- Jearvon Dharrie
|
||||
- Andrew Dunn
|
||||
- Josh Edwards
|
||||
- Ryan Emerle
|
||||
- David Grizzanti
|
||||
- Alejandro Guirao
|
||||
@ -41,8 +42,9 @@ in any way, but do not see your name here, please open a PR to add yourself (in
|
||||
- Khalid Reid
|
||||
- Timo Schmid
|
||||
- Trent Schmidt
|
||||
- Nick Spadaccino
|
||||
- Arpit Shah
|
||||
- Ghafar Shah
|
||||
- Nick Spadaccino
|
||||
- Rebecca Star
|
||||
- Jess Stodola
|
||||
- Juan Valencia
|
||||
|
@ -146,7 +146,8 @@ See the [Contributing Guide](CONTRIBUTING.md).
|
||||
The current maintainers (people who can merge pull requests) are:
|
||||
|
||||
- Ryan Emerle ([@remerle](https://github.com/remerle))
|
||||
- Sriram Ramakrishnan ([@sramakr](https://github.com/sramakr))
|
||||
- Arpit Shah ([@arpit4ever](https://github.com/arpit4ever))
|
||||
- Nick Spadaccino ([@nspadaccino](https://github.com/nspadaccino))
|
||||
- Jim Wakemen ([@jwakemen](https://github.com/jwakemen))
|
||||
|
||||
See [AUTHORS.md](AUTHORS.md) for the full list of contributors to VinylDNS.
|
||||
|
@ -351,6 +351,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
<logger name="scalikejdbc.StatementExecutor$$anon$1" level="OFF"/>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="TRACE">
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<logger name="play" level="INFO" />
|
||||
<logger name="application" level="DEBUG" />
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="TRACE">
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
@ -348,6 +348,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -367,6 +367,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="TRACE">
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
@ -30,7 +30,6 @@ import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.core.domain.record.RecordType.{RecordType, UNKNOWN}
|
||||
import vinyldns.core.queue.MessageQueue
|
||||
import java.net.InetAddress
|
||||
|
||||
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
|
||||
extends BatchChangeConverterAlgebra {
|
||||
@ -52,17 +51,16 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
s"Converting BatchChange [${batchChange.id}] with SingleChanges [${batchChange.changes.map(_.id)}]"
|
||||
)
|
||||
for {
|
||||
updatedBatchChange <- updateBatchChange(batchChange, groupedChanges).toRightBatchResult
|
||||
recordSetChanges <- createRecordSetChangesForBatch(
|
||||
updatedBatchChange.changes,
|
||||
batchChange.changes,
|
||||
existingZones,
|
||||
groupedChanges,
|
||||
batchChange.userId,
|
||||
ownerGroupId
|
||||
).toRightBatchResult
|
||||
_ <- allChangesWereConverted(updatedBatchChange.changes, recordSetChanges)
|
||||
_ <- allChangesWereConverted(batchChange.changes, recordSetChanges)
|
||||
_ <- batchChangeRepo
|
||||
.save(updatedBatchChange)
|
||||
.save(batchChange)
|
||||
.toBatchResult // need to save the change before queueing, backend processing expects the changes to exist
|
||||
queued <- putChangesOnQueue(recordSetChanges, batchChange.id)
|
||||
changeToStore = updateWithQueueingFailures(batchChange, queued)
|
||||
@ -129,7 +127,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
change match {
|
||||
case _: SingleDeleteRRSetChange if change.recordSetId.isEmpty =>
|
||||
// Mark as Complete since we don't want to throw it as an error
|
||||
change.withDoesNotExistMessage(nonExistentRecordDeleteMessage)
|
||||
change.withDoesNotExistMessage
|
||||
case _ =>
|
||||
// Failure here means there was a message queue issue for this change
|
||||
change.withFailureMessage(failedMessage)
|
||||
@ -142,35 +140,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
|
||||
// Update if Single change is Failed or if a record that does not exist is deleted
|
||||
val failedAndNotExistsChanges = batchChange.changes.collect {
|
||||
case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(nonExistentRecordDeleteMessage) => change
|
||||
case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(nonExistentRecordDeleteMessage) || change.systemMessage.contains(nonExistentRecordDataDeleteMessage) => change
|
||||
}
|
||||
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
|
||||
val storeChanges = batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
|
||||
storeChanges
|
||||
}.toBatchResult
|
||||
|
||||
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean =
|
||||
existingRecordSetData.exists { rd =>
|
||||
(rd, recordData) match {
|
||||
case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
|
||||
InetAddress.getByName(proposedAddress).getHostName == InetAddress
|
||||
.getByName(rdAddress)
|
||||
.getHostName
|
||||
case _ => rd == recordData
|
||||
}
|
||||
}
|
||||
|
||||
def updateBatchChange(batchChange: BatchChange, groupedChanges: ChangeForValidationMap): BatchChange = {
|
||||
// Update system message to be display the information if record data doesn't exist for the delete request
|
||||
val singleChanges = batchChange.changes.map {
|
||||
case change@(sd: SingleDeleteRRSetChange) =>
|
||||
if (sd.recordData.isDefined && !groupedChanges.getExistingRecordSet(change.recordKey.get).exists(rs => matchRecordData(rs.records, sd.recordData.get))) {
|
||||
sd.copy(systemMessage = Some(nonExistentRecordDataDeleteMessage))
|
||||
}
|
||||
else change
|
||||
case change => change
|
||||
}
|
||||
batchChange.copy(changes = singleChanges)
|
||||
}
|
||||
|
||||
def createRecordSetChangesForBatch(
|
||||
changes: List[SingleChange],
|
||||
existingZones: ExistingZones,
|
||||
|
@ -44,12 +44,14 @@ object BatchChangeInput {
|
||||
sealed trait ChangeInput {
|
||||
val inputName: String
|
||||
val typ: RecordType
|
||||
val systemMessage: Option[String]
|
||||
def asNewStoredChange(errors: NonEmptyList[DomainValidationError], defaultTtl: Long): SingleChange
|
||||
}
|
||||
|
||||
final case class AddChangeInput(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
ttl: Option[Long],
|
||||
record: RecordData
|
||||
) extends ChangeInput {
|
||||
@ -68,7 +70,7 @@ final case class AddChangeInput(
|
||||
knownTtl,
|
||||
record,
|
||||
SingleChangeStatus.NeedsReview,
|
||||
None,
|
||||
systemMessage,
|
||||
None,
|
||||
None,
|
||||
errors.toList.map(SingleChangeError(_))
|
||||
@ -79,6 +81,7 @@ final case class AddChangeInput(
|
||||
final case class DeleteRRSetChangeInput(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
record: Option[RecordData]
|
||||
) extends ChangeInput {
|
||||
def asNewStoredChange(
|
||||
@ -93,7 +96,7 @@ final case class DeleteRRSetChangeInput(
|
||||
typ,
|
||||
record,
|
||||
SingleChangeStatus.NeedsReview,
|
||||
None,
|
||||
systemMessage,
|
||||
None,
|
||||
None,
|
||||
errors.toList.map(SingleChangeError(_))
|
||||
@ -104,6 +107,7 @@ object AddChangeInput {
|
||||
def apply(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
ttl: Option[Long],
|
||||
record: RecordData
|
||||
): AddChangeInput = {
|
||||
@ -111,28 +115,29 @@ object AddChangeInput {
|
||||
case PTR => inputName
|
||||
case _ => ensureTrailingDot(inputName)
|
||||
}
|
||||
new AddChangeInput(transformName, typ, ttl, record)
|
||||
new AddChangeInput(transformName, typ, systemMessage, ttl, record)
|
||||
}
|
||||
|
||||
def apply(sc: SingleAddChange): AddChangeInput =
|
||||
AddChangeInput(sc.inputName, sc.typ, Some(sc.ttl), sc.recordData)
|
||||
AddChangeInput(sc.inputName, sc.typ, sc.systemMessage, Some(sc.ttl), sc.recordData)
|
||||
}
|
||||
|
||||
object DeleteRRSetChangeInput {
|
||||
def apply(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
record: Option[RecordData] = None
|
||||
): DeleteRRSetChangeInput = {
|
||||
val transformName = typ match {
|
||||
case PTR => inputName
|
||||
case _ => ensureTrailingDot(inputName)
|
||||
}
|
||||
new DeleteRRSetChangeInput(transformName, typ, record)
|
||||
new DeleteRRSetChangeInput(transformName, typ, systemMessage, record)
|
||||
}
|
||||
|
||||
def apply(sc: SingleDeleteRRSetChange): DeleteRRSetChangeInput =
|
||||
DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.recordData)
|
||||
DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.systemMessage, sc.recordData)
|
||||
}
|
||||
|
||||
object ChangeInputType extends Enumeration {
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package vinyldns.api.domain.batch
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import cats.data._
|
||||
@ -315,16 +314,34 @@ class BatchChangeValidations(
|
||||
else
|
||||
().validNel
|
||||
|
||||
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean =
|
||||
existingRecordSetData.exists { rd =>
|
||||
(rd, recordData) match {
|
||||
case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
|
||||
InetAddress.getByName(proposedAddress).getHostName == InetAddress
|
||||
.getByName(rdAddress)
|
||||
.getHostName
|
||||
case _ => rd == recordData
|
||||
}
|
||||
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = {
|
||||
existingRecordSetData.par.exists { rd =>
|
||||
rd == recordData
|
||||
}
|
||||
}
|
||||
|
||||
def ensureRecordExists(
|
||||
change: ChangeForValidation,
|
||||
groupedChanges: ChangeForValidationMap
|
||||
): Boolean = {
|
||||
change match {
|
||||
// For DeleteRecord inputs, need to verify that the record data actually exists
|
||||
case DeleteRRSetChangeForValidation(_, _, DeleteRRSetChangeInput(_, _, _, Some(recordData)))
|
||||
if !groupedChanges
|
||||
.getExistingRecordSet(change.recordKey)
|
||||
.exists(rs => matchRecordData(rs.records, recordData)) =>
|
||||
false
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
def updateSystemMessage(changeInput: ChangeInput, systemMessage: String): ChangeInput = {
|
||||
changeInput match {
|
||||
case dci: DeleteRRSetChangeInput => dci.copy(systemMessage = Some(systemMessage))
|
||||
case _ => changeInput
|
||||
}
|
||||
}
|
||||
|
||||
def validateDeleteWithContext(
|
||||
change: ChangeForValidation,
|
||||
@ -333,26 +350,37 @@ class BatchChangeValidations(
|
||||
isApproved: Boolean
|
||||
): SingleValidation[ChangeForValidation] = {
|
||||
|
||||
// To handle add and delete for the record with same record data is present in the batch
|
||||
val nonExistentRecordDeleteMessage = "This record does not exist. No further action is required."
|
||||
val nonExistentRecordDataDeleteMessage = "Record data entered does not exist. No further action is required."
|
||||
|
||||
val recordData = change match {
|
||||
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => if(inputChange.record.isDefined) inputChange.record.get.toString else ""
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
|
||||
}
|
||||
|
||||
val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
|
||||
val isSameRecordUpdateInBatch = if(recordData.nonEmpty){
|
||||
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
|
||||
} else false
|
||||
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
|
||||
|
||||
val validations =
|
||||
groupedChanges.getExistingRecordSet(change.recordKey) match {
|
||||
case Some(rs) =>
|
||||
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved)
|
||||
case None =>
|
||||
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
validations.map(_ => change)
|
||||
// Perform the system message update based on the condition
|
||||
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else if (!ensureRecordExists(change, groupedChanges)) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else {
|
||||
change
|
||||
}
|
||||
|
||||
val validations = groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
|
||||
case Some(rs) =>
|
||||
userCanDeleteRecordSet(updatedChange, auth, rs.ownerGroupId, rs.records) |+|
|
||||
zoneDoesNotRequireManualReview(updatedChange, isApproved)
|
||||
case None =>
|
||||
if (isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
|
||||
validations.map(_ => updatedChange)
|
||||
}
|
||||
|
||||
def validateAddUpdateWithContext(
|
||||
@ -397,28 +425,40 @@ class BatchChangeValidations(
|
||||
isApproved: Boolean
|
||||
): SingleValidation[ChangeForValidation] = {
|
||||
|
||||
val nonExistentRecordDeleteMessage = "This record does not exist. No further action is required."
|
||||
val nonExistentRecordDataDeleteMessage = "Record data entered does not exist. No further action is required."
|
||||
|
||||
// To handle add and delete for the record with same record data is present in the batch
|
||||
val recordData = change match {
|
||||
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => if(inputChange.record.isDefined) inputChange.record.get.toString else ""
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
|
||||
}
|
||||
|
||||
val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
|
||||
val isSameRecordUpdateInBatch = if(recordData.nonEmpty){
|
||||
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
|
||||
} else false
|
||||
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
|
||||
|
||||
// Perform the system message update based on the condition
|
||||
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else if (!ensureRecordExists(change, groupedChanges)) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else {
|
||||
change
|
||||
}
|
||||
|
||||
val validations =
|
||||
groupedChanges.getExistingRecordSet(change.recordKey) match {
|
||||
groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
|
||||
case Some(rs) =>
|
||||
val adds = groupedChanges.getProposedAdds(change.recordKey).toList
|
||||
userCanUpdateRecordSet(change, auth, rs.ownerGroupId, adds) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved)
|
||||
val adds = groupedChanges.getProposedAdds(updatedChange.recordKey).toList
|
||||
userCanUpdateRecordSet(updatedChange, auth, rs.ownerGroupId, adds) |+|
|
||||
zoneDoesNotRequireManualReview(updatedChange, isApproved)
|
||||
case None =>
|
||||
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel
|
||||
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
|
||||
validations.map(_ => change)
|
||||
validations.map(_ => updatedChange)
|
||||
}
|
||||
|
||||
def validateAddWithContext(
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package vinyldns.api.domain.batch
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.util.UUID
|
||||
|
||||
import vinyldns.api.domain.ReverseZoneHelpers
|
||||
@ -24,7 +23,7 @@ import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
|
||||
import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType.LogicalChangeType
|
||||
import vinyldns.api.backend.dns.DnsConversions.getIPv6FullReverseName
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.core.domain.record.{AAAAData, RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
@ -82,6 +81,7 @@ object BatchTransformations {
|
||||
val recordKey = RecordKey(zone.id, recordName, inputChange.typ)
|
||||
def asStoredChange(changeId: Option[String] = None): SingleChange
|
||||
def isAddChangeForValidation: Boolean
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation
|
||||
}
|
||||
|
||||
object ChangeForValidation {
|
||||
@ -118,7 +118,7 @@ object BatchTransformations {
|
||||
ttl,
|
||||
inputChange.record,
|
||||
SingleChangeStatus.Pending,
|
||||
None,
|
||||
inputChange.systemMessage,
|
||||
None,
|
||||
None,
|
||||
List.empty,
|
||||
@ -127,6 +127,10 @@ object BatchTransformations {
|
||||
}
|
||||
|
||||
def isAddChangeForValidation: Boolean = true
|
||||
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
|
||||
this.copy(inputChange = inputChange.asInstanceOf[AddChangeInput])
|
||||
}
|
||||
}
|
||||
|
||||
final case class DeleteRRSetChangeForValidation(
|
||||
@ -143,7 +147,7 @@ object BatchTransformations {
|
||||
inputChange.typ,
|
||||
inputChange.record,
|
||||
SingleChangeStatus.Pending,
|
||||
None,
|
||||
inputChange.systemMessage,
|
||||
None,
|
||||
None,
|
||||
List.empty,
|
||||
@ -151,6 +155,10 @@ object BatchTransformations {
|
||||
)
|
||||
|
||||
def isAddChangeForValidation: Boolean = false
|
||||
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
|
||||
this.copy(inputChange = inputChange.asInstanceOf[DeleteRRSetChangeInput])
|
||||
}
|
||||
}
|
||||
|
||||
final case class BatchConversionOutput(
|
||||
@ -197,13 +205,6 @@ object BatchTransformations {
|
||||
}
|
||||
|
||||
object ValidationChanges {
|
||||
def matchRecordData(existingRecord: RecordData, recordData: String): Boolean =
|
||||
existingRecord match {
|
||||
case AAAAData(address) =>
|
||||
InetAddress.getByName(address).getHostName ==
|
||||
InetAddress.getByName(recordData).getHostName
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def apply(
|
||||
changes: List[ChangeForValidation],
|
||||
@ -223,16 +224,11 @@ object BatchTransformations {
|
||||
case DeleteRRSetChangeForValidation(
|
||||
_,
|
||||
_,
|
||||
DeleteRRSetChangeInput(_, AAAA, Some(AAAAData(address)))
|
||||
) =>
|
||||
existingRecords.filter(r => matchRecordData(r, address))
|
||||
case DeleteRRSetChangeForValidation(
|
||||
_,
|
||||
_,
|
||||
DeleteRRSetChangeInput(_, _, Some(recordData))
|
||||
DeleteRRSetChangeInput(_, _, _, Some(recordData))
|
||||
) =>
|
||||
Set(recordData)
|
||||
case _: DeleteRRSetChangeForValidation => existingRecords
|
||||
case _: DeleteRRSetChangeForValidation =>
|
||||
existingRecords
|
||||
}
|
||||
.toSet
|
||||
.flatten
|
||||
|
@ -670,21 +670,19 @@ class RecordSetService(
|
||||
} yield change
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: Option[String] = None,
|
||||
zoneId: String,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
fqdn: Option[String] = None,
|
||||
recordType: Option[RecordType] = None,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId.get)
|
||||
zone <- getZone(zoneId)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
recordSetChangesResults <- recordChangeRepository
|
||||
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, fqdn, recordType)
|
||||
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, None, None)
|
||||
.toResult[ListRecordSetChangesResults]
|
||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||
} yield ListRecordSetChangesResponse(zoneId.get, recordSetChangesResults, recordSetChangesInfo)
|
||||
} yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String] = None,
|
||||
|
@ -101,11 +101,9 @@ trait RecordSetServiceAlgebra {
|
||||
): Result[RecordSetChange]
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: Option[String],
|
||||
zoneId: String,
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse]
|
||||
|
||||
|
@ -38,6 +38,7 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
private val outOfSyncFailureMessage: String = "This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."
|
||||
private val incompatibleRecordFailureMessage: String = "Incompatible record in DNS."
|
||||
private val syncZoneMessage: String = "This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."
|
||||
private val wrongRecordDataMessage: String = "The record data entered doesn't exist. Please enter the correct record data or leave the field empty if it's a delete operation."
|
||||
private val recordConflictMessage: String = "Conflict due to the record having the same name as an NS record in the same zone. Please create the record using the DNS service the NS record has been delegated to (ex. AWS r53), or use a different record name."
|
||||
|
||||
final case class Requeue(change: RecordSetChange) extends Throwable
|
||||
@ -392,12 +393,18 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
case AlreadyApplied(_) => Completed(change.successful)
|
||||
case ReadyToApply(_) => Validated(change)
|
||||
case Failure(_, message) =>
|
||||
if(message == outOfSyncFailureMessage || message == incompatibleRecordFailureMessage){
|
||||
if(message == outOfSyncFailureMessage){
|
||||
Completed(
|
||||
change.failed(
|
||||
syncZoneMessage
|
||||
)
|
||||
)
|
||||
} else if (message == incompatibleRecordFailureMessage) {
|
||||
Completed(
|
||||
change.failed(
|
||||
wrongRecordDataMessage
|
||||
)
|
||||
)
|
||||
} else if (message == "referral") {
|
||||
Completed(
|
||||
change.failed(
|
||||
|
@ -88,6 +88,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
|
||||
(
|
||||
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
|
||||
recordType,
|
||||
(js \ "systemMessage").optional[String],
|
||||
(js \ "ttl").optional[Long],
|
||||
recordType.andThen(extractRecord(_, js \ "record"))
|
||||
).mapN(AddChangeInput.apply)
|
||||
@ -114,6 +115,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
|
||||
(
|
||||
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
|
||||
recordType,
|
||||
(js \ "systemMessage").optional[String],
|
||||
recordData
|
||||
).mapN(DeleteRRSetChangeInput.apply)
|
||||
}
|
||||
|
@ -16,13 +16,12 @@
|
||||
|
||||
package vinyldns.api.route
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.{RejectionHandler, Route, ValidationRejection}
|
||||
import vinyldns.api.config.LimitsConfig
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import vinyldns.api.config.ManualReviewConfig
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.api.config.{LimitsConfig, ManualReviewConfig}
|
||||
import vinyldns.api.domain.batch._
|
||||
import vinyldns.core.domain.batch._
|
||||
|
||||
class BatchChangeRoute(
|
||||
batchChangeService: BatchChangeServiceAlgebra,
|
||||
@ -71,52 +70,52 @@ class BatchChangeRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
(get & monitor("Endpoint.listBatchChangeSummaries")) {
|
||||
parameters(
|
||||
"userName".as[String].?,
|
||||
"dateTimeRangeStart".as[String].?,
|
||||
"dateTimeRangeEnd".as[String].?,
|
||||
"startFrom".as[Int].?,
|
||||
"maxItems".as[Int].?(MAX_ITEMS_LIMIT),
|
||||
"ignoreAccess".as[Boolean].?(false),
|
||||
"approvalStatus".as[String].?
|
||||
) {
|
||||
(
|
||||
userName: Option[String],
|
||||
dateTimeRangeStart: Option[String],
|
||||
dateTimeRangeEnd: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean,
|
||||
approvalStatus: Option[String]
|
||||
) =>
|
||||
{
|
||||
val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
|
||||
(get & monitor("Endpoint.listBatchChangeSummaries")) {
|
||||
parameters(
|
||||
"userName".as[String].?,
|
||||
"dateTimeRangeStart".as[String].?,
|
||||
"dateTimeRangeEnd".as[String].?,
|
||||
"startFrom".as[Int].?,
|
||||
"maxItems".as[Int].?(MAX_ITEMS_LIMIT),
|
||||
"ignoreAccess".as[Boolean].?(false),
|
||||
"approvalStatus".as[String].?
|
||||
) {
|
||||
(
|
||||
userName: Option[String],
|
||||
dateTimeRangeStart: Option[String],
|
||||
dateTimeRangeEnd: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean,
|
||||
approvalStatus: Option[String]
|
||||
) =>
|
||||
{
|
||||
val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
|
||||
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
s"maxItems was $maxItems, maxItems must be between 1 and $MAX_ITEMS_LIMIT, inclusive."
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
batchChangeService.listBatchChangeSummaries(
|
||||
_,
|
||||
userName,
|
||||
dateTimeRangeStart,
|
||||
dateTimeRangeEnd,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess,
|
||||
convertApprovalStatus
|
||||
)
|
||||
) { summaries =>
|
||||
complete(StatusCodes.OK, summaries)
|
||||
}
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
s"maxItems was $maxItems, maxItems must be between 1 and $MAX_ITEMS_LIMIT, inclusive."
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
batchChangeService.listBatchChangeSummaries(
|
||||
_,
|
||||
userName,
|
||||
dateTimeRangeStart,
|
||||
dateTimeRangeEnd,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess,
|
||||
convertApprovalStatus
|
||||
)
|
||||
) { summaries =>
|
||||
complete(StatusCodes.OK, summaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / "batchrecordchanges" / Segment) { id =>
|
||||
(get & monitor("Endpoint.getBatchChange")) {
|
||||
|
@ -230,8 +230,8 @@ class RecordSetRoute(
|
||||
} ~
|
||||
path("zones" / Segment / "recordsetchanges") { zoneId =>
|
||||
(get & monitor("Endpoint.listRecordSetChanges")) {
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
|
||||
(startFrom: Option[Int], maxItems: Int, fqdn: Option[String], _: Option[String]) =>
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[Int], maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||
@ -240,7 +240,7 @@ class RecordSetRoute(
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
recordSetService
|
||||
.listRecordSetChanges(Some(zoneId), startFrom, maxItems, fqdn, None, _)
|
||||
.listRecordSetChanges(zoneId, startFrom, maxItems, _)
|
||||
) { changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ pyhamcrest==2.0.2
|
||||
pytz>=2014
|
||||
pytest==6.2.5
|
||||
mock==4.0.3
|
||||
dnspython==2.1.0
|
||||
dnspython==2.6.1
|
||||
boto3==1.18.51
|
||||
botocore==1.21.51
|
||||
requests==2.31.0
|
||||
requests==2.32.3
|
||||
pytest-xdist==2.4.0
|
||||
python-dateutil==2.8.2
|
||||
filelock==3.2.0
|
||||
pytest-custom_exit_code==0.3.0
|
||||
pytest-custom_exit_code==0.3.0
|
||||
|
@ -39,8 +39,6 @@ import vinyldns.core.domain.zone.Zone
|
||||
class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
private val nonExistentRecordDeleteMessage: String = "This record does not exist. " +
|
||||
"No further action is required."
|
||||
private val nonExistentRecordDataDeleteMessage: String = "Record data entered does not exist. " +
|
||||
"No further action is required."
|
||||
|
||||
private def makeSingleAddChange(
|
||||
name: String,
|
||||
@ -64,7 +62,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
)
|
||||
}
|
||||
|
||||
private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone) = {
|
||||
private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone, systemMessage: Option[String] = None) = {
|
||||
val fqdn = s"$name.${zone.name}"
|
||||
SingleDeleteRRSetChange(
|
||||
Some(zone.id),
|
||||
@ -74,7 +72,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
typ,
|
||||
None,
|
||||
SingleChangeStatus.Pending,
|
||||
None,
|
||||
systemMessage,
|
||||
None,
|
||||
None
|
||||
)
|
||||
@ -88,18 +86,19 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
AddChangeForValidation(
|
||||
okZone,
|
||||
s"$recordName",
|
||||
AddChangeInput(s"$recordName.ok.", typ, Some(123), recordData),
|
||||
AddChangeInput(s"$recordName.ok.", typ, None, Some(123), recordData),
|
||||
7200L
|
||||
)
|
||||
|
||||
private def makeDeleteRRSetChangeForValidation(
|
||||
recordName: String,
|
||||
typ: RecordType = RecordType.A
|
||||
typ: RecordType = RecordType.A,
|
||||
systemMessage: Option[String] = None
|
||||
): DeleteRRSetChangeForValidation =
|
||||
DeleteRRSetChangeForValidation(
|
||||
okZone,
|
||||
s"$recordName",
|
||||
DeleteRRSetChangeInput(s"$recordName.ok.", typ)
|
||||
DeleteRRSetChangeInput(s"$recordName.ok.", typ, systemMessage, None)
|
||||
)
|
||||
|
||||
private val addSingleChangesGood = List(
|
||||
@ -165,19 +164,11 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
)
|
||||
|
||||
private val singleChangesOneDelete = List(
|
||||
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A)
|
||||
)
|
||||
|
||||
private val singleChangesOneDeleteGood = List(
|
||||
makeSingleDeleteRRSetChange("aToDelete", A).copy(recordData = Some(AData("2.3.4.6"))),
|
||||
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A, okZone, Some(nonExistentRecordDeleteMessage))
|
||||
)
|
||||
|
||||
private val changeForValidationOneDelete = List(
|
||||
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A)
|
||||
)
|
||||
|
||||
private val changeForValidationOneDeleteGood = List(
|
||||
makeDeleteRRSetChangeForValidation("aToDelete", A)
|
||||
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A, Some(nonExistentRecordDeleteMessage))
|
||||
)
|
||||
|
||||
private val singleChangesOneBad = List(
|
||||
@ -621,45 +612,6 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
|
||||
}
|
||||
}
|
||||
|
||||
"updateBatchChange" should {
|
||||
"update the batch change system message when there is a delete request with non-existent record data" in {
|
||||
val batchWithBadChange =
|
||||
BatchChange(
|
||||
okUser.id,
|
||||
okUser.userName,
|
||||
None,
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
singleChangesOneDeleteGood,
|
||||
approvalStatus = BatchChangeApprovalStatus.AutoApproved
|
||||
)
|
||||
val result =
|
||||
underTest
|
||||
.updateBatchChange(
|
||||
batchWithBadChange,
|
||||
ChangeForValidationMap(changeForValidationOneDeleteGood.map(_.validNel), existingRecordSets),
|
||||
)
|
||||
|
||||
// validate the batch change returned
|
||||
val receivedChange = result.changes(0)
|
||||
receivedChange.systemMessage shouldBe Some(nonExistentRecordDataDeleteMessage)
|
||||
result.changes(0) shouldBe singleChangesOneDeleteGood(0).copy(systemMessage = Some(nonExistentRecordDataDeleteMessage))
|
||||
}
|
||||
}
|
||||
|
||||
"matchRecordData" should {
|
||||
"check if the record data given matches the record data present" in {
|
||||
val recordData = List(AData("1.2.3.5"), AAAAData("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
|
||||
val result1 = underTest.matchRecordData(recordData, AData("1.2.3.5"))
|
||||
result1 shouldBe true
|
||||
val result2 = underTest.matchRecordData(recordData, AData("1.2.3.4"))
|
||||
result2 shouldBe false
|
||||
val result3 = underTest.matchRecordData(recordData, AAAAData("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
|
||||
result3 shouldBe true
|
||||
val result4 = underTest.matchRecordData(recordData, AAAAData("abcd:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
|
||||
result4 shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"generateAddChange" should {
|
||||
val singleAddChange = makeSingleAddChange("shared-rs", AData("1.2.3.4"), A, sharedZone)
|
||||
val ownerGroupId = Some("some-owner-group-id")
|
||||
|
@ -31,16 +31,16 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
|
||||
|
||||
"BatchChangeInput" should {
|
||||
"ensure trailing dot on A, AAAA, and CNAME fqdn" in {
|
||||
val changeA = AddChangeInput("apex.test.com", A, Some(100), AData("1.1.1.1"))
|
||||
val changeA = AddChangeInput("apex.test.com", A, None, Some(100), AData("1.1.1.1"))
|
||||
val changeAAAA =
|
||||
AddChangeInput("aaaa.test.com", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
AddChangeInput("aaaa.test.com", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
val changeCname =
|
||||
AddChangeInput("cname.test.com", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com")))
|
||||
val changeADotted = AddChangeInput("adot.test.com.", A, Some(100), AData("1.1.1.1"))
|
||||
AddChangeInput("cname.test.com", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com")))
|
||||
val changeADotted = AddChangeInput("adot.test.com.", A, None, Some(100), AData("1.1.1.1"))
|
||||
val changeAAAADotted =
|
||||
AddChangeInput("aaaadot.test.com.", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
AddChangeInput("aaaadot.test.com.", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
val changeCnameDotted =
|
||||
AddChangeInput("cnamedot.test.com.", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com.")))
|
||||
AddChangeInput("cnamedot.test.com.", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com.")))
|
||||
|
||||
val input = BatchChangeInput(
|
||||
None,
|
||||
@ -58,7 +58,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
|
||||
}
|
||||
"asNewStoredChange" should {
|
||||
"Convert an AddChangeInput into SingleAddChange" in {
|
||||
val changeA = AddChangeInput("some.test.com", A, None, AData("1.1.1.1"))
|
||||
val changeA = AddChangeInput("some.test.com", A, None, None, AData("1.1.1.1"))
|
||||
val converted = changeA.asNewStoredChange(
|
||||
NonEmptyList.of(ZoneDiscoveryError("test")),
|
||||
VinylDNSTestHelpers.defaultTtl
|
||||
@ -80,7 +80,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
|
||||
asAdd.recordSetId shouldBe None
|
||||
}
|
||||
"Convert a DeleteChangeInput into SingleDeleteRRSetChange" in {
|
||||
val changeA = DeleteRRSetChangeInput("some.test.com", A)
|
||||
val changeA = DeleteRRSetChangeInput("some.test.com", A, None)
|
||||
val converted = changeA.asNewStoredChange(
|
||||
NonEmptyList.of(ZoneDiscoveryError("test")),
|
||||
VinylDNSTestHelpers.defaultTtl
|
||||
@ -111,14 +111,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
|
||||
1234,
|
||||
AData("1.2.3.4"),
|
||||
SingleChangeStatus.NeedsReview,
|
||||
Some("msg"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
|
||||
)
|
||||
|
||||
val expectedAddChange =
|
||||
AddChangeInput("testRname.testZoneName.", A, Some(1234), AData("1.2.3.4"))
|
||||
AddChangeInput("testRname.testZoneName.", A, None, Some(1234), AData("1.2.3.4"))
|
||||
|
||||
val singleDelChange = SingleDeleteRRSetChange(
|
||||
Some("testZoneId"),
|
||||
@ -128,14 +128,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
|
||||
A,
|
||||
None,
|
||||
SingleChangeStatus.NeedsReview,
|
||||
Some("msg"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
|
||||
)
|
||||
|
||||
val expectedDelChange =
|
||||
DeleteRRSetChangeInput("testRname.testZoneName.", A)
|
||||
DeleteRRSetChangeInput("testRname.testZoneName.", A, None)
|
||||
|
||||
val change = BatchChange(
|
||||
"userId",
|
||||
|
@ -81,34 +81,36 @@ class BatchChangeServiceSpec
|
||||
)
|
||||
private val ttl = Some(200L)
|
||||
|
||||
private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
|
||||
private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
|
||||
private val nonApexAddA =
|
||||
AddChangeInput("non-apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
|
||||
AddChangeInput("non-apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
|
||||
private val onlyApexAddA =
|
||||
AddChangeInput("only.apex.exists", RecordType.A, ttl, AData("1.1.1.1"))
|
||||
AddChangeInput("only.apex.exists", RecordType.A, None, ttl, AData("1.1.1.1"))
|
||||
private val onlyBaseAddAAAA =
|
||||
AddChangeInput("have.only.base", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
|
||||
private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, ttl, AData("1.1.1.1"))
|
||||
AddChangeInput("have.only.base", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
|
||||
private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, None, ttl, AData("1.1.1.1"))
|
||||
private val dottedAddA =
|
||||
AddChangeInput("dot.ted.apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
|
||||
AddChangeInput("dot.ted.apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
|
||||
private val cnameAdd =
|
||||
AddChangeInput("cname.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com.")))
|
||||
AddChangeInput("cname.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
|
||||
private val cnameApexAdd =
|
||||
AddChangeInput("apex.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com.")))
|
||||
AddChangeInput("apex.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
|
||||
private val cnameReverseAdd = AddChangeInput(
|
||||
"cname.55.144.10.in-addr.arpa",
|
||||
RecordType.CNAME,
|
||||
None,
|
||||
ttl,
|
||||
CNAMEData(Fqdn("testing.cname.com."))
|
||||
)
|
||||
private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
|
||||
private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
|
||||
private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
|
||||
private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
|
||||
private val ptrDelegatedAdd =
|
||||
AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
|
||||
AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
|
||||
private val ptrV6Add =
|
||||
AddChangeInput(
|
||||
"2001:0000:0000:0000:0000:ff00:0042:8329",
|
||||
RecordType.PTR,
|
||||
None,
|
||||
ttl,
|
||||
PTRData(Fqdn("ptr"))
|
||||
)
|
||||
@ -489,6 +491,7 @@ class BatchChangeServiceSpec
|
||||
val ptr = AddChangeInput(
|
||||
"2001:0000:0000:0001:0000:ff00:0042:8329",
|
||||
RecordType.PTR,
|
||||
None,
|
||||
ttl,
|
||||
PTRData(Fqdn("ptr"))
|
||||
)
|
||||
@ -520,6 +523,7 @@ class BatchChangeServiceSpec
|
||||
val ptr = AddChangeInput(
|
||||
"2001:0000:0000:0001:0000:ff00:0042:8329",
|
||||
RecordType.PTR,
|
||||
None,
|
||||
ttl,
|
||||
PTRData(Fqdn("ptr"))
|
||||
)
|
||||
@ -577,12 +581,12 @@ class BatchChangeServiceSpec
|
||||
}
|
||||
|
||||
"succeed with excluded TTL" in {
|
||||
val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, AData("1.1.1.1"))
|
||||
val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, None, AData("1.1.1.1"))
|
||||
val withTtl =
|
||||
AddChangeInput("with-ttl-add-2.test.com", RecordType.A, Some(900), AData("1.1.1.1"))
|
||||
val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT)
|
||||
AddChangeInput("with-ttl-add-2.test.com", RecordType.A, None, Some(900), AData("1.1.1.1"))
|
||||
val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT, None)
|
||||
val noTtlUpdate =
|
||||
AddChangeInput("non-apex.test.com.", RecordType.TXT, None, TXTData("hello"))
|
||||
AddChangeInput("non-apex.test.com.", RecordType.TXT, None, None, TXTData("hello"))
|
||||
|
||||
val input = BatchChangeInput(None, List(noTtl, withTtl, noTtlDel, noTtlUpdate))
|
||||
val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get
|
||||
@ -1172,7 +1176,7 @@ class BatchChangeServiceSpec
|
||||
"0.1.0.0.2.ip6.arpa."
|
||||
)
|
||||
|
||||
val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
|
||||
|
||||
val zoneNames = underTestPTRZonesList.zones.map(_.name)
|
||||
@ -1198,7 +1202,7 @@ class BatchChangeServiceSpec
|
||||
)
|
||||
|
||||
val ip = "2001:0db8:0000:0000:0000:ff00:0042:8329"
|
||||
val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
|
||||
|
||||
val zoneNames = underTestPTRZonesList.zones.map(_.name)
|
||||
@ -1245,7 +1249,7 @@ class BatchChangeServiceSpec
|
||||
|
||||
val ips = ip1 :: ip2s
|
||||
val ptrs = ips.map { v6Name =>
|
||||
AddChangeInput(v6Name, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
AddChangeInput(v6Name, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
|
||||
}
|
||||
|
||||
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(ptrs).unsafeRunSync()
|
||||
@ -1300,10 +1304,10 @@ class BatchChangeServiceSpec
|
||||
"properly discover records in forward zones" in {
|
||||
val apex = apexZone.name
|
||||
|
||||
val aApex = AddChangeInput(apex, RecordType.A, ttl, AData("1.2.3.4"))
|
||||
val aNormal = AddChangeInput(s"record.$apex", RecordType.A, ttl, AData("1.2.3.4"))
|
||||
val aApex = AddChangeInput(apex, RecordType.A, None, ttl, AData("1.2.3.4"))
|
||||
val aNormal = AddChangeInput(s"record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
|
||||
val aDotted =
|
||||
AddChangeInput(s"some.dotted.record.$apex", RecordType.A, ttl, AData("1.2.3.4"))
|
||||
AddChangeInput(s"some.dotted.record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
|
||||
|
||||
val expected = List(
|
||||
AddChangeForValidation(apexZone, apex, aApex, 7200L),
|
||||
@ -1322,10 +1326,10 @@ class BatchChangeServiceSpec
|
||||
"properly discover TXT records" in {
|
||||
val apex = apexZone.name
|
||||
|
||||
val txtApex = AddChangeInput(apex, RecordType.TXT, ttl, TXTData("test"))
|
||||
val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, ttl, TXTData("test"))
|
||||
val txtApex = AddChangeInput(apex, RecordType.TXT, None, ttl, TXTData("test"))
|
||||
val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
|
||||
val txtDotted =
|
||||
AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, ttl, TXTData("test"))
|
||||
AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
|
||||
|
||||
val expected = List(
|
||||
AddChangeForValidation(apexZone, apex, txtApex, 7200L),
|
||||
@ -1419,20 +1423,22 @@ class BatchChangeServiceSpec
|
||||
val ptrv6ZoneBig = Zone("0.1.0.0.2.ip6.arpa.", "email", id = "ptrv6big")
|
||||
|
||||
val smallZoneAdd =
|
||||
AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
|
||||
AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
|
||||
val medZoneAdd = AddChangeInput(
|
||||
"2001:0db8:0111:0000:0000:ff00:0042:8329",
|
||||
RecordType.PTR,
|
||||
None,
|
||||
ttl,
|
||||
PTRData(Fqdn("ptr"))
|
||||
)
|
||||
val bigZoneAdd = AddChangeInput(
|
||||
"2001:0000:0000:0000:0000:ff00:0042:8329",
|
||||
RecordType.PTR,
|
||||
None,
|
||||
ttl,
|
||||
PTRData(Fqdn("ptr"))
|
||||
)
|
||||
val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
|
||||
val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
|
||||
|
||||
val ptripv6Adds = List(
|
||||
smallZoneAdd.validNel,
|
||||
@ -1678,7 +1684,7 @@ class BatchChangeServiceSpec
|
||||
|
||||
"return a BatchChange if all data inputs are valid/soft failures and manual review is enabled and owner group ID " +
|
||||
"is provided" in {
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
|
||||
val result = underTestManualEnabled
|
||||
.buildResponse(
|
||||
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete), Some("owner-group-ID")),
|
||||
@ -1805,7 +1811,7 @@ class BatchChangeServiceSpec
|
||||
}
|
||||
|
||||
"return a BatchChangeErrorList if all data inputs are valid/soft failures and manual review is disabled" in {
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
|
||||
val result = underTest
|
||||
.buildResponse(
|
||||
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
|
||||
@ -1825,7 +1831,7 @@ class BatchChangeServiceSpec
|
||||
|
||||
"return a BatchChangeErrorList if all data inputs are valid/soft failures, scheduled, " +
|
||||
"and manual review is disabled" in {
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
|
||||
val result = underTest
|
||||
.buildResponse(
|
||||
BatchChangeInput(
|
||||
@ -1872,7 +1878,7 @@ class BatchChangeServiceSpec
|
||||
|
||||
"return a BatchChangeErrorList if all data inputs are valid/soft failures, manual review is enabled, " +
|
||||
"but batch change allowManualReview attribute is false" in {
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
|
||||
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
|
||||
val result = underTestManualEnabled
|
||||
.buildResponse(
|
||||
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1951,19 +1951,22 @@ class RecordSetServiceSpec
|
||||
val completeRecordSetChanges: List[RecordSetChange] =
|
||||
List(pendingCreateAAAA, pendingCreateCNAME, completeCreateAAAA, completeCreateCNAME)
|
||||
|
||||
doReturn(IO.pure(Some(zoneActive)))
|
||||
.when(mockZoneRepo)
|
||||
.getZone(zoneActive.id)
|
||||
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
|
||||
.when(mockRecordChangeRepo)
|
||||
.listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
|
||||
.listRecordSetChanges(zoneId = Some(zoneActive.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(zoneActive.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val changesWithName =
|
||||
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
zoneId = okZone.id,
|
||||
zoneId = zoneActive.id,
|
||||
recordSetChanges = changesWithName,
|
||||
nextId = None,
|
||||
startFrom = None,
|
||||
@ -2010,7 +2013,7 @@ class RecordSetServiceSpec
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
zoneId = okZone.id,
|
||||
recordSetChanges = List(),
|
||||
@ -2076,7 +2079,7 @@ class RecordSetServiceSpec
|
||||
|
||||
"return a NotAuthorizedError" in {
|
||||
val error =
|
||||
underTest.listRecordSetChanges(Some(zoneNotAuthorized.id), authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
|
||||
underTest.listRecordSetChanges(zoneNotAuthorized.id, authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
|
||||
|
||||
error shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
@ -2093,7 +2096,7 @@ class RecordSetServiceSpec
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val changesWithName =
|
||||
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
|
@ -127,15 +127,15 @@ class BatchChangeJsonProtocolSpec
|
||||
addCNAMEChangeInputJson
|
||||
)
|
||||
|
||||
val addAChangeInput = AddChangeInput("foo.", A, Some(3600), AData("1.1.1.1"))
|
||||
val addAChangeInput = AddChangeInput("foo.", A, None, Some(3600), AData("1.1.1.1"))
|
||||
|
||||
val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A)
|
||||
val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A, None)
|
||||
|
||||
val addAAAAChangeInput = AddChangeInput("bar.", AAAA, Some(1200), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
val addAAAAChangeInput = AddChangeInput("bar.", AAAA, None, Some(1200), AAAAData("1:2:3:4:5:6:7:8"))
|
||||
|
||||
val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, Some(200), CNAMEData(Fqdn("buzz.")))
|
||||
val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, None, Some(200), CNAMEData(Fqdn("buzz.")))
|
||||
|
||||
val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, Some(200), PTRData(Fqdn("test.com.")))
|
||||
val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, None, Some(200), PTRData(Fqdn("test.com.")))
|
||||
|
||||
val fooDiscoveryError = ZoneDiscoveryError("foo.")
|
||||
|
||||
@ -211,7 +211,7 @@ class BatchChangeJsonProtocolSpec
|
||||
)
|
||||
val result = ChangeInputSerializer.fromJson(json).value
|
||||
|
||||
result shouldBe AddChangeInput("foo.", A, None, AData("1.1.1.1"))
|
||||
result shouldBe AddChangeInput("foo.", A, None, None, AData("1.1.1.1"))
|
||||
}
|
||||
|
||||
"return an error if the record is not specified for add" in {
|
||||
@ -250,7 +250,7 @@ class BatchChangeJsonProtocolSpec
|
||||
|
||||
"serializing ChangeInputSerializer to JSON" should {
|
||||
"successfully serialize valid data for delete" in {
|
||||
val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, Some(AData("1.1.1.1")))
|
||||
val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, None, Some(AData("1.1.1.1")))
|
||||
val json: JObject = buildDeleteRRSetInputJson(Some("foo."), Some(A), Some(AData("1.1.1.1")))
|
||||
val result = ChangeInputSerializer.toJson(deleteChangeInput)
|
||||
|
||||
|
@ -758,16 +758,14 @@ class RecordSetRoutingSpec
|
||||
}.toResult
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: Option[String],
|
||||
zoneId: String,
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse] = {
|
||||
zoneId match {
|
||||
case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
|
||||
case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case notAuthorizedZone.id => Left(NotAuthorizedError("no way"))
|
||||
case _ => Right(listRecordSetChangesResponse)
|
||||
}
|
||||
}.toResult
|
||||
|
@ -352,6 +352,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="TRACE">
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
@ -46,11 +46,11 @@ sealed trait SingleChange {
|
||||
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
|
||||
}
|
||||
|
||||
def withDoesNotExistMessage(error: String): SingleChange = this match {
|
||||
def withDoesNotExistMessage: SingleChange = this match {
|
||||
case add: SingleAddChange =>
|
||||
add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
|
||||
add.copy(status = SingleChangeStatus.Failed)
|
||||
case delete: SingleDeleteRRSetChange =>
|
||||
delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error))
|
||||
delete.copy(status = SingleChangeStatus.Complete)
|
||||
}
|
||||
|
||||
def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange =
|
||||
|
@ -990,7 +990,10 @@ dotted-hosts = {
|
||||
# The time period within which the TCP binding process must be completed.
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ can read data from the Directory. Once you have that information, proceed to th
|
||||
**Considerations**
|
||||
You _should_ communicate to your Directory over LDAP using TLS. To do so, the SSL certs should be installed
|
||||
on the portal servers, or provided via a java trust store (key store). The portal provides an option to specific
|
||||
a java key store when it starts up.
|
||||
a java key store when it starts up. For more information: [Using Java Key Store In VinylDNS](https://github.com/vinyldns/vinyldns/tree/master/modules/portal#building-locally)
|
||||
|
||||
## Configuring LDAP
|
||||
Before you can configure LDAP, make note of the host, username, and password that you will be using.
|
||||
|
@ -216,13 +216,11 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
||||
}
|
||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
||||
val page1 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 0).unsafeRunSync()
|
||||
println(page1.items)
|
||||
page1.nextId shouldBe 3
|
||||
page1.maxItems shouldBe 2
|
||||
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
|
||||
|
||||
val page2 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, page1.nextId).unsafeRunSync()
|
||||
println(page2.items)
|
||||
page2.nextId shouldBe 6
|
||||
page2.maxItems shouldBe 2
|
||||
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(3, 5))
|
||||
|
@ -17,9 +17,9 @@
|
||||
package vinyldns.mysql.repository
|
||||
|
||||
import java.sql.Timestamp
|
||||
|
||||
import cats.data._
|
||||
import cats.effect._
|
||||
|
||||
import java.time.Instant
|
||||
import org.slf4j.LoggerFactory
|
||||
import scalikejdbc._
|
||||
@ -166,8 +166,8 @@ class MySqlBatchChangeRepository
|
||||
}
|
||||
|
||||
def getBatchFromSingleChangeId(
|
||||
singleChangeId: String
|
||||
)(implicit s: DBSession): Option[BatchChange] =
|
||||
singleChangeId: String
|
||||
)(implicit s: DBSession): Option[BatchChange] =
|
||||
GET_BATCH_CHANGE_METADATA_FROM_SINGLE_CHANGE
|
||||
.bind(singleChangeId)
|
||||
.map(extractBatchChange(None))
|
||||
@ -181,12 +181,9 @@ class MySqlBatchChangeRepository
|
||||
.apply()
|
||||
batchMeta.copy(changes = changes)
|
||||
}
|
||||
|
||||
monitor("repo.BatchChangeJDBC.updateSingleChanges") {
|
||||
IO {
|
||||
logger.info(
|
||||
s"Updating single change statuses: ${singleChanges.map(ch => (ch.id, ch.status))}"
|
||||
)
|
||||
logger.info(s"Updating single change status: ${singleChanges.map(ch => (ch.id, ch.status))}")
|
||||
DB.localTx { implicit s =>
|
||||
for {
|
||||
headChange <- singleChanges.headOption
|
||||
@ -194,8 +191,7 @@ class MySqlBatchChangeRepository
|
||||
_ = UPDATE_SINGLE_CHANGE.batchByName(batchParams: _*).apply()
|
||||
batchChange <- getBatchFromSingleChangeId(headChange.id)
|
||||
} yield batchChange
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +390,6 @@ class MySqlBatchChangeRepository
|
||||
case Left(e) => throw e
|
||||
}
|
||||
}
|
||||
|
||||
PUT_SINGLE_CHANGE.batchByName(singleChangesParams: _*).apply()
|
||||
batchChange
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.protobuf.ProtobufConversions
|
||||
import vinyldns.core.route.Monitored
|
||||
import vinyldns.mysql.repository.MySqlRecordSetRepository.fromRecordType
|
||||
import vinyldns.mysql.repository.MySqlRecordSetRepository.{fromRecordType, toFQDN}
|
||||
import vinyldns.proto.VinylDNSProto
|
||||
|
||||
class MySqlRecordChangeRepository
|
||||
@ -103,7 +103,7 @@ class MySqlRecordChangeRepository
|
||||
change.zoneId,
|
||||
change.created.toEpochMilli,
|
||||
fromChangeType(change.changeType),
|
||||
if(change.recordSet.name == change.zone.name) change.zone.name else change.recordSet.name + "." + change.zone.name,
|
||||
toFQDN(change.zone.name, change.recordSet.name),
|
||||
fromRecordType(change.recordSet.typ),
|
||||
toPB(change).toByteArray,
|
||||
)
|
||||
|
@ -326,7 +326,7 @@
|
||||
<label class="batch-change-csv-label btn btn-default" for="batchChangeCsv" id="batchChangeCsvImportLabel">
|
||||
<span><span class="glyphicon glyphicon-import"></span> Import CSV</span>
|
||||
</label>
|
||||
<input type="file" id="batchChangeCsv" ng-model="csvInput" name="batchChangeCsv" class="batchChangeCsv" ng-change="uploadCSV(createBatchChangeForm.batchChangeCsv.$viewValue)" batch-change-file>
|
||||
<input type="file" id="batchChangeCsv" ng-model="csvInput" name="batchChangeCsv" class="batchChangeCsv" ng-change="uploadCSV(createBatchChangeForm.batchChangeCsv.$viewValue, batchChangeLimit )" batch-change-file>
|
||||
<p><a href="https://www.vinyldns.io/portal/dns-changes#dns-change-csv-import" target="_blank" rel="noopener noreferrer">See documentation for sample CSV</a></p>
|
||||
</div>
|
||||
<p ng-if="newBatch.changes.length >= batchChangeLimit">Limit reached. Cannot add more than {{batchChangeLimit}} records per DNS change.</p>
|
||||
@ -336,6 +336,9 @@
|
||||
<button type="button" id="create-batch-changes-button" class="btn btn-primary" ng-click="submitChange(manualReviewEnabled)">Submit</button>
|
||||
</div>
|
||||
<div ng-if="formStatus=='pendingConfirm'" class="pull-right">
|
||||
<div class="modal fade" id="loader" tabindex="-1" role="dialog" >
|
||||
<div class="spinner" ></div>
|
||||
</div>
|
||||
<span ng-if="!batchChangeErrors">{{ confirmationPrompt }}</span>
|
||||
<span ng-if="batchChangeErrors" class="batch-change-error-help">There were errors, please review the highlighted rows and then proceed.</span>
|
||||
<button class="btn btn-default" ng-click="cancelSubmit()">Cancel</button>
|
||||
|
@ -347,7 +347,7 @@
|
||||
name="name"
|
||||
class="form-control"
|
||||
ng-model="currentGroup.name"
|
||||
ng-model-options="{ updateOn: 'submit' }"
|
||||
ng-change="checkForChanges()"
|
||||
type="text"
|
||||
required>
|
||||
</input>
|
||||
@ -360,7 +360,7 @@
|
||||
name="email"
|
||||
class="form-control"
|
||||
ng-model="currentGroup.email"
|
||||
ng-model-options="{ updateOn: 'submit' }"
|
||||
ng-change="checkForChanges()"
|
||||
type="text"
|
||||
required>
|
||||
</input>
|
||||
@ -389,7 +389,7 @@
|
||||
name="description"
|
||||
class="form-control"
|
||||
ng-model="currentGroup.description"
|
||||
ng-model-options="{ updateOn: 'submit' }"
|
||||
ng-change="checkForChanges()"
|
||||
type="text">
|
||||
</input>
|
||||
<span class="help-block">
|
||||
@ -399,7 +399,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="edit-group-button" class="btn btn-primary pull-right">Update</button>
|
||||
<button id="edit-group-button" class="btn btn-primary pull-right" ng-disabled="submitEditGroupForm.$invalid || !hasChanges">Update</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeEditModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,6 +67,7 @@
|
||||
<div class="col-md-2 pull-right">
|
||||
<form class="input-group remove-bottom-margin" ng-submit="refreshRecords()">
|
||||
<div class="input-group remove-bottom-margin">
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button id="record-search-button" type="submit" class="btn btn-primary"><span class="fa fa-search"></span></button>
|
||||
</span>
|
||||
@ -86,6 +87,9 @@
|
||||
<div class="panel-body">
|
||||
<div class="vinyldns-panel-top">
|
||||
<div class="btn-group">
|
||||
<span class="modal fade" id="loader" tabindex="-1" role="dialog" >
|
||||
<span class="spinner" ></span>
|
||||
</span>
|
||||
<button id="refresh-records-button" class="btn btn-default" ng-click="refreshRecords()"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
<button id="create-record-button" class="btn btn-default" ng-if="canReadZone && (zoneInfo.accessLevel == 'Delete' || canCreateRecordsViaAcl())" ng-click="createRecord(defaultTtl)"><span class="fa fa-plus"></span> Create Record Set</button>
|
||||
<button id="zone-sync-button" class="btn btn-default mb-control" ng-if="zoneInfo.accessLevel=='Delete'" data-toggle="modal" data-target="#mb-sync"><span class="fa fa-exchange"></span> Sync Zone</button>
|
||||
|
@ -282,12 +282,10 @@
|
||||
<div class="tab-pane" id="deletedZones">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<!-- SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
<button id="zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
|
||||
<button id="deleted-zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
|
||||
<span class="fa fa-refresh"></span> Refresh
|
||||
</button>
|
||||
|
||||
@ -295,7 +293,7 @@
|
||||
<div class="pull-right">
|
||||
<form class="input-group" ng-submit="refreshZones()">
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<span class="input-group-btn" >
|
||||
<button id="my-deleted-zones-search-button" type="submit" class="btn btn-primary btn-left-round">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
@ -304,192 +302,184 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- END SEARCH BOX -->
|
||||
|
||||
<!-- DELETED ZONES TABS -->
|
||||
<div class="panel panel-default panel-tabs">
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<li class="active"><a href="#myDeletedZones" data-toggle="tab" ng-click="myZonesAccess()" >My Zones</a></li>
|
||||
<li><a id="tab2-button" href="#allDeletedZones" data-toggle="tab" ng-click="allZonesAccess()">All Zones</a></li>
|
||||
</ul>
|
||||
<div class="panel-body tab-content">
|
||||
<div class="tab-pane active" id="myDeletedZones">
|
||||
<div id="zone-list-table" class="panel-body">
|
||||
<p ng-if="!myDeletedZonesLoaded">Loading my deleted zones...</p>
|
||||
<p ng-if="myDeletedZonesLoaded && !myDeletedZones.length">No zones match the search criteria.</p>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table class="table" ng-if="myDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in myDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="allDeletedZones">
|
||||
<div id="zone-list-table" class="panel-body">
|
||||
<p ng-if="!allDeletedZonesLoaded">Loading all deleted zones...</p>
|
||||
<p ng-if="allDeletedZonesLoaded && !allDeletedZones.length">No zones match the search criteria.</p>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table class="table" ng-if="allDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in allDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END DELETED ZONES TABS -->
|
||||
</div>
|
||||
<!-- END SEARCH BOX -->
|
||||
|
||||
<!-- DELETED ZONES TABS -->
|
||||
<div class="panel-default panel-tabs">
|
||||
<span class="container-fluid" >
|
||||
<!-- MY DELETED ZONE PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_paginate" ng-show="tab == 'myDeletedZones'" >
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="vinyldns_paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="vinyldns_paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- MY DELETED ZONE END PAGINATION -->
|
||||
<!-- ALL DELETED ZONE PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_paginate" ng-show="tab == 'allDeletedZones'" >
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination ">
|
||||
<li class="vinyldns_paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="vinyldns_paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--ALL DELETED ZONE END PAGINATION -->
|
||||
<ul class="nav nav-tabs bar_tabs" ng-init = "tab='myDeletedZones'">
|
||||
<li class = "active"><a ng-class="{active: tab == 'myDeletedZones'}" id = "myDeletedZone" href="#myDeletedZones" data-toggle="tab" ng-click="tab = 'myDeletedZones'; myZonesAccess()" >My Zones</a></li>
|
||||
<li><a ng-class="{active: tab == 'allDeletedZones'}" href="#allDeletedZones" data-toggle="tab" ng-click="tab = 'allDeletedZones'; allZonesAccess()" >All Zones</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="myDeletedZones">
|
||||
<div id="my-deleted-zone-list-table" class="panel-body">
|
||||
<p ng-if="!myDeletedZonesLoaded">Loading my deleted zones...</p>
|
||||
<p ng-if="myDeletedZonesLoaded && !myDeletedZones.length">No zones match the search criteria.</p>
|
||||
<table class="table" ng-if="myDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in myDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{deletedZone.zoneChange.zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="allDeletedZones">
|
||||
<div id="all-deleted-zone-list-table" class="panel-body">
|
||||
<p ng-if="!allDeletedZonesLoaded">Loading all deleted zones...</p>
|
||||
<p ng-if="allDeletedZonesLoaded && !allDeletedZones.length">No zones match the search criteria.</p>
|
||||
<table class="table" ng-if="allDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in allDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{deletedZone.zoneChange.zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<!-- END DELETED ZONES TABS -->
|
||||
<div class="panel-footer"></div>
|
||||
</div>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END VERTICAL TABS -->
|
||||
|
||||
|
||||
</div>
|
||||
<!-- END PAGE CONTENT WRAPPER -->
|
||||
|
||||
</div>
|
||||
<!-- END PAGE CONTENT -->
|
||||
|
||||
|
@ -88,6 +88,9 @@ api {
|
||||
}
|
||||
}
|
||||
|
||||
# Batch change limit
|
||||
batch-change-limit = 1000
|
||||
|
||||
http.port = 9001
|
||||
http.port = ${?PORTAL_PORT}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<logger name="play" level="INFO" />
|
||||
<logger name="application" level="DEBUG" />
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="TRACE">
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
@ -462,6 +462,10 @@ input[type="file"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.vinyldns_paginate_button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.no-top-margin {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
@ -210,8 +210,8 @@ angular.module('controller.groups', []).controller('GroupsController', function
|
||||
handleError(error, 'groupsService::getGroups-failure');
|
||||
});
|
||||
}
|
||||
//Function for fetching list of valid domains
|
||||
|
||||
//Function for fetching list of valid domains
|
||||
$scope.validDomains=function getValidEmailDomains() {
|
||||
function success(response) {
|
||||
$log.debug('groupsService::listEmailDomains-success', response);
|
||||
@ -247,10 +247,25 @@ angular.module('controller.groups', []).controller('GroupsController', function
|
||||
|
||||
$scope.editGroup = function (groupInfo) {
|
||||
$scope.currentGroup = groupInfo;
|
||||
$scope.initialGroup = {
|
||||
name: $scope.currentGroup.name,
|
||||
email: $scope.currentGroup.email,
|
||||
description: $scope.currentGroup.description
|
||||
};
|
||||
$scope.hasChanges = false;
|
||||
$scope.validDomains();
|
||||
$("#modal_edit_group").modal("show");
|
||||
};
|
||||
|
||||
// Function to check for changes
|
||||
$scope.checkForChanges = function() {
|
||||
$scope.hasChanges =
|
||||
$scope.currentGroup.name !== $scope.initialGroup.name ||
|
||||
$scope.currentGroup.email !== $scope.initialGroup.email ||
|
||||
($scope.currentGroup.description !== $scope.initialGroup.description &&
|
||||
!($scope.currentGroup.description === "" && $scope.initialGroup.description === undefined));
|
||||
};
|
||||
|
||||
$scope.getGroupAndUpdate = function(groupId, name, email, description) {
|
||||
function success(response) {
|
||||
$log.debug('groupsService::getGroup-success');
|
||||
@ -281,6 +296,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
|
||||
return groupsService.updateGroup(groupId, payload)
|
||||
.then(success)
|
||||
.catch(function (error) {
|
||||
$scope.closeEditModal();
|
||||
handleError(error, 'groupsService::updateGroup-failure');
|
||||
});
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
$scope.manualReviewEnabled;
|
||||
$scope.naptrFlags = ["U", "S", "A", "P"];
|
||||
|
||||
|
||||
$scope.addSingleChange = function() {
|
||||
$scope.newBatch.changes.push({changeType: "Add", type: "A+PTR"});
|
||||
var changesLength = $scope.newBatch.changes.length;
|
||||
@ -116,7 +117,7 @@
|
||||
$scope.alerts.push(alert);
|
||||
$timeout(function(){
|
||||
location.href = "/dnschanges/" + response.data.id;
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
$scope.batch = response.data;
|
||||
}
|
||||
|
||||
@ -161,14 +162,14 @@
|
||||
$scope.alerts.push(alert);
|
||||
}
|
||||
|
||||
$scope.uploadCSV = function(file) {
|
||||
parseFile(file).then(function(dataLength){
|
||||
$scope.alerts.push({type: 'success', content: 'Successfully imported ' + dataLength + ' changes.' });
|
||||
$scope.uploadCSV = function(file, batchChangeLimit) {
|
||||
parseFile(file, batchChangeLimit).then(function(dataLength){
|
||||
$scope.alerts.push({type: 'success', content: 'Successfully imported ' + dataLength + ' DNS changes.' });
|
||||
}, function(error) {
|
||||
$scope.alerts.push({type: 'danger', content: error});
|
||||
});
|
||||
|
||||
function parseFile(file) {
|
||||
function parseFile(file, batchChangeLimit) {
|
||||
return $q(function(resolve, reject) {
|
||||
if (!file.name.endsWith('.csv')) {
|
||||
reject("Import failed. File should be of ‘.csv’ type.");
|
||||
@ -177,6 +178,9 @@
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var rows = e.target.result.split("\n");
|
||||
if(rows.length - 1 > batchChangeLimit)
|
||||
{reject("Import failed. Cannot add more than " + batchChangeLimit + " records per DNS change.");
|
||||
} else {
|
||||
if (rows[0].trim() == "Change Type,Record Type,Input Name,TTL,Record Data") {
|
||||
$scope.newBatch.changes = [];
|
||||
for(var i = 1; i < rows.length; i++) {
|
||||
@ -186,10 +190,10 @@
|
||||
}
|
||||
$scope.$apply()
|
||||
resolve($scope.newBatch.changes.length);
|
||||
} else {
|
||||
} else {
|
||||
reject("Import failed. CSV header must be: Change Type,Record Type,Input Name,TTL,Record Data");
|
||||
}
|
||||
}
|
||||
}}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
|
@ -30,7 +30,24 @@
|
||||
"allowManualReview": allowManualReview
|
||||
}
|
||||
var url = utilityService.urlBuilder('/api/dnschanges', params);
|
||||
return $http.post(url, data, {headers: utilityService.getCsrfHeader()});
|
||||
let loader = $("#loader");
|
||||
loader.modal({
|
||||
backdrop: "static",
|
||||
keyboard: false, //remove option to close with keyboard
|
||||
show: true //Display loader!
|
||||
})
|
||||
let promis = $http.post(url, data, {headers: utilityService.getCsrfHeader()});
|
||||
|
||||
function hideLoader() {
|
||||
loader.modal("hide");
|
||||
|
||||
// Manually remove the backdrop after the modal is hidden
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open'); // Remove the class that prevents scrolling
|
||||
}
|
||||
// Hide loader when api gets response
|
||||
promis.then(hideLoader, hideLoader).catch(hideLoader).finally(hideLoader);
|
||||
return promis
|
||||
};
|
||||
|
||||
this.getBatchChanges = function (maxItems, startFrom, ignoreAccess, approvalStatus, userName, dateTimeRangeStart, dateTimeRangeEnd) {
|
||||
|
@ -279,7 +279,7 @@
|
||||
$scope.changeHistoryPrevPage = function() {
|
||||
var startFrom = pagingService.getPrevStartFrom(changePaging);
|
||||
return recordsService
|
||||
.listRecordSetChangeHistory(changePaging.maxItems, startFrom, $scope.recordFqdn, $scope.recordType)
|
||||
.listRecordSetChangeHistory($scope.zoneId, changePaging.maxItems, startFrom, $scope.recordFqdn, $scope.recordType)
|
||||
.then(function(response) {
|
||||
changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging);
|
||||
updateChangeDisplay(response.data.recordSetChanges);
|
||||
@ -291,7 +291,7 @@
|
||||
|
||||
$scope.changeHistoryNextPage = function() {
|
||||
return recordsService
|
||||
.listRecordSetChangeHistory(changePaging.maxItems, changePaging.next, $scope.recordFqdn, $scope.recordType)
|
||||
.listRecordSetChangeHistory($scope.zoneId, changePaging.maxItems, changePaging.next, $scope.recordFqdn, $scope.recordType)
|
||||
.then(function(response) {
|
||||
var changes = response.data.recordSetChanges;
|
||||
changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging);
|
||||
|
@ -77,7 +77,16 @@ angular.module('service.records', [])
|
||||
"recordTypeSort": recordTypeSort
|
||||
};
|
||||
var url = utilityService.urlBuilder("/api/zones/"+id+"/recordsets", params);
|
||||
return $http.get(url);
|
||||
let loader = $("#loader");
|
||||
loader.modal({
|
||||
backdrop: "static", //remove ability to close modal with click
|
||||
keyboard: false, //remove option to close with keyboard
|
||||
show: true //Display loader!
|
||||
})
|
||||
let promis = $http.get(url);
|
||||
// Hide loader when api gets response
|
||||
promis.then(()=>loader.modal("hide"), ()=>loader.modal("hide"))
|
||||
return promis;
|
||||
};
|
||||
|
||||
this.getRecordSet = function (rsid) {
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
|
||||
# LDAP container hosting example users
|
||||
@ -9,7 +7,7 @@ services:
|
||||
ports:
|
||||
- "19004:19004"
|
||||
|
||||
# Integration image hosting r53, sns, sqs, bind, and mysql
|
||||
# Integration image hosting r53, sns, sqs, bind and mysql
|
||||
integration:
|
||||
container_name: "vinyldns-api-integration"
|
||||
hostname: "vinyldns-integration"
|
||||
|
@ -302,6 +302,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -360,6 +360,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
version in ThisBuild := "0.20.0"
|
||||
version in ThisBuild := "0.20.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user