From 85577a27f0f0232c8e2aa541a59c8235a85023e7 Mon Sep 17 00:00:00 2001 From: Ryan Emerle Date: Wed, 8 Dec 2021 14:36:00 -0500 Subject: [PATCH] Consistency Updates - Clean up documentation - Update architecure diagram - Fix discrepencies between local and docker test execution - Fix inconsistencies in various configuration files used for tests and execution --- AUTHORS.md | 9 +- DEVELOPER_GUIDE.md | 168 +++---- MAINTAINERS.md | 6 - README.md | 29 +- SYSTEM_DESIGN.md | 53 +- build.sbt | 10 +- build/docker/.env | 2 +- build/docker/api/application.conf | 8 +- build/sbt.sh | 2 +- img/VinylDNS_overview.png | Bin 56261 -> 0 bytes img/vinyldns_overview.png | Bin 0 -> 93231 bytes modules/api/src/it/resources/application.conf | 345 +++++++++++-- .../dns/DnsBackendIntegrationSpec.scala | 7 +- .../zone/ZoneViewLoaderIntegrationSpec.scala | 6 +- .../sns/SnsNotifierIntegrationSpec.scala | 11 +- .../route53/Route53ApiIntegrationSpec.scala | 5 +- .../api/src/main/resources/application.conf | 461 ++++++++++++------ modules/api/src/main/resources/reference.conf | 8 + .../api/src/main/resources/vinyldns-ascii.txt | 12 +- .../api/src/test/resources/application.conf | 383 +++++++++++---- .../api/config/VinylDNSConfigSpec.scala | 29 +- .../api/src/universal/conf/application.conf | 341 ++++++++++--- .../docs/src/main/mdoc/operator/config-api.md | 2 +- .../src/main/mdoc/operator/config-portal.md | 33 +- .../mysql/src/it/resources/application.conf | 10 +- modules/mysql/src/main/resources/test/ddl.sql | 82 ++-- .../scala/vinyldns/mysql/MySqlConnector.scala | 27 +- modules/portal/conf/application.conf | 71 ++- .../backend/Route53IntegrationSpec.scala | 4 +- modules/sqs/src/it/resources/application.conf | 1 + ...sMessageQueueProviderIntegrationSpec.scala | 151 +++--- quickstart/.env | 3 +- test/api/functional/application.conf | 2 +- test/api/integration/.env.integration | 27 + test/api/integration/Makefile | 9 +- test/api/integration/application.conf | 2 +- 36 files changed, 1597 insertions(+), 722 deletions(-) delete mode 100644 img/VinylDNS_overview.png create mode 100755 img/vinyldns_overview.png create mode 100755 test/api/integration/.env.integration diff --git a/AUTHORS.md b/AUTHORS.md index bd5098d89..6e42640fd 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,14 +3,9 @@ This project would not be possible without the generous contributions of many people. Thank you! If you have contributed in any way, but do not see your name here, please open a PR to add yourself (in alphabetical order by last name)! -## DNS SMEs - -- Joe Crowe -- David Back -- Hong Ye - ## Contributors +- David Back - Mike Ball - Tommy Barker - Robert Barrimond @@ -23,6 +18,7 @@ in any way, but do not see your name here, please open a PR to add yourself (in - Peter Cline - Kemar Cockburn - Luke Cori +- Joe Crowe - Jearvon Dharrie - Andrew Dunn - Ryan Emerle @@ -54,3 +50,4 @@ in any way, but do not see your name here, please open a PR to add yourself (in - Andrew Wang - Peter Willis - Britney Wright +- Hong Ye diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 5bfce78b9..4d66eab0e 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -2,10 +2,28 @@ ## Table of Contents -- [Developer Requirements](#developer-requirements) +- [Developer Requirements (Local)](#developer-requirements-local) +- [Developer Requirements (Docker)](#developer-requirements-docker) - [Project Layout](#project-layout) + * [Core](#core) + * [API](#api) + * [Portal](#portal) + * [Documentation](#documentation) - [Running VinylDNS Locally](#running-vinyldns-locally) + * [Starting the API Server](#starting-the-api-server) + * [Starting the Portal](#starting-the-portal) - [Testing](#testing) + * [Unit Tests](#unit-tests) + * [Integration Tests](#integration-tests) + + [Running both](#running-both) + * [Functional Tests](#functional-tests) + + [Running Functional Tests](#running-functional-tests) + - [API Functional Tests](#api-functional-tests) + + [Setup](#setup) + - [Functional Test Context](#functional-test-context) + - [Partitioning](#partitioning) + - [Really Important Test Context Rules!](#really-important-test-context-rules) + - [Managing Test Zone Files](#managing-test-zone-files) ## Developer Requirements (Local) @@ -41,14 +59,14 @@ its components. The main codebase is a multi-module Scala project with multiple sub-modules. To start working with the project, from the root directory run `sbt`. Most of the code can be found in the `modules` directory. The following modules are present: -* `root` - this is the parent project, if you run tasks here, it will run against all sub-modules -* [`core`](#core): core modules that are used by both the API and portal, such as cryptography implementations. -* [`api`](#api): the API is the main engine for all of VinylDNS. This is the most active area of the codebase, as +- `root` - this is the parent project, if you run tasks here, it will run against all sub-modules +- [`core`](#core): core modules that are used by both the API and portal, such as cryptography implementations. +- [`api`](#api): the API is the main engine for all of VinylDNS. This is the most active area of the codebase, as everything else typically just funnels through the API. -* [`portal`](#portal): The portal is a user interface wrapper around the API. Most of the business rules, logic, and +- [`portal`](#portal): The portal is a user interface wrapper around the API. Most of the business rules, logic, and processing can be found in the API. The _only_ features in the portal not found in the API are creation of users and user authentication. -* [`docs`](#documentation): documentation for VinylDNS. +- [`docs`](#documentation): documentation for VinylDNS. ### Core @@ -56,76 +74,76 @@ Code that is used across multiple modules in the VinylDNS ecosystem live in `cor #### Code Layout -* `src/main` - the main source code -* `src/test` - unit tests +- `src/main` - the main source code +- `src/test` - unit tests ### API The API is the RESTful API for interacting with VinylDNS. The following technologies are used: -* [Akka HTTP](https://doc.akka.io/docs/akka-http/current/) - Used primarily for REST and HTTP calls. -* [FS2](https://functional-streams-for-scala.github.io/fs2/) - Used for backend change processing off of message queues. +- [Akka HTTP](https://doc.akka.io/docs/akka-http/current/) - Used primarily for REST and HTTP calls. +- [FS2](https://functional-streams-for-scala.github.io/fs2/) - Used for backend change processing off of message queues. FS2 has back-pressure built in, and gives us tools like throttling and concurrency. -* [Cats Effect](https://typelevel.org/cats-effect/) - A replacement of `Future` with the `IO` monad -* [Cats](https://typelevel.org/cats) - Used for functional programming. -* [PureConfig](https://pureconfig.github.io/) - For loading configuration values. +- [Cats Effect](https://typelevel.org/cats-effect/) - A replacement of `Future` with the `IO` monad +- [Cats](https://typelevel.org/cats) - Used for functional programming. +- [PureConfig](https://pureconfig.github.io/) - For loading configuration values. The API has the following dependencies: -* MySQL - the SQL database that houses the data -* SQS - for managing concurrent updates and enabling high-availability -* Bind9 - for testing integration with a real DNS system +- MySQL - the SQL database that houses the data +- SQS - for managing concurrent updates and enabling high-availability +- Bind9 - for testing integration with a real DNS system #### Code Layout The API code can be found in `modules/api`. -* `src/it` - integration tests -* `src/main` - the main source code -* `src/test` - unit tests -* `src/universal` - items that are packaged in the Docker image for the VinylDNS API +- `src/it` - integration tests +- `src/main` - the main source code +- `src/test` - unit tests +- `src/universal` - items that are packaged in the Docker image for the VinylDNS API The package structure for the source code follows: -* `vinyldns.api.domain` - contains the core front-end logic. This includes things like the application services, +- `vinyldns.api.domain` - contains the core front-end logic. This includes things like the application services, repository interfaces, domain model, validations, and business rules. -* `vinyldns.api.engine` - the back-end processing engine. This is where we process commands including record changes, +- `vinyldns.api.engine` - the back-end processing engine. This is where we process commands including record changes, zone changes, and zone syncs. -* `vinyldns.api.protobuf` - marshalling and unmarshalling to and from protobuf to types in our system -* `vinyldns.api.repository` - repository implementations live here -* `vinyldns.api.route` - HTTP endpoints +- `vinyldns.api.protobuf` - marshalling and unmarshalling to and from protobuf to types in our system +- `vinyldns.api.repository` - repository implementations live here +- `vinyldns.api.route` - HTTP endpoints ### Portal The project is built using: -* [Play Framework](https://www.playframework.com/documentation/2.6.x/Home) -* [AngularJS](https://angularjs.org/) +- [Play Framework](https://www.playframework.com/documentation/2.6.x/Home) +- [AngularJS](https://angularjs.org/) The portal is _mostly_ a shim around the API. Most actions in the user interface are translated into API calls. The features that the Portal provides that are not in the API include: -* Authentication against LDAP -* Creation of users - when a user logs in for the first time, VinylDNS automatically creates a user and new credentials +- Authentication against LDAP +- Creation of users - when a user logs in for the first time, VinylDNS automatically creates a user and new credentials for them in the database with their LDAP information. #### Code Layout The portal code can be found in `modules/portal`. -* `app` - source code for portal back-end - * `models` - data structures that are used by the portal - * `views` - HTML templates for each web page - * `controllers` - logic for updating data -* `conf` - configurations and endpoint routes -* `public` - source code for portal front-end - * `css` - stylesheets - * `images` - images, including icons, used in the portal - * `js` - scripts - * `mocks` - mock JSON used in Grunt tests - * `templates` - modal templates -* `test` - unit tests for portal back-end +- `app` - source code for portal back-end + - `models` - data structures that are used by the portal + - `views` - HTML templates for each web page + - `controllers` - logic for updating data +- `conf` - configurations and endpoint routes +- `public` - source code for portal front-end + - `css` - stylesheets + - `images` - images, including icons, used in the portal + - `js` - scripts + - `mocks` - mock JSON used in Grunt tests + - `templates` - modal templates +- `test` - unit tests for portal back-end ### Documentation @@ -134,8 +152,8 @@ settings for the microsite are also configured in `build.sbt` of the project roo #### Code Layout -* `src/main/resources` - Microsite resources and configurations -* `src/main/mdoc` - Content for microsite web pages +- `src/main/resources` - Microsite resources and configurations +- `src/main/mdoc` - Content for microsite web pages ## Running VinylDNS Locally @@ -146,20 +164,19 @@ README. However, VinylDNS can also be run in the foreground. Before starting the API service, you can start the dependencies for local development: -``` -cd test/api/integration -make build && make run-bg +```shell +quickstart/quickstart-vinyldns.sh --deps-only ``` This will start a container running in the background with necessary prerequisites. Once the prerequisites are running, you can start up sbt by running `sbt` from the root directory. -* `project api` to change the sbt project to the API -* `reStart` to start up the API server -* Wait until you see the message `VINYLDNS SERVER STARTED SUCCESSFULLY` before working with the server -* To stop the VinylDNS server, run `reStop` from the api project -* To stop the dependent Docker containers: `utils/clean-vinyldns-containers.sh` +- `project api` to change the sbt project to the API +- `reStart` to start up the API server +- Wait until you see the message `VINYLDNS SERVER STARTED SUCCESSFULLY` before working with the server +- To stop the VinylDNS server, run `reStop` from the api project +- To stop the dependent Docker containers: `utils/clean-vinyldns-containers.sh` See the [API Configuration Guide](https://www.vinyldns.io/operator/config-api) for information regarding API configuration. @@ -167,9 +184,9 @@ configuration. ### Starting the Portal To run the portal locally, you _first_ have to start up the VinylDNS API Server. This can be done by following the -instructions for [Staring the API Server](#Starting the API Server) or by using the QuickStart: +instructions for [Staring the API Server](#starting-the-api-server) or by using the QuickStart: -``` +```shell quickstart/quickstart-vinyldns.sh --api-only ``` @@ -179,29 +196,6 @@ execute `;preparePortal; run`. See the [Portal Configuration Guide](https://www.vinyldns.io/operator/config-portal) for information regarding portal configuration. -### Loading test data - -Normally the portal can be used for all VinylDNS requests. Test users are locked down to only have access to test zones, -which the portal connection modal has not been updated to incorporate. To connect to a zone with testuser, you will need -to use an alternative client and set `isTest=true` on the zone being connected to. - -Use the vinyldns-js client (Note, you need Node installed): - -``` -git clone https://github.com/vinyldns/vinyldns-js.git -cd vinyldns-js -npm install -export VINYLDNS_API_SERVER=http://localhost:9000 -export VINYLDNS_ACCESS_KEY_ID=testUserAccessKey -export VINYLDNS_SECRET_ACCESS_KEY=testUserSecretKey -npm run repl -> var groupId; -> vinyl.createGroup({"name": "test-group", "email":"test@test.com", members: [{id: "testuser"}], admins: [{id: "testuser"}]}).then(res => {groupId = res.id}).catch(err => {console.log(err)}); -> vinyl.createZone ({name: "ok.", isTest: true, adminGroupId: groupId, email: "test@test.com"}).then(res => { console.log(res) }).catch(err => { console.log(err) }) - -You should now be able to see the zone in the portal at localhost:9001 when logged in as username=testuser password=testpassword -``` - ## Testing ### Unit Tests @@ -247,7 +241,7 @@ When adding new features, you will often need to write new functional tests that To run functional tests you can simply execute the following commands: -``` +```shell build/func-test-api.sh build/func-test-portal.sh ``` @@ -259,9 +253,9 @@ These command will run the API functional tests and portal functional tests resp To run functional tests you can simply execute `build/func-test-api.sh`, but if you'd like finer-grained control, you can work with the `Makefile` in `test/api/functional`: -``` -cd test/api/functional -make build && make run +```shell +# Build and then run the function test container +make -C test/api/functional build run ``` During iterative test development, you can use `make run-local` which will bind-mount the current functional tests in @@ -289,20 +283,20 @@ There aren't a lot, so it should be quick. In the `modules/api/src/test/functional` directory are a few important files for you to be familiar with: -* `vinyl_python.py` - this provides the interface to the VinylDNS API. It handles signing the request for you, as well +- `vinyl_python.py` - this provides the interface to the VinylDNS API. It handles signing the request for you, as well as building and executing the requests, and giving you back valid responses. For all new API endpoints, there should be a corresponding function in the vinyl_client -* `utils.py` - provides general use functions that can be used anywhere in your tests. Feel free to contribute new +- `utils.py` - provides general use functions that can be used anywhere in your tests. Feel free to contribute new functions here when you see repetition in the code In the `modules/api/src/test/functional/tests` directory, we have directories / modules for different areas of the application. -* `batch` - for managing batch updates -* `internal` - for internal endpoints (not intended for public consumption) -* `membership` - for managing groups and users -* `recordsets` - for managing record sets -* `zones` - for managing zones +- `batch` - for managing batch updates +- `internal` - for internal endpoints (not intended for public consumption) +- `membership` - for managing groups and users +- `recordsets` - for managing record sets +- `zones` - for managing zones ##### Functional Test Context diff --git a/MAINTAINERS.md b/MAINTAINERS.md index a048cc87f..6a0e5ebc6 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -27,12 +27,6 @@ The offline root key and repository keys are managed by the core maintainer team * api key: used to sign tagged images in vinyldns/api * portal key: used to sign tagged images in vinyldns/portal -These keys are named in a .key format, e.g. 5526ecd15bd413e08718e66c440d17a28968d5cd2922b59a17510da802ca6572.key, -do not change the names of the keys. - -Docker expects these keys to be saved in `~/.docker/trust/private`. Each key is encrypted with a passphrase, that you -must have available when pushing an image. - ## Release Process The release process is automated by GitHub Actions. diff --git a/README.md b/README.md index 4068fd50f..6e262c095 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ ![Build](https://github.com/vinyldns/vinyldns/workflows/Continuous%20Integration/badge.svg) -[![CodeCov ](https://codecov.io/gh/vinyldns/vinyldns/branch/master/graph/badge.svg)](https://codecov.io/gh/vinyldns/vinyldns) [![License](https://img.shields.io/github/license/vinyldns/vinyldns)](https://github.com/vinyldns/vinyldns/blob/master/LICENSE) [![conduct](https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg)](https://github.com/vinyldns/vinyldns/blob/master/CODE_OF_CONDUCT.md)

- + VinylDNS that can be used to make API requests, using the endpoint `http://localhost:9000` -## Things to try in the portal +## Things to Try in the Portal 1. View the portal at in a web browser 2. Login with the credentials `professor` and `professor` 3. Navigate to the `groups` tab: 4. Click on the **New Group** button and create a new group, the group id is the uuid in the url after you view the group -5. View zones you connected to in the `zones` tab: . For a quick test, create a new zone - named `ok` with an email of `test@test.com` and choose a group you created from the previous step. (Note, - see [Developer Guide](DEVELOPER_GUIDE.md#loading-test-data) for creating a zone) -6. You will see that some records are preloaded in the zoned already, this is because these records are preloaded in the +5. Connect a zone by going to the `zones` tab: . + 1. Click the `-> Connect` button + 2. For `Zone Name` enter `ok` with an email of `test@test.com` + 3. For `Admin Group`, choose a group you created from the previous step + 4. Leave everything else as-is and click the `Connect` button at the bottom of the form +6. A new zone `ok` should appear in your `My Zones` tab _(you may need to refresh your browser)_ +7. You will see that some records are preloaded in the zone already, this is because these records are preloaded in the local docker DNS server and VinylDNS automatically syncs records with the backend DNS server upon zone connection -7. From here, you can create DNS record sets in the **Manage Records** tab, and manage zone settings and ***ACL rules*** +8. From here, you can create DNS record sets in the **Manage Records** tab, and manage zone settings and ***ACL rules*** in the **Manage Zone** tab -8. To try creating a DNS record, click on the **Create Record Set** button under +9. To try creating a DNS record, click on the **Create Record Set** button under Records, `Record Type = A, Record Name = my-test-a, TTL = 300, IP Addressess = 1.1.1.1` -9. Click on the **Refresh** button under Records, you should see your new record created +10. Click on the **Refresh** button under Records, you should see your new record created ### Verifying Your Changes @@ -96,7 +101,7 @@ This tells `dig` to use `127.0.0.1` as the resolver on port `19001`. The `+short verbose. Finally, the record we're looking up is `my-test-a.ok`. You can see the returned output of `1.1.1.1` matches the record data we entered. -## Other things to note +### Other things to note 1. Upon connecting to a zone for the first time, a zone sync is executed to provide VinylDNS a copy of the records in the zone diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md index 8654c7526..1eedc2bdd 100644 --- a/SYSTEM_DESIGN.md +++ b/SYSTEM_DESIGN.md @@ -1,56 +1,57 @@ # System Design ## Table of Contents + - [Components](#components) - [Process Flow](#process-flow) - [Integration](#integration) ## Components -The following diagram illustrates the major components in the VinylDNS ecosystem and the external systems they interact with. +The following diagram illustrates the major components in the VinylDNS ecosystem and the external systems they interact +with. -![VinylDNS Architecture Diagram](img/VinylDNS_overview.png) +![VinylDNS Architecture Diagram](img/vinyldns_overview.png) -* API - RESTful endpoints to allow interaction with VinylDNS - -* Database - stores information that the VinylDNS application needs - -* DNS servers - communicates DNS changes and resolves DNS records - -* Message queue - temporarily stores DNS requests for processing - -* LDAP Service - application protocol used to authenticate user access to the VinylDNS portal - -* Portal - graphical user interface to interact with the VinylDNS API - -* Tooling - external libraries and utilities used to interact with the VinylDNS API +| Component | Description | +|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Portal | Web user interface to interact with the VinylDNS API | +| API | RESTful endpoints to allow interaction with VinylDNS | +| API Worker Nodes | These are API components with `processing-disabled` set to `false` (see [documentation](https://www.vinyldns.io/operator/config-api.html#processing-disabled)) | +| Message queue | Queue for DNS commands to enable flow control to the DNS backends (see [documentation](https://www.vinyldns.io/operator/pre.html#message-queues)) | +| Database | Stores information about users, membership, and DNS records | +| DNS Backend(s) | The DNS backend servers which VinylDNS will query and update. | +| LDAP Service | The optional LDAP service that VinylDNS can be configured to communicate with (see [documentation](https://www.vinyldns.io/operator/setup-ldap.html#setup-ldap)) | ## Process Flow 1. LDAP service authenticates user credentials and grants access to the portal. 1. If the user is accessing the portal for the first time, VinylDNS credentials are generated and stored. 1. User navigates portal or uses integration tooling to generate a signed API request. -1. When the API receives a request, it loads the credentials for the calling user from the database and validates the request signature to ensure that the request was not modified in transit. +1. When the API receives a request, it loads the credentials for the calling user from the database and validates the + request signature to ensure that the request was not modified in transit. 1. The request is then validated to ensure that: - - the request data is correct + - the request data is correct - the request passes all validation checks - the user has access to make the change 1. Assuming the request is in good order, the request is put on a message queue for handling. -1. One of the VinylDNS API server instances pulls the message from the queue for processing. For record changes, a DDNS message is issued to the DNS backend server. -1. When the message completes processing, it is removed from the message queue. The changes are applied to the VinylDNS database along with an audit record for the request. +1. One of the VinylDNS API server instances pulls the message from the queue for processing. For record changes, a DDNS + message is issued to the DNS backend server. +1. When the message completes processing, it is removed from the message queue. The changes are applied to the VinylDNS + database along with an audit record for the request. ## Integration -Integrating with VinylDNS is simple since each API endpoint is effectively a distinct DNS operation (eg. create record, update record, delete record, etc.). The only requirement for sending a request is generating the correct AWS SIG4 signature without content length and providing the corresponding HTTP headers so that VinylDNS can verify it. See [API Authentication](https://www.vinyldns.io/api/auth-mechanism.html) for more details. +Integrating with VinylDNS is simple since each API endpoint is effectively a distinct DNS operation (eg. create record, +update record, delete record, etc.). The only requirement for sending a request is generating the correct AWS SIG4 +signature without content length and providing the corresponding HTTP headers so that VinylDNS can verify it. +See [API Authentication](https://www.vinyldns.io/api/auth-mechanism.html) for more details. The current tooling available to perform VinylDNS API requests include: * [go-vinyldns](https://github.com/vinyldns/go-vinyldns) - Golang client package - -* [vinyldns-cli](https://github.com/vinyldns/vinyldns-cli) - command line utility written in Golang - +* [terraform-provider-vinyldns](https://github.com/vinyldns/terraform-provider-vinyldns) - A [Terraform](https://terraform.io/) provider for VinylDNS +* [vinyldns-cli](https://github.com/vinyldns/vinyldns-cli) - Command line utility written in Golang * [vinyldns-java](https://github.com/vinyldns/vinyldns-java) - Java client - +* [vinyldns-js](https://github.com/vinyldns/vinyldns-js) - JavaScript client * [vinyldns-python](https://github.com/vinyldns/vinyldns-python) - Python client library - -* [vinyldns-ruby](https://github.com/vinyldns/vinyldns-ruby) - Ruby gem diff --git a/build.sbt b/build.sbt index a1af2654b..006c919a6 100644 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,7 @@ import org.scalafmt.sbt.ScalafmtPlugin._ import scoverage.ScoverageKeys.{coverageFailOnMinimum, coverageMinimum} import scala.language.postfixOps +import scala.sys.env import scala.util.Try lazy val IntegrationTest = config("it").extend(Test) @@ -16,9 +17,9 @@ lazy val sharedSettings = Seq( organizationName := "Comcast Cable Communications Management, LLC", startYear := Some(2018), licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")), - maintainer := "VinylDNS Maintainers ", + maintainer := "VinylDNS Maintainers ", scalacOptions ++= scalacOptionsByV(scalaVersion.value), - scalacOptions in (Compile, doc) += "-no-link-warnings", + scalacOptions in(Compile, doc) += "-no-link-warnings", // Use wart remover to eliminate code badness wartremoverErrors := ( if (getPropertyFlagOrDefault("build.lintOnCompile", true)) @@ -31,13 +32,13 @@ lazy val sharedSettings = Seq( Wart.ExplicitImplicitTypes ) else Seq.empty - ), + ), // scala format scalafmtOnCompile := getPropertyFlagOrDefault("build.scalafmtOnCompile", false), // coverage options coverageMinimum := 85, coverageFailOnMinimum := true, - coverageHighlighting := true + coverageHighlighting := true, ) lazy val testSettings = Seq( @@ -287,7 +288,6 @@ lazy val docs = (project in file("modules/docs")) .settings(docSettings) - def getPropertyFlagOrDefault(name: String, value: Boolean): Boolean = sys.props.get(name).flatMap(propValue => Try(propValue.toBoolean).toOption).getOrElse(value) diff --git a/build/docker/.env b/build/docker/.env index 4bb852ad4..f5bcadd7d 100644 --- a/build/docker/.env +++ b/build/docker/.env @@ -7,7 +7,7 @@ TEST_LOGIN=false # API Settings REST_PORT=9000 -SQS_ENDPOINT=http://vinyldns-integration:19003 +SQS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 SNS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 MYSQL_ENDPOINT=vinyldns-integration:19002 DEFAULT_DNS_ADDRESS=vinyldns-integration:19001 diff --git a/build/docker/api/application.conf b/build/docker/api/application.conf index b1f9eb837..5857c4599 100644 --- a/build/docker/api/application.conf +++ b/build/docker/api/application.conf @@ -49,7 +49,7 @@ vinyldns { key-name = ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" key = ${?DEFAULT_DNS_KEY_SECRET} - primary-server = "127.0.0.1" + primary-server = "127.0.0.1:19001" primary-server = ${?DEFAULT_DNS_ADDRESS} } transfer-connection = { @@ -58,7 +58,7 @@ vinyldns { key-name = ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" key = ${?DEFAULT_DNS_KEY_SECRET} - primary-server = "127.0.0.1" + primary-server = "127.0.0.1:19001" primary-server = ${?DEFAULT_DNS_ADDRESS} }, tsig-usage = "always" @@ -71,7 +71,7 @@ vinyldns { key-name = ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" key = ${?DEFAULT_DNS_KEY_SECRET} - primary-server = "127.0.0.1" + primary-server = "127.0.0.1:19001" primary-server = ${?DEFAULT_DNS_ADDRESS} } transfer-connection = { @@ -80,7 +80,7 @@ vinyldns { key-name = ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" key = ${?DEFAULT_DNS_KEY_SECRET} - primary-server = "127.0.0.1" + primary-server = "127.0.0.1:19001" primary-server = ${?DEFAULT_DNS_ADDRESS} }, tsig-usage = "always" diff --git a/build/sbt.sh b/build/sbt.sh index 730b36b61..e02750d64 100755 --- a/build/sbt.sh +++ b/build/sbt.sh @@ -4,4 +4,4 @@ set -euo pipefail DIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) cd "$DIR/../test/api/integration" -make build DOCKER_PARAMS="--build-arg SKIP_API_BUILD=true" && make run-local WITH_ARGS="sbt" DOCKER_PARAMS="-e RUN_SERVICES=none" +make build DOCKER_PARAMS="--build-arg SKIP_API_BUILD=true" && make run-local WITH_ARGS="sbt" DOCKER_PARAMS="-e RUN_SERVICES=none --env-file \"$DIR/../test/api/integration/.env.integration\"" diff --git a/img/VinylDNS_overview.png b/img/VinylDNS_overview.png deleted file mode 100644 index b673b077860ede51003709a0d38066aebd703451..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56261 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 VXnyqT6TASWc|}LHK-n_ne*ngML(KpH literal 0 HcmV?d00001 diff --git a/modules/api/src/it/resources/application.conf b/modules/api/src/it/resources/application.conf index df6975a45..5857c4599 100644 --- a/modules/api/src/it/resources/application.conf +++ b/modules/api/src/it/resources/application.conf @@ -1,63 +1,268 @@ -akka { - loglevel = "OFF" - log-dead-letters-during-shutdown = off - log-dead-letters = 0 - logger-startup-timeout = 60s -} - vinyldns { + base-version = "0.0.0-local-dev" + version = ${vinyldns.base-version} # default to the base version if not overridden + version = ${?VINYLDNS_VERSION} # override the base version via env var + + # How often to any particular zone can be synchronized in milliseconds + sync-delay = 10000 + sync-delay = ${?SYNC_DELAY} + + # If we should start up polling for change requests, set this to false for the inactive cluster + processing-disabled = false + processing-disabled = ${?PROCESSING_DISABLED} + + # Number of records that can be in a zone + max-zone-size = 60000 + max-zone-size = ${?MAX_ZONE_SIZE} + + # Types of unowned records that users can access in shared zones + shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] + + # Batch change settings + batch-change-limit = 1000 + batch-change-limit = ${?BATCH_CHANGE_LIMIT} + manual-batch-review-enabled = true + manual-batch-review-enabled = ${?MANUAL_BATCH_REVIEW_ENABLED} + scheduled-changes-enabled = true + scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED} + multi-record-batch-change-enabled = true + multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED} + + # configured backend providers + backend { + # Use "default" when dns backend legacy = true + # otherwise, use the id of one of the connections in any of your backends + default-backend-id = "default" + + # this is where we can save additional backends + backend-providers = [ + { + class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader" + settings = { + legacy = false + backends = [ + { + id = "default" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + }, + { + id = "func-test-backend" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + } + ] + } + } + ] + } + + + queue { + class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + + messages-per-poll = 10 + polling-interval = 250.millis + + settings { + # AWS access key and secret. + access-key = "test" + access-key = ${?AWS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?AWS_SECRET_ACCESS_KEY} + + # Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed. + signing-region = "us-east-1" + signing-region = ${?SQS_REGION} + + # Endpoint to access queue + service-endpoint = "http://vinyldns-integration:19003/" + service-endpoint = ${?SQS_SERVICE_ENDPOINT} + + # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. + queue-name = "vinyldns" + queue-name = ${?SQS_QUEUE_NAME} + } + } + + email { + class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" + class-name = ${?EMAIL_CLASS_NAME} + settings = { + from = "VinylDNS " + from = ${?EMAIL_FROM} + } + } + + sns { + class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" + class-name = ${?SNS_CLASS_NAME} + settings { + topic-arn = "arn:aws:sns:us-east-1:000000000000:batchChanges" + topic-arn = ${?SNS_TOPIC_ARN} + access-key = "test" + access-key = ${?SNS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?SNS_SECRET_KEY} + service-endpoint = "http://vinyldns-integration:19003" + service-endpoint = ${?SNS_SERVICE_ENDPOINT} + signing-region = "us-east-1" + signing-region = ${?SNS_REGION} + } + } + + rest { + host = "0.0.0.0" + port = 9000 + port=${?API_SERVICE_PORT} + } + + approved-name-servers = [ - "172.17.42.1." + "172.17.42.1.", + "ns1.parent.com." + "ns1.parent.com1." + "ns1.parent.com2." + "ns1.parent.com3." + "ns1.parent.com4." ] + # Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production + crypto { + type = "vinyldns.core.crypto.NoOpCrypto" + type = ${?CRYPTO_TYPE} + secret = ${?CRYPTO_SECRET} + } + + data-stores = ["mysql"] mysql { settings { - # see https://github.com/brettwooldridge/HikariCP - connection-timeout-millis = 1000 - idle-timeout = 10000 - max-lifetime = 600000 - maximum-pool-size = 5 - minimum-idle = 1 - my-sql-properties = { - cachePrepStmts=true - prepStmtCacheSize=250 - prepStmtCacheSqlLimit=2048 - rewriteBatchedStatements=true - } - + # JDBC Settings, these are all values in scalikejdbc-config, not our own + # these must be overridden to use MYSQL for production use + # assumes a docker or mysql instance running locally + name = "vinyldns" + name = ${?DATABASE_NAME} + driver = "org.h2.Driver" + driver = ${?JDBC_DRIVER} + migration-url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + migration-url = ${?JDBC_MIGRATION_URL} + url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + url = ${?JDBC_URL} + user = "sa" + user = ${?JDBC_USER} + password = "" + password = ${?JDBC_PASSWORD} } + # TODO: Remove the need for these useless configuration blocks repositories { zone { - # no additional settings for now } batch-change { - # no additional settings for now } user { - } record-set { - - } - group { - - } - membership { - - } - group-change { - } zone-change { - } record-change { - + } + group { + } + group-change { + } + membership { } } } + backends = [] + + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.2.252", + "192.0.2.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0" + ] + } + + # FQDNs / IPs / zone names that require manual review upon submission in batch change interface + # domain-list used for all record types except PTR + # ip-list used exclusively for PTR records + manual-review-domains = { + domain-list = [ + "needs-review.*" + ] + ip-list = [ + "192.0.1.254", + "192.0.1.255", + "192.0.2.254", + "192.0.2.255", + "192.0.3.254", + "192.0.3.255", + "192.0.4.254", + "192.0.4.255", + "fd69:27cc:fe91:0:0:0:ffff:1", + "fd69:27cc:fe91:0:0:0:ffff:2", + "fd69:27cc:fe92:0:0:0:ffff:1", + "fd69:27cc:fe92:0:0:0:ffff:2", + "fd69:27cc:fe93:0:0:0:ffff:1", + "fd69:27cc:fe93:0:0:0:ffff:2", + "fd69:27cc:fe94:0:0:0:ffff:1", + "fd69:27cc:fe94:0:0:0:ffff:2" + ] + zone-name-list = [ + "zone.requires.review." + "zone.requires.review1." + "zone.requires.review2." + "zone.requires.review3." + "zone.requires.review4." + ] + } + # FQDNs / IPs that cannot be modified via VinylDNS # regex-list used for all record types except PTR # ip-list used exclusively for PTR records @@ -66,31 +271,65 @@ vinyldns { "high-value-domain.*" # for testing ] ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.1.252", + "192.0.1.253", "192.0.2.252", "192.0.2.253", + "192.0.3.252", + "192.0.3.253", + "192.0.4.252", + "192.0.4.253", "fd69:27cc:fe91:0:0:0:0:ffff", - "fd69:27cc:fe91:0:0:0:ffff:0" + "fd69:27cc:fe91:0:0:0:ffff:0", + "fd69:27cc:fe92:0:0:0:0:ffff", + "fd69:27cc:fe92:0:0:0:ffff:0", + "fd69:27cc:fe93:0:0:0:0:ffff", + "fd69:27cc:fe93:0:0:0:ffff:0", + "fd69:27cc:fe94:0:0:0:0:ffff", + "fd69:27cc:fe94:0:0:0:ffff:0" ] } - # types of unowned records that users can access in shared zones - shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] + global-acl-rules = [ + { + group-ids: ["global-acl-group-id"], + fqdn-regex-list: [".*shared[0-9]{1}."] + }, + { + group-ids: ["another-global-acl-group"], + fqdn-regex-list: [".*ok[0-9]{1}."] + } + ] +} - crypto { - type = "vinyldns.core.crypto.NoOpCrypto" - } +akka { + loglevel = "INFO" + loggers = ["akka.event.slf4j.Slf4jLogger"] + logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" + logger-startup-timeout = 30s - email.settings.smtp { - port = 19025 + actor { + provider = "akka.actor.LocalActorRefProvider" } } -# Global settings -scalikejdbc.global.loggingSQLAndTime.enabled=true -scalikejdbc.global.loggingSQLAndTime.logLevel=error -scalikejdbc.global.loggingSQLAndTime.warningEnabled=true -scalikejdbc.global.loggingSQLAndTime.warningThresholdMillis=1000 -scalikejdbc.global.loggingSQLAndTime.warningLogLevel=warn -scalikejdbc.global.loggingSQLAndTime.singleLineMode=false -scalikejdbc.global.loggingSQLAndTime.printUnprocessedStackTrace=false -scalikejdbc.global.loggingSQLAndTime.stackTraceDepth=10 +akka.http { + server { + # The time period within which the TCP binding process must be completed. + # Set to `infinite` to disable. + bind-timeout = 5s + + # Show verbose error messages back to the client + verbose-error-messages = on + } + + parsing { + # Spray doesn't like the AWS4 headers + illegal-header-warnings = on + } +} + +# You can provide configuration overrides via local.conf if you don't want to replace everything in +# this configuration file +include "local.conf" diff --git a/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala index 22e5fa22a..0fd274b79 100644 --- a/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala @@ -34,15 +34,16 @@ import vinyldns.core.domain.zone.{Algorithm, Zone, ZoneConnection} class DnsBackendIntegrationSpec extends AnyWordSpec with Matchers { private val testConnection = ZoneConnection( - "test", - "test", + "vinyldns.", + "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", - "127.0.0.1:19001", + sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"), Algorithm.HMAC_MD5 ) "DNSBackend" should { "connect to a zone without a tsig key for transfer or update" in { + val config = DnsBackendConfig( "test", diff --git a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala index c04795003..b757d5518 100644 --- a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala @@ -51,7 +51,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers { "vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", - "127.0.0.1:19001" + sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001") ) ), transferConnection = @@ -84,7 +84,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers { "vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", - "127.0.0.1:19001" + sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001") ) ), transferConnection = Some( @@ -92,7 +92,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers { "vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", - "127.0.0.1:19001" + sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001") ) ) ) diff --git a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala index 5677647e8..86fe39540 100644 --- a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala @@ -16,7 +16,7 @@ package vinyldns.api.notifier.sns -import cats.effect.IO +import cats.effect.{IO, Timer} import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials} import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration import com.amazonaws.services.sns.AmazonSNSClientBuilder @@ -26,6 +26,7 @@ import org.joda.time.DateTime import org.json4s.DefaultFormats import org.json4s.jackson.JsonMethods._ import org.scalatest.matchers.should.Matchers +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.scalatest.wordspec.AnyWordSpecLike import vinyldns.api.MySqlApiIntegrationSpec import vinyldns.core.TestMembershipData._ @@ -34,6 +35,8 @@ import vinyldns.core.domain.record.{AData, RecordType} import vinyldns.core.notifier._ import vinyldns.mysql.MySqlIntegrationSpec +import scala.concurrent.ExecutionContext + class SnsNotifierIntegrationSpec extends MySqlApiIntegrationSpec with MySqlIntegrationSpec @@ -43,7 +46,7 @@ class SnsNotifierIntegrationSpec import vinyldns.api.domain.DomainValidations._ implicit val formats = DefaultFormats - + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) val snsConfig: Config = ConfigFactory.load().getConfig("vinyldns.sns.settings") "Sns Notifier" should { @@ -91,7 +94,7 @@ class SnsNotifierIntegrationSpec val sqs = AmazonSQSClientBuilder .standard() .withEndpointConfiguration( - new EndpointConfiguration("http://127.0.0.1:19003", "us-east-1") + new EndpointConfiguration(sys.env.getOrElse("SNS_SERVICE_ENDPOINT","http://vinyldns-integration:19003"), "us-east-1") ) .withCredentials(credentialsProvider) .build() @@ -103,7 +106,7 @@ class SnsNotifierIntegrationSpec notifier <- new SnsNotifierProvider() .load(NotifierConfig("", snsConfig), userRepository) _ <- notifier.notify(Notification(batchChange)) - _ <- IO { Thread.sleep(100) } + _ <- IO.sleep(1.seconds) messages <- IO { sqs.receiveMessage(queueUrl).getMessages } _ <- IO { sns.deleteTopic(topic) diff --git a/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala index 11d2d166f..d5ca3daf3 100644 --- a/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala @@ -39,7 +39,7 @@ import scala.collection.JavaConverters._ import scala.util.matching.Regex class Route53ApiIntegrationSpec - extends AnyWordSpec + extends AnyWordSpec with ScalaFutures with Matchers with MockitoSugar @@ -49,6 +49,7 @@ class Route53ApiIntegrationSpec with MySqlApiIntegrationSpec { private val testZone = Zone("example.com.", "test@test.com", backendId = Some("test")) + private def testConnection: Route53Backend = Route53Backend .load( @@ -56,7 +57,7 @@ class Route53ApiIntegrationSpec "test", Some("access"), Some("secret"), - "http://127.0.0.1:19003", + sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"), "us-east-1" ) ) diff --git a/modules/api/src/main/resources/application.conf b/modules/api/src/main/resources/application.conf index 433095214..5857c4599 100644 --- a/modules/api/src/main/resources/application.conf +++ b/modules/api/src/main/resources/application.conf @@ -1,15 +1,317 @@ -################################################################################################################ -# This configuration is used primarily when running re-start or starting Vinyll locally. The configuration -# presumes a stand-alone Vinyll server with no backend services. -################################################################################################################ -akka { - loglevel = "ERROR" +vinyldns { + base-version = "0.0.0-local-dev" + version = ${vinyldns.base-version} # default to the base version if not overridden + version = ${?VINYLDNS_VERSION} # override the base version via env var - # The following settings are required to have Akka logging output to SLF4J and logback; without - # these, akka will output to STDOUT + # How often to any particular zone can be synchronized in milliseconds + sync-delay = 10000 + sync-delay = ${?SYNC_DELAY} + + # If we should start up polling for change requests, set this to false for the inactive cluster + processing-disabled = false + processing-disabled = ${?PROCESSING_DISABLED} + + # Number of records that can be in a zone + max-zone-size = 60000 + max-zone-size = ${?MAX_ZONE_SIZE} + + # Types of unowned records that users can access in shared zones + shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] + + # Batch change settings + batch-change-limit = 1000 + batch-change-limit = ${?BATCH_CHANGE_LIMIT} + manual-batch-review-enabled = true + manual-batch-review-enabled = ${?MANUAL_BATCH_REVIEW_ENABLED} + scheduled-changes-enabled = true + scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED} + multi-record-batch-change-enabled = true + multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED} + + # configured backend providers + backend { + # Use "default" when dns backend legacy = true + # otherwise, use the id of one of the connections in any of your backends + default-backend-id = "default" + + # this is where we can save additional backends + backend-providers = [ + { + class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader" + settings = { + legacy = false + backends = [ + { + id = "default" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + }, + { + id = "func-test-backend" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + } + ] + } + } + ] + } + + + queue { + class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + + messages-per-poll = 10 + polling-interval = 250.millis + + settings { + # AWS access key and secret. + access-key = "test" + access-key = ${?AWS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?AWS_SECRET_ACCESS_KEY} + + # Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed. + signing-region = "us-east-1" + signing-region = ${?SQS_REGION} + + # Endpoint to access queue + service-endpoint = "http://vinyldns-integration:19003/" + service-endpoint = ${?SQS_SERVICE_ENDPOINT} + + # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. + queue-name = "vinyldns" + queue-name = ${?SQS_QUEUE_NAME} + } + } + + email { + class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" + class-name = ${?EMAIL_CLASS_NAME} + settings = { + from = "VinylDNS " + from = ${?EMAIL_FROM} + } + } + + sns { + class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" + class-name = ${?SNS_CLASS_NAME} + settings { + topic-arn = "arn:aws:sns:us-east-1:000000000000:batchChanges" + topic-arn = ${?SNS_TOPIC_ARN} + access-key = "test" + access-key = ${?SNS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?SNS_SECRET_KEY} + service-endpoint = "http://vinyldns-integration:19003" + service-endpoint = ${?SNS_SERVICE_ENDPOINT} + signing-region = "us-east-1" + signing-region = ${?SNS_REGION} + } + } + + rest { + host = "0.0.0.0" + port = 9000 + port=${?API_SERVICE_PORT} + } + + + approved-name-servers = [ + "172.17.42.1.", + "ns1.parent.com." + "ns1.parent.com1." + "ns1.parent.com2." + "ns1.parent.com3." + "ns1.parent.com4." + ] + + # Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production + crypto { + type = "vinyldns.core.crypto.NoOpCrypto" + type = ${?CRYPTO_TYPE} + secret = ${?CRYPTO_SECRET} + } + + data-stores = ["mysql"] + mysql { + settings { + # JDBC Settings, these are all values in scalikejdbc-config, not our own + # these must be overridden to use MYSQL for production use + # assumes a docker or mysql instance running locally + name = "vinyldns" + name = ${?DATABASE_NAME} + driver = "org.h2.Driver" + driver = ${?JDBC_DRIVER} + migration-url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + migration-url = ${?JDBC_MIGRATION_URL} + url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + url = ${?JDBC_URL} + user = "sa" + user = ${?JDBC_USER} + password = "" + password = ${?JDBC_PASSWORD} + } + + # TODO: Remove the need for these useless configuration blocks + repositories { + zone { + } + batch-change { + } + user { + } + record-set { + } + zone-change { + } + record-change { + } + group { + } + group-change { + } + membership { + } + } + } + + backends = [] + + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.2.252", + "192.0.2.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0" + ] + } + + # FQDNs / IPs / zone names that require manual review upon submission in batch change interface + # domain-list used for all record types except PTR + # ip-list used exclusively for PTR records + manual-review-domains = { + domain-list = [ + "needs-review.*" + ] + ip-list = [ + "192.0.1.254", + "192.0.1.255", + "192.0.2.254", + "192.0.2.255", + "192.0.3.254", + "192.0.3.255", + "192.0.4.254", + "192.0.4.255", + "fd69:27cc:fe91:0:0:0:ffff:1", + "fd69:27cc:fe91:0:0:0:ffff:2", + "fd69:27cc:fe92:0:0:0:ffff:1", + "fd69:27cc:fe92:0:0:0:ffff:2", + "fd69:27cc:fe93:0:0:0:ffff:1", + "fd69:27cc:fe93:0:0:0:ffff:2", + "fd69:27cc:fe94:0:0:0:ffff:1", + "fd69:27cc:fe94:0:0:0:ffff:2" + ] + zone-name-list = [ + "zone.requires.review." + "zone.requires.review1." + "zone.requires.review2." + "zone.requires.review3." + "zone.requires.review4." + ] + } + + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.1.252", + "192.0.1.253", + "192.0.2.252", + "192.0.2.253", + "192.0.3.252", + "192.0.3.253", + "192.0.4.252", + "192.0.4.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0", + "fd69:27cc:fe92:0:0:0:0:ffff", + "fd69:27cc:fe92:0:0:0:ffff:0", + "fd69:27cc:fe93:0:0:0:0:ffff", + "fd69:27cc:fe93:0:0:0:ffff:0", + "fd69:27cc:fe94:0:0:0:0:ffff", + "fd69:27cc:fe94:0:0:0:ffff:0" + ] + } + + global-acl-rules = [ + { + group-ids: ["global-acl-group-id"], + fqdn-regex-list: [".*shared[0-9]{1}."] + }, + { + group-ids: ["another-global-acl-group"], + fqdn-regex-list: [".*ok[0-9]{1}."] + } + ] +} + +akka { + loglevel = "INFO" loggers = ["akka.event.slf4j.Slf4jLogger"] logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" logger-startup-timeout = 30s + + actor { + provider = "akka.actor.LocalActorRefProvider" + } } akka.http { @@ -28,143 +330,6 @@ akka.http { } } -vinyldns { - queue { - class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - - messages-per-poll = 10 - polling-interval = 250.millis - max-retries = 100 # Max retries for message on queue; currently only applies to MySqlMessageQueue - - settings { - # AWS access key and secret. - access-key = "x" - secret-key = "x" - } - } - data-stores = ["mysql"] - - mysql { - settings { - # see https://github.com/brettwooldridge/HikariCP - connection-timeout-millis = 1000 - idle-timeout = 10000 - max-lifetime = 600000 - maximum-pool-size = 5 - minimum-idle = 1 - register-mbeans = true - } - - repositories { - zone { - # no additional settings for now - } - batch-change { - # no additional settings for now - } - user { - - } - record-set { - - } - group { - - } - membership { - - } - group-change { - - } - zone-change { - - } - record-change { - - } - } - } - - sync-delay = 10000 # 10 second delay for resyncing zone - - batch-change-limit = 1000 # Max change limit per batch request - - # this key is used in order to encrypt/decrypt DNS TSIG keys. We use this dummy one for test purposes, this - # should be overridden with a real value that is hidden for production deployment - crypto { - type = "vinyldns.core.crypto.JavaCrypto" - secret = "8B06A7F3BC8A2497736F1916A123AA40E88217BE9264D8872597EF7A6E5DCE61" - } - - # FQDNs / IPs that cannot be modified via VinylDNS - # regex-list used for all record types except PTR - # ip-list used exclusively for PTR records - high-value-domains = { - regex-list = [ - "high-value-domain.*" # for testing - ] - ip-list = [ - "192.0.2.252", - "192.0.2.253", - "fd69:27cc:fe91:0:0:0:0:ffff", - "fd69:27cc:fe91:0:0:0:ffff:0" - ] - } - - # FQDNs / IPs / zone names that require manual review upon submission in batch change interface - # domain-list used for all record types except PTR - # ip-list used exclusively for PTR records - manual-review-domains = { - domain-list = [ - "needs-review.*" - ] - ip-list = [ - "192.0.2.254", - "192.0.2.255", - "fd69:27cc:fe91:0:0:0:ffff:1", - "fd69:27cc:fe91:0:0:0:ffff:2" - ] - zone-name-list = [ - "zone.requires.review." - ] - } - - # types of unowned records that users can access in shared zones - shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] - - backends = [ - { - id = "func-test-backend" - zone-connection { - name = "vinyldns." - key-name = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primary-server = "127.0.0.1:19001" - } - transfer-connection { - name = "vinyldns." - key-name = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primary-server = "127.0.0.1:19001" - } - } - ] - - multi-record-batch-change-enabled = true - - # feature flag for manual batch review - manual-batch-review-enabled = true - scheduled-changes-enabled = true - - global-acl-rules = [ - { - group-ids: ["global-acl-group-id"], - fqdn-regex-list: [".*shared."] - }, - { - group-ids: ["another-global-acl-group"], - fqdn-regex-list: [".*ok."] - } - ] -} +# You can provide configuration overrides via local.conf if you don't want to replace everything in +# this configuration file +include "local.conf" diff --git a/modules/api/src/main/resources/reference.conf b/modules/api/src/main/resources/reference.conf index e7ced14b2..fdbb0fb00 100644 --- a/modules/api/src/main/resources/reference.conf +++ b/modules/api/src/main/resources/reference.conf @@ -148,16 +148,24 @@ vinyldns { defaultZoneConnection { name = "vinyldns." + name= ${?DEFAULT_DNS_KEY_NAME} keyName = "vinyldns." + keyName= ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} primaryServer = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} } defaultTransferConnection { name = "vinyldns." + name= ${?DEFAULT_DNS_KEY_NAME} keyName = "vinyldns." + keyName= ${?DEFAULT_DNS_KEY_NAME} key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} primaryServer = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} } batch-change-limit = 1000 diff --git a/modules/api/src/main/resources/vinyldns-ascii.txt b/modules/api/src/main/resources/vinyldns-ascii.txt index bb10a843a..b84f40a76 100644 --- a/modules/api/src/main/resources/vinyldns-ascii.txt +++ b/modules/api/src/main/resources/vinyldns-ascii.txt @@ -1,6 +1,6 @@ - .__ .__ .___ -___ _|__| ____ ___.__.| | __| _/____ ______ -\ \/ / |/ < | || | / __ |/ \ / ___/ - \ /| | | \___ || |__/ /_/ | | \\___ \ - \_/ |__|___| / ____||____/\____ |___| /____ > - \/\/ \/ \/ \/ + _ ___ ______ _ _______ +| | / (_)___ __ __/ / __ \/ | / / ___/ +| | / / / __ \/ / / / / / / / |/ /\__ \ +| |/ / / / / / /_/ / / /_/ / /| /___/ / +|___/_/_/ /_/\__, /_/_____/_/ |_//____/ + /____/ diff --git a/modules/api/src/test/resources/application.conf b/modules/api/src/test/resources/application.conf index b1bd68c8d..18faa24ec 100644 --- a/modules/api/src/test/resources/application.conf +++ b/modules/api/src/test/resources/application.conf @@ -1,30 +1,227 @@ -akka { - loglevel = "OFF" - loggers = ["akka.testkit.TestEventListener"] - log-dead-letters-during-shutdown = off - log-dead-letters = 0 - test.timefactor = 5 - logger-startup-timeout = 30s -} - vinyldns { - active-node-count = 3 - sync-delay = 10000 # 10 second delay for resyncing zone - color = "blue" + base-version = "0.0.0-local-dev" + version = ${vinyldns.base-version} # default to the base version if not overridden + version = ${?VINYLDNS_VERSION} # override the base version via env var + + # How often to any particular zone can be synchronized in milliseconds + sync-delay = 10000 + sync-delay = ${?SYNC_DELAY} + + # If we should start up polling for change requests, set this to false for the inactive cluster + processing-disabled = false + processing-disabled = ${?PROCESSING_DISABLED} + + # Number of records that can be in a zone + max-zone-size = 60000 + max-zone-size = ${?MAX_ZONE_SIZE} + + # Types of unowned records that users can access in shared zones + shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] + + # Batch change settings + batch-change-limit = 1000 + batch-change-limit = ${?BATCH_CHANGE_LIMIT} + manual-batch-review-enabled = true + manual-batch-review-enabled = ${?MANUAL_BATCH_REVIEW_ENABLED} + scheduled-changes-enabled = true + scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED} + multi-record-batch-change-enabled = true + multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED} + + # configured backend providers + backend { + # Use "default" when dns backend legacy = true + # otherwise, use the id of one of the connections in any of your backends + default-backend-id = "default" + + # this is where we can save additional backends + backend-providers = [ + { + class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader" + settings = { + legacy = false + backends = [ + { + id = "default" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + }, + { + id = "func-test-backend" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + } + ] + } + } + ] + } + + + queue { + class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + + messages-per-poll = 10 + polling-interval = 250.millis + + settings { + # AWS access key and secret. + access-key = "test" + access-key = ${?AWS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?AWS_SECRET_ACCESS_KEY} + + # Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed. + signing-region = "us-east-1" + signing-region = ${?SQS_REGION} + + # Endpoint to access queue + service-endpoint = "http://vinyldns-integration:19003/" + service-endpoint = ${?SQS_SERVICE_ENDPOINT} + + # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. + queue-name = "vinyldns" + queue-name = ${?SQS_QUEUE_NAME} + } + } + + email { + class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" + class-name = ${?EMAIL_CLASS_NAME} + settings = { + from = "VinylDNS " + from = ${?EMAIL_FROM} + } + } + + sns { + class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" + class-name = ${?SNS_CLASS_NAME} + settings { + topic-arn = "arn:aws:sns:us-east-1:000000000000:batchChanges" + topic-arn = ${?SNS_TOPIC_ARN} + access-key = "test" + access-key = ${?SNS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?SNS_SECRET_KEY} + service-endpoint = "http://vinyldns-integration:19003" + service-endpoint = ${?SNS_SERVICE_ENDPOINT} + signing-region = "us-east-1" + signing-region = ${?SNS_REGION} + } + } + + notifiers = ["test-notifier"] + + test-notifier { + class-name = "someclass" + settings { + value = "test" + } + } + rest { - host = "127.0.0.1" + host = "0.0.0.0" port = 9000 + port=${?API_SERVICE_PORT} } - # this key is used in order to encrypt/decrypt DNS TSIG keys. We use this dummy one for test purposes, this - # should be overridden with a real value that is hidden for production deployment - crypto { - type = "vinyldns.core.crypto.JavaCrypto" - secret = "8B06A7F3BC8A2497736F1916A123AA40E88217BE9264D8872597EF7A6E5DCE61" - } + + approved-name-servers = [ - "some.test.ns." + "172.17.42.1.", + "ns1.parent.com." + "ns1.parent.com1." + "ns1.parent.com2." + "ns1.parent.com3." + "ns1.parent.com4." ] + # Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production + crypto { + type = "vinyldns.core.crypto.NoOpCrypto" + type = ${?CRYPTO_TYPE} + secret = ${?CRYPTO_SECRET} + } + + data-stores = ["mysql"] + mysql { + settings { + # JDBC Settings, these are all values in scalikejdbc-config, not our own + # these must be overridden to use MYSQL for production use + # assumes a docker or mysql instance running locally + name = "vinyldns" + name = ${?DATABASE_NAME} + driver = "org.h2.Driver" + driver = ${?JDBC_DRIVER} + migration-url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + migration-url = ${?JDBC_MIGRATION_URL} + url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + url = ${?JDBC_URL} + user = "sa" + user = ${?JDBC_USER} + password = "" + password = ${?JDBC_PASSWORD} + } + + # TODO: Remove the need for these useless configuration blocks + repositories { + zone { + } + batch-change { + } + user { + } + record-set { + } + zone-change { + } + record-change { + } + group { + } + group-change { + } + membership { + } + } + } + + backends = [] + # FQDNs / IPs that cannot be modified via VinylDNS # regex-list used for all record types except PTR # ip-list used exclusively for PTR records @@ -33,6 +230,7 @@ vinyldns { "high-value-domain.*" # for testing ] ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing "192.0.2.252", "192.0.2.253", "fd69:27cc:fe91:0:0:0:0:ffff", @@ -48,80 +246,99 @@ vinyldns { "needs-review.*" ] ip-list = [ + "192.0.1.254", + "192.0.1.255", "192.0.2.254", "192.0.2.255", + "192.0.3.254", + "192.0.3.255", + "192.0.4.254", + "192.0.4.255", "fd69:27cc:fe91:0:0:0:ffff:1", - "fd69:27cc:fe91:0:0:0:ffff:2" + "fd69:27cc:fe91:0:0:0:ffff:2", + "fd69:27cc:fe92:0:0:0:ffff:1", + "fd69:27cc:fe92:0:0:0:ffff:2", + "fd69:27cc:fe93:0:0:0:ffff:1", + "fd69:27cc:fe93:0:0:0:ffff:2", + "fd69:27cc:fe94:0:0:0:ffff:1", + "fd69:27cc:fe94:0:0:0:ffff:2" ] zone-name-list = [ - "zone.needs.review." + "zone.requires.review." + "zone.requires.review1." + "zone.requires.review2." + "zone.requires.review3." + "zone.requires.review4." ] } - # types of unowned records that users can access in shared zones - shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] - - # used for testing only - string-list-test = ["test"] - - mysql.repositories { - zone { - # no additional settings for now - } - batch-change { - # no additional settings for now - } - user { - - } - record-set { - - } - group { - - } - membership { - - } - group-change { - - } - zone-change { - - } - record-change { - - } + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.1.252", + "192.0.1.253", + "192.0.2.252", + "192.0.2.253", + "192.0.3.252", + "192.0.3.253", + "192.0.4.252", + "192.0.4.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0", + "fd69:27cc:fe92:0:0:0:0:ffff", + "fd69:27cc:fe92:0:0:0:ffff:0", + "fd69:27cc:fe93:0:0:0:0:ffff", + "fd69:27cc:fe93:0:0:0:ffff:0", + "fd69:27cc:fe94:0:0:0:0:ffff", + "fd69:27cc:fe94:0:0:0:ffff:0" + ] } - notifiers = ["test-notifier"] - - test-notifier { - class-name = "someclass" - settings { - value = "test" - } - } - - backends = [ + global-acl-rules = [ { - id = "test" - zone-connection { - name = "zoneconn." - key-name = "vinyldns." - key = "test-key" - primary-server = "127.0.0.1:19001" - } - transfer-connection { - name = "transferconn." - key-name = "vinyldns." - key = "test-key" - primary-server = "127.0.0.1:19001" - } + group-ids: ["global-acl-group-id"], + fqdn-regex-list: [".*shared[0-9]{1}."] + }, + { + group-ids: ["another-global-acl-group"], + fqdn-regex-list: [".*ok[0-9]{1}."] } ] - - manual-batch-review-enabled = true - - scheduled-changes-enabled = true } + +akka { + loglevel = "INFO" + loggers = ["akka.event.slf4j.Slf4jLogger"] + logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" + logger-startup-timeout = 30s + + actor { + provider = "akka.actor.LocalActorRefProvider" + } +} + +akka.http { + server { + # The time period within which the TCP binding process must be completed. + # Set to `infinite` to disable. + bind-timeout = 5s + + # Show verbose error messages back to the client + verbose-error-messages = on + } + + parsing { + # Spray doesn't like the AWS4 headers + illegal-header-warnings = on + } +} + +# You can provide configuration overrides via local.conf if you don't want to replace everything in +# this configuration file +include "local.conf" diff --git a/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala b/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala index b278b637d..f61b43701 100644 --- a/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala @@ -16,15 +16,18 @@ package vinyldns.api.config +import cats.effect.{ContextShift, IO} import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import vinyldns.api.backend.dns.DnsBackendProviderConfig import vinyldns.core.domain.zone.ZoneConnection import vinyldns.core.repository.RepositoryName._ class VinylDNSConfigSpec extends AnyWordSpec with Matchers with BeforeAndAfterAll { private val underTest: VinylDNSConfig = VinylDNSConfig.load().unsafeRunSync() + private implicit val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) "VinylDNSConfig" should { "load the rest config" in { @@ -63,27 +66,19 @@ class VinylDNSConfigSpec extends AnyWordSpec with Matchers with BeforeAndAfterAl notifierConfigs.head.settings.getString("value").shouldBe("test") } - "load default keys" in { - val defaultConn = - ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "127.0.0.1:19001") - - underTest.configuredDnsConnections.defaultZoneConnection - .decrypted(underTest.crypto) shouldBe - defaultConn - underTest.configuredDnsConnections.defaultTransferConnection - .decrypted(underTest.crypto) shouldBe - defaultConn - } "load specified backends" in { - val zc = ZoneConnection("zoneconn.", "vinyldns.", "test-key", "127.0.0.1:19001") - val tc = zc.copy(name = "transferconn.") + val zc = ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")) + val tc = zc.copy() - val backends = underTest.configuredDnsConnections.dnsBackends + val backends = underTest.backendConfigs.backendProviders backends.length shouldBe 1 - backends.head.id shouldBe "test" - backends.head.zoneConnection.decrypted(underTest.crypto) shouldBe zc - backends.head.transferConnection.decrypted(underTest.crypto) shouldBe tc + val config = DnsBackendProviderConfig.load(backends.head.settings).unsafeRunSync() + config.backends.length shouldBe 2 + + config.backends.head.id shouldBe "default" + config.backends.head.zoneConnection.decrypted(underTest.crypto) shouldBe zc + config.backends.head.transferConnection.get.decrypted(underTest.crypto) shouldBe tc } } } diff --git a/modules/api/src/universal/conf/application.conf b/modules/api/src/universal/conf/application.conf index 6b90dbe92..5857c4599 100644 --- a/modules/api/src/universal/conf/application.conf +++ b/modules/api/src/universal/conf/application.conf @@ -1,124 +1,317 @@ -# The default application.conf is not intended to be used in production. It assumes a docker-compose -# setup for all of the services. Provide your own application.conf on the docker mount with your -# own settings vinyldns { - - # Should be provided in the start up script via an environment - base-version = "unset" + base-version = "0.0.0-local-dev" version = ${vinyldns.base-version} # default to the base version if not overridden version = ${?VINYLDNS_VERSION} # override the base version via env var - queue.settings.service-endpoint = "http://vinyldns-elasticmq:9324/" + # How often to any particular zone can be synchronized in milliseconds + sync-delay = 10000 + sync-delay = ${?SYNC_DELAY} + + # If we should start up polling for change requests, set this to false for the inactive cluster + processing-disabled = false + processing-disabled = ${?PROCESSING_DISABLED} + + # Number of records that can be in a zone + max-zone-size = 60000 + max-zone-size = ${?MAX_ZONE_SIZE} + + # Types of unowned records that users can access in shared zones + shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] + + # Batch change settings + batch-change-limit = 1000 + batch-change-limit = ${?BATCH_CHANGE_LIMIT} + manual-batch-review-enabled = true + manual-batch-review-enabled = ${?MANUAL_BATCH_REVIEW_ENABLED} + scheduled-changes-enabled = true + scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED} + multi-record-batch-change-enabled = true + multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED} + + # configured backend providers + backend { + # Use "default" when dns backend legacy = true + # otherwise, use the id of one of the connections in any of your backends + default-backend-id = "default" + + # this is where we can save additional backends + backend-providers = [ + { + class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader" + settings = { + legacy = false + backends = [ + { + id = "default" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + }, + { + id = "func-test-backend" + zone-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + } + transfer-connection = { + name = "vinyldns." + key-name = "vinyldns." + key-name = ${?DEFAULT_DNS_KEY_NAME} + key = "nzisn+4G2ldMn0q1CV3vsg==" + key = ${?DEFAULT_DNS_KEY_SECRET} + primary-server = "127.0.0.1:19001" + primary-server = ${?DEFAULT_DNS_ADDRESS} + }, + tsig-usage = "always" + } + ] + } + } + ] + } + + + queue { + class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + + messages-per-poll = 10 + polling-interval = 250.millis + + settings { + # AWS access key and secret. + access-key = "test" + access-key = ${?AWS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?AWS_SECRET_ACCESS_KEY} + + # Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed. + signing-region = "us-east-1" + signing-region = ${?SQS_REGION} + + # Endpoint to access queue + service-endpoint = "http://vinyldns-integration:19003/" + service-endpoint = ${?SQS_SERVICE_ENDPOINT} + + # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. + queue-name = "vinyldns" + queue-name = ${?SQS_QUEUE_NAME} + } + } + + email { + class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" + class-name = ${?EMAIL_CLASS_NAME} + settings = { + from = "VinylDNS " + from = ${?EMAIL_FROM} + } + } + + sns { + class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" + class-name = ${?SNS_CLASS_NAME} + settings { + topic-arn = "arn:aws:sns:us-east-1:000000000000:batchChanges" + topic-arn = ${?SNS_TOPIC_ARN} + access-key = "test" + access-key = ${?SNS_ACCESS_KEY} + secret-key = "test" + secret-key = ${?SNS_SECRET_KEY} + service-endpoint = "http://vinyldns-integration:19003" + service-endpoint = ${?SNS_SERVICE_ENDPOINT} + signing-region = "us-east-1" + signing-region = ${?SNS_REGION} + } + } - # host and port the server binds to. This should not be changed rest { host = "0.0.0.0" port = 9000 + port=${?API_SERVICE_PORT} } - # the delay between zone syncs so we are not syncing too often - sync-delay = 10000 - # crypto settings for symmetric cryptography of secrets in the system - # Note: for production systems secrets should not live in plain text in a file + approved-name-servers = [ + "172.17.42.1.", + "ns1.parent.com." + "ns1.parent.com1." + "ns1.parent.com2." + "ns1.parent.com3." + "ns1.parent.com4." + ] + + # Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production crypto { type = "vinyldns.core.crypto.NoOpCrypto" + type = ${?CRYPTO_TYPE} + secret = ${?CRYPTO_SECRET} } data-stores = ["mysql"] - - # default settings point to the setup from docker compose mysql { settings { + # JDBC Settings, these are all values in scalikejdbc-config, not our own + # these must be overridden to use MYSQL for production use + # assumes a docker or mysql instance running locally name = "vinyldns" - driver = "org.mariadb.jdbc.Driver" - migration-url = "jdbc:mariadb://vinyldns-mysql:3306/?user=root&password=pass" - url = "jdbc:mariadb://vinyldns-mysql:3306/vinyldns?user=root&password=pass" - user = "root" - password = "pass" - - # see https://github.com/brettwooldridge/HikariCP - connection-timeout-millis = 1000 - max-lifetime = 600000 - maximum-pool-size = 20 - register-mbeans = true + name = ${?DATABASE_NAME} + driver = "org.h2.Driver" + driver = ${?JDBC_DRIVER} + migration-url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + migration-url = ${?JDBC_MIGRATION_URL} + url = "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" + url = ${?JDBC_URL} + user = "sa" + user = ${?JDBC_USER} + password = "" + password = ${?JDBC_PASSWORD} } + + # TODO: Remove the need for these useless configuration blocks repositories { zone { - # no additional settings for now } batch-change { - # no additional settings for now } user { - # no additional settings for now } record-set { - # no additional settings for now - } - group { - # no additional settings for now - } - membership { - # no additional settings for now - } - group-change { - # no additional settings for now } zone-change { - # no additional settings for now } record-change { - # no additional settings for now + } + group { + } + group-change { + } + membership { } } } - # the DDNS connection information for the default dns backend - defaultZoneConnection { - name = "vinyldns." - keyName = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primaryServer = "localhost:19001" + backends = [] + + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.2.252", + "192.0.2.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0" + ] } - # the AXFR connection information for the default dns backend - defaultTransferConnection { - name = "vinyldns." - keyName = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primaryServer = "localhost:19001" + # FQDNs / IPs / zone names that require manual review upon submission in batch change interface + # domain-list used for all record types except PTR + # ip-list used exclusively for PTR records + manual-review-domains = { + domain-list = [ + "needs-review.*" + ] + ip-list = [ + "192.0.1.254", + "192.0.1.255", + "192.0.2.254", + "192.0.2.255", + "192.0.3.254", + "192.0.3.255", + "192.0.4.254", + "192.0.4.255", + "fd69:27cc:fe91:0:0:0:ffff:1", + "fd69:27cc:fe91:0:0:0:ffff:2", + "fd69:27cc:fe92:0:0:0:ffff:1", + "fd69:27cc:fe92:0:0:0:ffff:2", + "fd69:27cc:fe93:0:0:0:ffff:1", + "fd69:27cc:fe93:0:0:0:ffff:2", + "fd69:27cc:fe94:0:0:0:ffff:1", + "fd69:27cc:fe94:0:0:0:ffff:2" + ] + zone-name-list = [ + "zone.requires.review." + "zone.requires.review1." + "zone.requires.review2." + "zone.requires.review3." + "zone.requires.review4." + ] } - backends = [ + # FQDNs / IPs that cannot be modified via VinylDNS + # regex-list used for all record types except PTR + # ip-list used exclusively for PTR records + high-value-domains = { + regex-list = [ + "high-value-domain.*" # for testing + ] + ip-list = [ + # using reverse zones in the vinyldns/bind9 docker image for testing + "192.0.1.252", + "192.0.1.253", + "192.0.2.252", + "192.0.2.253", + "192.0.3.252", + "192.0.3.253", + "192.0.4.252", + "192.0.4.253", + "fd69:27cc:fe91:0:0:0:0:ffff", + "fd69:27cc:fe91:0:0:0:ffff:0", + "fd69:27cc:fe92:0:0:0:0:ffff", + "fd69:27cc:fe92:0:0:0:ffff:0", + "fd69:27cc:fe93:0:0:0:0:ffff", + "fd69:27cc:fe93:0:0:0:ffff:0", + "fd69:27cc:fe94:0:0:0:0:ffff", + "fd69:27cc:fe94:0:0:0:ffff:0" + ] + } + + global-acl-rules = [ { - id = "func-test-backend" - zone-connection { - name = "vinyldns." - key-name = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primary-server = "localhost:19001" - } - transfer-connection { - name = "vinyldns." - key-name = "vinyldns." - key = "nzisn+4G2ldMn0q1CV3vsg==" - primary-server = "localhost:19001" - } + group-ids: ["global-acl-group-id"], + fqdn-regex-list: [".*shared[0-9]{1}."] + }, + { + group-ids: ["another-global-acl-group"], + fqdn-regex-list: [".*ok[0-9]{1}."] } ] - - # the max number of changes in a single batch change. Change carefully as this has performance - # implications - batch-change-limit = 1000 } -# Akka settings, these should not need to be modified unless you know akka http really well. akka { loglevel = "INFO" loggers = ["akka.event.slf4j.Slf4jLogger"] logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" logger-startup-timeout = 30s + + actor { + provider = "akka.actor.LocalActorRefProvider" + } } akka.http { @@ -132,7 +325,11 @@ akka.http { } parsing { - # akka-http doesn't like the AWS4 headers + # Spray doesn't like the AWS4 headers illegal-header-warnings = on } } + +# You can provide configuration overrides via local.conf if you don't want to replace everything in +# this configuration file +include "local.conf" diff --git a/modules/docs/src/main/mdoc/operator/config-api.md b/modules/docs/src/main/mdoc/operator/config-api.md index d07a6821d..6e6c25349 100644 --- a/modules/docs/src/main/mdoc/operator/config-api.md +++ b/modules/docs/src/main/mdoc/operator/config-api.md @@ -52,7 +52,7 @@ snippet... access-key = ${AWS_ACCESS_KEY} secret-key = ${AWS_SECRET_ACCESS_KEY} signing-region = ${SQS_REGION} - service-endpoint = ${SQS_ENDPOINT} + service-endpoint = ${SQS_SERVICE_ENDPOINT} queue-name = ${SQS_QUEUE_NAME} } ``` diff --git a/modules/docs/src/main/mdoc/operator/config-portal.md b/modules/docs/src/main/mdoc/operator/config-portal.md index c61dc99d9..0d31f9a1e 100644 --- a/modules/docs/src/main/mdoc/operator/config-portal.md +++ b/modules/docs/src/main/mdoc/operator/config-portal.md @@ -251,7 +251,7 @@ portal.vinyldns.backend.url = "http://vinyldns-api:9000" portal.test_login = false # configuration for the users and groups store -data-stores = ["dynamodb", "mysql"] +data-stores = ["mysql"] mysql { class-name = "vinyldns.mysql.repository.MySqlDataStoreProvider" @@ -284,37 +284,6 @@ mysql { } } -dynamodb { - class-name = "vinyldns.dynamodb.repository.DynamoDBDataStoreProvider" - - settings { - key = "x" - secret = "x" - endpoint = "http://vinyldns-dynamodb:8000" - region = "us-east-1" - } - - repositories { - user-change { - table-name = "userChangeTest" - provisioned-reads = 30 - provisioned-writes = 20 - } - } - -LDAP { - user="test" - password="test" - domain="test" - - searchBase = [{organization = "someDomain", domainName = "DC=test,DC=test,DC=com"}, {organization = "anotherDomain", domainName = "DC=test,DC=com"}] - - context { - initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory" - securityAuthentication = "simple" - providerUrl = "ldaps://somedomain.com:9999" - } - } play.filters.enabled += "play.filters.csrf.CSRFFilter" diff --git a/modules/mysql/src/it/resources/application.conf b/modules/mysql/src/it/resources/application.conf index d134a7265..d17e78063 100644 --- a/modules/mysql/src/it/resources/application.conf +++ b/modules/mysql/src/it/resources/application.conf @@ -1,5 +1,7 @@ mysql { class-name = "vinyldns.mysql.repository.MySqlDataStoreProvider" + endpoint = "localhost:19002" + endpoint = ${?MYSQL_ENDPOINT} settings { # JDBC Settings, these are all values in scalikejdbc-config, not our own @@ -7,8 +9,8 @@ mysql { # assumes a docker or mysql instance running locally name = "vinyldns2" driver = "org.mariadb.jdbc.Driver" - migration-url = "jdbc:mariadb://localhost:19002/" - url = "jdbc:mariadb://localhost:19002/vinyldns2" + migration-url = "jdbc:mariadb://"${mysql.endpoint}"/" + url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns2" user = "root" password = "pass" @@ -43,8 +45,8 @@ queue { settings = { name = "vinyldns2" driver = "org.mariadb.jdbc.Driver" - migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass" - url = "jdbc:mariadb://localhost:19002/vinyldns2?user=root&password=pass" + migration-url = "jdbc:mariadb://"${mysql.endpoint}"/?user=root&password=pass" + url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns2?user=root&password=pass" user = "root" password = "pass" diff --git a/modules/mysql/src/main/resources/test/ddl.sql b/modules/mysql/src/main/resources/test/ddl.sql index c54ad1a54..851ac6672 100644 --- a/modules/mysql/src/main/resources/test/ddl.sql +++ b/modules/mysql/src/main/resources/test/ddl.sql @@ -6,7 +6,7 @@ -- Ex: "jdbc:h2:mem:vinyldns;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;INIT=RUNSCRIPT FROM 'classpath:test/ddl.sql'" -- -CREATE TABLE batch_change +CREATE TABLE IF NOT EXISTS batch_change ( id char(36) not null primary key, user_id char(36) not null, @@ -22,16 +22,16 @@ CREATE TABLE batch_change cancelled_timestamp datetime null ); -create index batch_change_approval_status_index +CREATE INDEX IF NOT EXISTS batch_change_approval_status_index on batch_change (approval_status); -create index batch_change_user_id_created_time_index +CREATE INDEX IF NOT EXISTS batch_change_user_id_created_time_index on batch_change (user_id, created_time); -create index batch_change_user_id_index +CREATE INDEX IF NOT EXISTS batch_change_user_id_index on batch_change (user_id); -create table group_change +CREATE TABLE IF NOT EXISTS group_change ( id char(36) not null primary key, group_id char(36) not null, @@ -39,10 +39,10 @@ create table group_change data blob not null ); -create index group_change_group_id_index +CREATE INDEX IF NOT EXISTS group_change_group_id_index on group_change (group_id); -create table `groups` +CREATE TABLE IF NOT EXISTS `groups` ( id char(36) not null primary key, name varchar(256) not null, @@ -52,10 +52,10 @@ create table `groups` email varchar(256) not null ); -create index groups_name_index +CREATE INDEX IF NOT EXISTS groups_name_index on `groups` (name); -create table membership +CREATE TABLE IF NOT EXISTS membership ( user_id char(36) not null, group_id char(36) not null, @@ -63,7 +63,7 @@ create table membership primary key (user_id, group_id) ); -create table message_queue +CREATE TABLE IF NOT EXISTS message_queue ( id char(36) not null primary key, message_type tinyint null, @@ -75,16 +75,16 @@ create table message_queue attempts int default 0 not null ); -create index message_queue_inflight_index +CREATE INDEX IF NOT EXISTS message_queue_inflight_index on message_queue (in_flight); -create index message_queue_timeout_index +CREATE INDEX IF NOT EXISTS message_queue_timeout_index on message_queue (timeout_seconds); -create index message_queue_updated_index +CREATE INDEX IF NOT EXISTS message_queue_updated_index on message_queue (updated); -create table record_change +CREATE TABLE IF NOT EXISTS record_change ( id char(36) not null primary key, zone_id char(36) not null, @@ -93,13 +93,13 @@ create table record_change data blob not null ); -create index record_change_created_index +CREATE INDEX IF NOT EXISTS record_change_created_index on record_change (created); -create index record_change_zone_id_index +CREATE INDEX IF NOT EXISTS record_change_zone_id_index on record_change (zone_id); -create table recordset +CREATE TABLE IF NOT EXISTS recordset ( id char(36) not null primary key, zone_id char(36) not null, @@ -112,16 +112,16 @@ create table recordset unique (zone_id, name, type) ); -create index recordset_fqdn_index +CREATE INDEX IF NOT EXISTS recordset_fqdn_index on recordset (fqdn); -create index recordset_owner_group_id_index +CREATE INDEX IF NOT EXISTS recordset_owner_group_id_index on recordset (owner_group_id); -create index recordset_type_index +CREATE INDEX IF NOT EXISTS recordset_type_index on recordset (type); -create table single_change +CREATE TABLE IF NOT EXISTS single_change ( id char(36) not null primary key, seq_num smallint not null, @@ -138,13 +138,13 @@ create table single_change on delete cascade ); -create index single_change_batch_change_id_index +CREATE INDEX IF NOT EXISTS single_change_batch_change_id_index on single_change (batch_change_id); -create index single_change_record_set_change_id_index +CREATE INDEX IF NOT EXISTS single_change_record_set_change_id_index on single_change (record_set_change_id); -create table stats +CREATE TABLE IF NOT EXISTS stats ( id bigint auto_increment primary key, name varchar(255) not null, @@ -152,13 +152,13 @@ create table stats created datetime not null ); -create index stats_name_created_index +CREATE INDEX IF NOT EXISTS stats_name_created_index on stats (name, created); -create index stats_name_index +CREATE INDEX IF NOT EXISTS stats_name_index on stats (name); -create table task +CREATE TABLE IF NOT EXISTS task ( name varchar(255) not null primary key, in_flight bit not null, @@ -166,7 +166,7 @@ create table task updated datetime null ); -create table user +CREATE TABLE IF NOT EXISTS user ( id char(36) not null primary key, user_name varchar(256) not null, @@ -174,13 +174,13 @@ create table user data blob not null ); -create index user_access_key_index +CREATE INDEX IF NOT EXISTS user_access_key_index on user (access_key); -create index user_user_name_index +CREATE INDEX IF NOT EXISTS user_user_name_index on user (user_name); -create table user_change +CREATE TABLE IF NOT EXISTS user_change ( change_id char(36) not null primary key, user_id char(36) not null, @@ -188,7 +188,7 @@ create table user_change created_timestamp bigint(13) not null ); -create table zone +CREATE TABLE IF NOT EXISTS zone ( id char(36) not null primary key, name varchar(256) not null, @@ -198,13 +198,13 @@ create table zone unique (name) ); -create index zone_admin_group_id_index +CREATE INDEX IF NOT EXISTS zone_admin_group_id_index on zone (admin_group_id); -create index zone_name_index +CREATE INDEX IF NOT EXISTS zone_name_index on zone (name); -create table zone_access +CREATE TABLE IF NOT EXISTS zone_access ( accessor_id char(36) not null, zone_id char(36) not null, @@ -214,13 +214,13 @@ create table zone_access on delete cascade ); -create index zone_access_accessor_id_index +CREATE INDEX IF NOT EXISTS zone_access_accessor_id_index on zone_access (accessor_id); -create index zone_access_zone_id_index +CREATE INDEX IF NOT EXISTS zone_access_zone_id_index on zone_access (zone_id); -create table zone_change +CREATE TABLE IF NOT EXISTS zone_change ( change_id char(36) not null primary key, zone_id char(36) not null, @@ -228,11 +228,13 @@ create table zone_change created_timestamp bigint(13) not null ); -create index zone_change_created_timestamp_index +CREATE INDEX IF NOT EXISTS zone_change_created_timestamp_index on zone_change (created_timestamp); -create index zone_change_zone_id_index +CREATE INDEX IF NOT EXISTS zone_change_zone_id_index on zone_change (zone_id); +DELETE FROM task WHERE name = 'user_sync'; + INSERT IGNORE INTO task(name, in_flight, created, updated) VALUES ('user_sync', 0, NOW(), NULL); diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/MySqlConnector.scala b/modules/mysql/src/main/scala/vinyldns/mysql/MySqlConnector.scala index 25c25b56b..ee4bc6e79 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/MySqlConnector.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/MySqlConnector.scala @@ -29,8 +29,8 @@ object MySqlConnector { private val logger = LoggerFactory.getLogger("MySqlConnector") def runDBMigrations(config: MySqlConnectionConfig): IO[Unit] = - // We can skip migrations for h2, we'll use the test/ddl.sql for initializing - // that for testing + // We can skip migrations for h2, we'll use the test/ddl.sql for initializing + // that for testing if (config.driver.contains("h2")) IO.unit else { val migrationConnectionSettings = MySqlDataSourceSettings( @@ -90,14 +90,21 @@ object MySqlConnector { def retry[T](times: Int, delayMs: Int)(op: => T) = Iterator .range(0, times) - .map(_ => Try(op)) - .flatMap { - case Success(t) => Some(t) - case Failure(_) => - logger.warn("failed to startup database connection, retrying..") - Thread.sleep(delayMs) - None - } + .map(index => (index, Try(op))) + .flatMap(x => { + val (currentIndex, result) = x + result match { + case Success(t) => Some(t) + case Failure(e) => + logger.warn("failed to startup database connection, retrying..") + // Hard abort if we exhaust retries + if (currentIndex >= times - 1) { + throw e + } + Thread.sleep(delayMs) + None + } + }) .toSeq .head diff --git a/modules/portal/conf/application.conf b/modules/portal/conf/application.conf index f6d6c77b6..35c7809e1 100644 --- a/modules/portal/conf/application.conf +++ b/modules/portal/conf/application.conf @@ -1,23 +1,62 @@ -http.port = 9001 +LDAP { + # For OpenLDAP, this would be a full DN to the admin for LDAP / user that can see all users + user = "cn=admin,dc=planetexpress,dc=com" -crypto { - type = "vinyldns.core.crypto.JavaCrypto" - secret = "8B06A7F3BC8A2497736F1916A123AA40E88217BE9264D8872597EF7A6E5DCE61" -} + # Password for the admin account + password = "GoodNewsEveryone" -data-stores = ["mysql"] -data-stores = ${?DATA_STORES} + # Keep this as an empty string for OpenLDAP + domain = "" -mysql { - repositories { - user { - } - task { - } - user-change { - } + # This will be the name of the LDAP field that carries the user's login id (what they enter in the username in login form) + userNameAttribute = "uid" + + # For organization, leave empty for this demo, the domainName is what matters, and that is the LDAP structure + # to search for users that require login + searchBase = [ + {organization = "", domainName = "ou=people,dc=planetexpress,dc=com"}, + ] + context { + initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory" + initialContextFactory = ${?LDAP_INITIAL_CONTEXT_CLASS} + securityAuthentication = "simple" + securityAuthentication = ${?LDAP_SECURITY_AUTH} + + # Note: The following assumes a purely docker setup, using container_name = vinyldns-ldap + providerUrl = "ldap://vinyldns-ldap:19004" + providerUrl = ${?LDAP_PROVIDER_URL} + } + + # This is only needed if keeping vinyldns user store in sync with ldap (to auto lock out users who left your + # company for example) + user-sync { + enabled = false + enabled = ${?USER_SYNC_ENABLED} + hours-polling-interval = 1 + hours-polling-interval = ${?USER_SYNC_POLL_INTERVAL} } } -// Local.conf has files specific to your environment, for example your own LDAP settings +# Note: This MUST match the API or strange errors will ensue, NoOpCrypto should not be used for production +crypto { + type = "vinyldns.core.crypto.NoOpCrypto" + type = ${?CRYPTO_TYPE} + secret = ${?CRYPTO_SECRET} +} + +http.port = 9001 +http.port = ${?PORTAL_PORT} + +data-stores = ["mysql"] + +# Must be true to manage shared zones through the portal +shared-display-enabled = true +shared-display-enabled = ${?SHARED_ZONES_ENABLED} + +# You generate this yourself following https://www.playframework.com/documentation/2.7.x/ApplicationSecret +play.http.secret.key = "changeme" +play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY} + +# You can provide configuration overrides via local.conf if you don't want to replace everything in +# this configuration file include "local.conf" diff --git a/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala b/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala index dbd5a17e7..cd799f182 100644 --- a/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala +++ b/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala @@ -30,7 +30,7 @@ import vinyldns.core.domain.{Fqdn, record} import vinyldns.core.domain.record.{RecordSet, RecordType} class Route53IntegrationSpec - extends AnyWordSpec + extends AnyWordSpec with BeforeAndAfterAll with BeforeAndAfterEach with Matchers { @@ -52,7 +52,7 @@ class Route53IntegrationSpec "test", Option("access"), Option("secret"), - "http://127.0.0.1:19003", + sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"), "us-east-1" ) ) diff --git a/modules/sqs/src/it/resources/application.conf b/modules/sqs/src/it/resources/application.conf index 49705a5b0..b235d0f8c 100644 --- a/modules/sqs/src/it/resources/application.conf +++ b/modules/sqs/src/it/resources/application.conf @@ -8,6 +8,7 @@ sqs { secret-key = "test" signing-region = "us-east-1" service-endpoint = "http://localhost:19003/" + service-endpoint = ${?SQS_SERVICE_ENDPOINT} queue-name = "sqs-override-name" } } diff --git a/modules/sqs/src/it/scala/vinyldns/sqs/queue/SqsMessageQueueProviderIntegrationSpec.scala b/modules/sqs/src/it/scala/vinyldns/sqs/queue/SqsMessageQueueProviderIntegrationSpec.scala index 496212e8a..561d7936b 100644 --- a/modules/sqs/src/it/scala/vinyldns/sqs/queue/SqsMessageQueueProviderIntegrationSpec.scala +++ b/modules/sqs/src/it/scala/vinyldns/sqs/queue/SqsMessageQueueProviderIntegrationSpec.scala @@ -15,6 +15,7 @@ */ package vinyldns.sqs.queue + import com.typesafe.config.ConfigFactory import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -24,50 +25,53 @@ import pureconfig._ import pureconfig.generic.auto._ class SqsMessageQueueProviderIntegrationSpec extends AnyWordSpec with Matchers { - val undertest = new SqsMessageQueueProvider() + val underTest = new SqsMessageQueueProvider() + private val sqsEndpoint = sys.env.getOrElse("SQS_SERVICE_ENDPOINT", "http://localhost:19003") "load" should { "fail if a required setting is not provided" in { val badConfig = - ConfigFactory.parseString(""" - | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - | polling-interval = 250.millis - | messages-per-poll = 10 - | max-retries = 100 - | - | settings { - | service-endpoint = "http://localhost:19003/" - | queue-name = "queue-name" - | } - | """.stripMargin) + ConfigFactory.parseString( + s""" + | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + | polling-interval = 250.millis + | messages-per-poll = 10 + | max-retries = 100 + | + | settings { + | service-endpoint = "$sqsEndpoint" + | queue-name = "queue-name" + | } + | """.stripMargin) val badSettings = ConfigSource.fromConfig(badConfig).loadOrThrow[MessageQueueConfig] - a[pureconfig.error.ConfigReaderException[MessageQueueConfig]] should be thrownBy undertest + a[pureconfig.error.ConfigReaderException[MessageQueueConfig]] should be thrownBy underTest .load(badSettings) .unsafeRunSync() } "create the queue if the queue is non-existent" in { val nonExistentQueueConfig = - ConfigFactory.parseString(""" - | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - | polling-interval = 250.millis - | messages-per-poll = 10 - | max-retries = 100 - | - | settings { - | access-key = "x" - | secret-key = "x" - | signing-region = "us-east-1" - | service-endpoint = "http://localhost:19003/" - | queue-name = "new-queue" - | } - | """.stripMargin) + ConfigFactory.parseString( + s""" + | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + | polling-interval = 250.millis + | messages-per-poll = 10 + | max-retries = 100 + | + | settings { + | access-key = "x" + | secret-key = "x" + | signing-region = "us-east-1" + | service-endpoint = "$sqsEndpoint" + | queue-name = "new-queue" + | } + | """.stripMargin) val messageConfig = ConfigSource.fromConfig(nonExistentQueueConfig).loadOrThrow[MessageQueueConfig] - val messageQueue = undertest.load(messageConfig).unsafeRunSync() + val messageQueue = underTest.load(messageConfig).unsafeRunSync() noException should be thrownBy messageQueue .asInstanceOf[SqsMessageQueue] @@ -77,65 +81,68 @@ class SqsMessageQueueProviderIntegrationSpec extends AnyWordSpec with Matchers { "fail with InvalidQueueName if an invalid queue name is given" in { val invalidQueueNameConfig = - ConfigFactory.parseString(""" - | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - | polling-interval = 250.millis - | messages-per-poll = 10 - | max-retries = 100 - | - | settings { - | access-key = "x" - | secret-key = "x" - | signing-region = "us-east-1" - | service-endpoint = "http://localhost:19003/" - | queue-name = "bad*queue*name" - | } - | """.stripMargin) + ConfigFactory.parseString( + s""" + | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + | polling-interval = 250.millis + | messages-per-poll = 10 + | max-retries = 100 + | + | settings { + | access-key = "x" + | secret-key = "x" + | signing-region = "us-east-1" + | service-endpoint = "$sqsEndpoint" + | queue-name = "bad*queue*name" + | } + | """.stripMargin) val messageConfig = ConfigSource.fromConfig(invalidQueueNameConfig).loadOrThrow[MessageQueueConfig] - assertThrows[InvalidQueueName](undertest.load(messageConfig).unsafeRunSync()) + assertThrows[InvalidQueueName](underTest.load(messageConfig).unsafeRunSync()) } "fail with InvalidQueueName if a FIFO queue is specified" in { val fifoQueueName = - ConfigFactory.parseString(""" - | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - | polling-interval = 250.millis - | messages-per-poll = 10 - | max-retries = 100 - | - | settings { - | access-key = "x" - | secret-key = "x" - | signing-region = "us-east-1" - | service-endpoint = "http://localhost:19003/" - | queue-name = "queue.fifo" - | } - | """.stripMargin) + ConfigFactory.parseString( + s""" + | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + | polling-interval = 250.millis + | messages-per-poll = 10 + | max-retries = 100 + | + | settings { + | access-key = "x" + | secret-key = "x" + | signing-region = "us-east-1" + | service-endpoint = "$sqsEndpoint" + | queue-name = "queue.fifo" + | } + | """.stripMargin) val messageConfig = ConfigSource.fromConfig(fifoQueueName).loadOrThrow[MessageQueueConfig] - assertThrows[InvalidQueueName](undertest.load(messageConfig).unsafeRunSync()) + assertThrows[InvalidQueueName](underTest.load(messageConfig).unsafeRunSync()) } } "MessageQueueLoader" should { "invoke SQS provider properly" in { val nonExistentQueueConfig = - ConfigFactory.parseString(""" - | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" - | polling-interval = 250.millis - | messages-per-poll = 10 - | max-retries = 100 - | - | settings { - | access-key = "x" - | secret-key = "x" - | signing-region = "us-east-1" - | service-endpoint = "http://localhost:19003/" - | queue-name = "new-queue" - | } - | """.stripMargin) + ConfigFactory.parseString( + s""" + | class-name = "vinyldns.sqs.queue.SqsMessageQueueProvider" + | polling-interval = 250.millis + | messages-per-poll = 10 + | max-retries = 100 + | + | settings { + | access-key = "x" + | secret-key = "x" + | signing-region = "us-east-1" + | service-endpoint = "$sqsEndpoint" + | queue-name = "new-queue" + | } + | """.stripMargin) val messageConfig = ConfigSource.fromConfig(nonExistentQueueConfig).loadOrThrow[MessageQueueConfig] diff --git a/quickstart/.env b/quickstart/.env index 4a0f5894f..fb0b428e2 100644 --- a/quickstart/.env +++ b/quickstart/.env @@ -11,8 +11,9 @@ TEST_LOGIN=false # API Settings REST_PORT=9000 -SQS_ENDPOINT=http://vinyldns-integration:19003 +SQS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 SNS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 +R53_SERVICE_ENDPOINT=http://vinyldns-integration:19003 MYSQL_ENDPOINT=vinyldns-integration:19002 DEFAULT_DNS_ADDRESS=vinyldns-integration:19001 diff --git a/test/api/functional/application.conf b/test/api/functional/application.conf index 5b02a3601..a79c5cf1b 100644 --- a/test/api/functional/application.conf +++ b/test/api/functional/application.conf @@ -85,7 +85,7 @@ vinyldns { # Endpoint to access queue service-endpoint = "http://localhost:19003/" - service-endpoint = ${?SQS_ENDPOINT} + service-endpoint = ${?SQS_SERVICE_ENDPOINT} # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. queue-name = "vinyldns" diff --git a/test/api/integration/.env.integration b/test/api/integration/.env.integration new file mode 100755 index 000000000..12521a5c3 --- /dev/null +++ b/test/api/integration/.env.integration @@ -0,0 +1,27 @@ +# Overrides that can be used when dependent services are running in the vinyldns-integration host +# and 'sbt' or tests are running in another container. More importantly, when these services are +# not accessible via "localhost" +# +# Any docker container that wishes to to use these, can simply specify this file using the `--env-file` +# argument. + +# General settings +VINYLDNS_API_URL=http://vinyldns-integration:9000 +VINYLDNS_PORTAL_URL=http://vinyldns-integration:9001 + +# Portal settings +VINYLDNS_BACKEND_URL=http://vinyldns-integration:9000 +LDAP_PROVIDER_URL=ldap://vinyldns-integration:19004 + +# API Settings +SQS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 +SNS_SERVICE_ENDPOINT=http://vinyldns-integration:19003 +R53_SERVICE_ENDPOINT=http://vinyldns-integration:19003 +MYSQL_ENDPOINT=vinyldns-integration:19002 +DEFAULT_DNS_ADDRESS=vinyldns-integration:19001 + +JDBC_DRIVER=org.mariadb.jdbc.Driver +JDBC_USER=root +JDBC_PASSWORD=pass +JDBC_URL=jdbc:mariadb://vinyldns-integration:19002/vinyldns?user=${JDBC_USER}&password=${JDBC_PASSWORD} +JDBC_MIGRATION_URL=jdbc:mariadb://vinyldns-integration:19002/?user=${JDBC_USER}&password=${JDBC_PASSWORD} diff --git a/test/api/integration/Makefile b/test/api/integration/Makefile index 1bbb79ff8..4abd336cb 100644 --- a/test/api/integration/Makefile +++ b/test/api/integration/Makefile @@ -38,14 +38,16 @@ build: run: @set -euo pipefail + docker network create --driver bridge vinyldns_net &> /dev/null || true USE_TTY="" && test -t 1 && USE_TTY="-t" - docker run -i $${USE_TTY} --rm $(DOCKER_PARAMS) $(IMAGE_NAME) $(ARG_SEPARATOR) $(WITH_ARGS) + docker run -i $${USE_TTY} --rm --network vinyldns_net $(DOCKER_PARAMS) $(IMAGE_NAME) $(ARG_SEPARATOR) $(WITH_ARGS) run-bg: @set -euo pipefail docker stop $(IMAGE_NAME) &> /dev/null || true + docker network create --driver bridge vinyldns_net &> /dev/null || true USE_TTY="" && test -t 1 && USE_TTY="-t" - docker run -d $${USE_TTY} --name $(IMAGE_NAME) --rm $(DOCKER_PARAMS) -e RUN_SERVICES="deps-only tail-logs" -p 19001-19003:19001-19003 -p 19001:19001/udp $(IMAGE_NAME) + docker run -d $${USE_TTY} --name $(IMAGE_NAME) --rm --network vinyldns_net $(DOCKER_PARAMS) -e RUN_SERVICES="deps-only tail-logs" -p 19001-19003:19001-19003 -p 19001:19001/udp $(IMAGE_NAME) stop-bg: @set -euo pipefail @@ -53,8 +55,9 @@ stop-bg: run-local: @set -euo pipefail + docker network create --driver bridge vinyldns_net &> /dev/null || true USE_TTY="" && test -t 1 && USE_TTY="-t" - docker run -i $${USE_TTY} --rm $(DOCKER_PARAMS) -v "$(ROOT_DIR)/../../..:/build" $(IMAGE_NAME) -- $(WITH_ARGS) + docker run -i $${USE_TTY} --rm --network vinyldns_net $(DOCKER_PARAMS) -v "$(ROOT_DIR)/../../..:/build" $(IMAGE_NAME) -- $(WITH_ARGS) clean-containers: @set -euo pipefail diff --git a/test/api/integration/application.conf b/test/api/integration/application.conf index a13a8b256..d6a7500d5 100644 --- a/test/api/integration/application.conf +++ b/test/api/integration/application.conf @@ -114,7 +114,7 @@ vinyldns { # Endpoint to access queue service-endpoint = "http://localhost:19003/" - service-endpoint = ${?SQS_ENDPOINT} + service-endpoint = ${?SQS_SERVICE_ENDPOINT} # Queue name. Should be used in conjunction with service endpoint, rather than using a queue url which is subject to change. queue-name = "vinyldns"