druid/integration-tests-ex/docs/test-config.md

602 lines
20 KiB
Markdown

<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
## Test Configuration
Tests typically need to understand how the cluster is structured.
To create a test, you must supply at least three key components:
* A `cluster/<category>/docker-compose.yaml` file that launches the desired cluster.
(The folder name `<category>` becomes the application name in Docker.)
* A `src/test/resources/cluster/<category>/docker.yaml` file that describes the cluster
for tests. This file can also include Metastore SQL statements needed to
populate the metastore.
* The test itself, as a JUnit test that uses the `Initializer` class to
configure the tests to match the cluster.
This section explains the test configuration file which defines the test
cluster.
Note that you can create multiple versions of the `docker.yaml` file. For example,
you might want to create one that lists hosts and credentials unique to your
debugging environment. You then use your custom version in place of the standard
one.
## Cluster Types
The integration tests can run in a variety of cluster types, depending
on the details of the test:
* Docker Compose: the normal configuration that all tests support.
* Micro Quickstart: allows for a manual cluster setup, if, say, you
want to run services in your IDE. Supported by a subset of tests.
* Kubernetes: (Details needed.)
Each cluster type has its own quirks. The job of the tests's cluster configuration
file is to communicate those quirks to the test.
Docker and Kubernetes use proxies to communicate to the cluster. Thus, the host
known to the tests is different than the hosts known within the cluster. Ports
may also are mapped differently "outside" than "inside."
Clusters outside of Docker don't provide a good way to start and stop
services, so tests that want to do that (to, say, test high availability)
can't run except in a Docker cluster.
### Specify the Cluster Type
To reflect this, tests provide named configuration files. The configuration
itself is passed in via the environment:
```bash
export TEST_CONFIG=quickstart
```
```bash
java ... -DtestConfig=quickstart
```
The system property taskes precedence over the environment variable.
If neither are set, `docker` is the default. The configuration file
itself is assumed to be a resource named `/yaml/<config>.yaml`.
As a debug aide, a test can specify and ad-hoc file in the file system
to load for one-off special cases. See `Initialization.Builder` for
details.
## Cluster Configuration Files
Cluster configuration is specified in a file for ease of debugging. Since
configuration is in a file (resource), and not in environment variables or
system properties, you
should need no special launch setup in your IDE to run a test that uses
the standard Docker Compose cluster for that test.
The configuration file has the same name as the cluster type and resides on
the class path at `/yaml/<type>.yaml` and in the source tree at
`<test module>/src/test/resources/yaml/<type>.yaml`. The standard names are:
* `docker.yaml`: the default and required for all tests. Describes a Docker
Compose based test.
* `k8s.yaml`: a test cluster running in Kubernetes. (Details needed.)
* `local.yaml`: a local cluser such as Micro Quickstart cluster. (Details needed.)
* `<other>.yaml`: custom cluster configuration.
Configuration files support include files. Most of the boiler-plate configuration
should appear in commmon files. As a result, you should only need to specify
test-specific differences in your `docker.yaml` file, with all else obtained
from the included files.
## Configuration File Syntax
The configuration is a [YAML](https://yaml.org/spec/1.2.2/) file that
has a few top-level properties and an entry for each service in your
cluster.
### `type`
```yaml
type: docker|k8s|local|disabled
```
The type explains the infrastructure that runs the cluster:
* `docker`: a cluster launched in Docker, typically via Docker Compose.
A proxy host is needed. (See below.)
* `k8s`: a cluster run in Kubernets. (Details needed). A proxy host
is needed.
* `local`: a cluster running as processes on a network directly reachable
by the tests. Example: a micro-quickstart cluster running locally.
* `disabled`: the configuration is not supported by the test.
The `disabled` type is handy for tests that require Docker: you can say that
the test is not available when the cluster is local.
If the test tries to load a cluster name that does not exist, a "dummy"
configuration is loaded instead with the type set to `disabled`.
The type is separate from the cluster name (as explained earlier): there
may be multiple names for the same type. For example, you might have two
or three local cluster setups you wish to test.
### `include`
```yaml:
include:
- <file>
```
Allows including any number of other files. Similar to inheritance for
Docker Compose. The inheritance rules are:
* Properties set later in the list take precedence over properties set in
files earlier in the list.
* Properties set in the file take precedence over properties set in
included files.
* Includes can nest to any level.
Merging occurs as follows:
* Top level scalars: newer values replace older values.
* Services: newer values replace all older settings for that service.
* Metastore init: newer values add more queries to any list defined
by an earlier file.
* Properties: newer values replace values defined by earlier files.
The files are assumed to be resources (on the class path) and require
the full path name. Example: `/cluster/Commmon/base.yaml`
### `proxyHost`
```yaml
proxyHost: <host name>
```
When tests run in either Docker or Kubernetes, the test communicate with
a proxy, which forwards requests to the cluster hosts and ports. In
Docker, the proxy host is the machine that runs Docker. In Kubernetes,
the proxy host is the host running the Kubernetes proxy service.
There is no proxy host for clusters running directly on a machine.
If the proxy host is omitted for Docker, `localhost` is assumed.
### `datasourceSuffix`
```yaml
datasourceSuffix: <suffix>
```
Suffix to append to data source names in indexer tests. The default
is the empty string.
### `zk`
```yaml
zk:
<service object>
```
Specifies the ZooKeeper instances.
#### `startTimeoutSecs`
```yaml
startTimeoutSecs: <secs>
```
Specifies the amount of time to wait for ZK to become available when using the
test client. Optional.
### `metastore`
```yaml
metastore:
<service object>
```
Describes the Druid "metadata storage" (metastore) typically
hosted in the offical MySql container. See `MetastoreConfig` for
configuration options.
#### `driver`
```yaml
driver: <full class name>
```
The Driver to use to work with the metastore. The driver must be
available on the tests's class path.
#### `connectURI`
```yaml:
connectURI: <url>
```
The JDBC connetion URL. Example:
```text
jdbc:mysql://<host>:<port>/druid
```
The config system supports two special fields: `<host>` and `<port>`.
A string of form `<host>` will be replaced by the resolved host name
(proxy host for Docker) and `<port>` with the resolved port number.
#### `user`
```yaml
user: <user name>
```
The MySQL user name.
#### `password`
```yaml
user: <password>
```
The MySQL password.
#### `properties`
```yaml
properties:
<key>: <value>
```
Optional map of additional key/value pairs to pass to the JDBC driver.
### `kafka`
```yaml
zk:
<service object>
```
Describes the optional Kafka service.
### `druid`
```yaml
druid:
<service>:
<service object>
```
Describes the set of Druid services using the `ServiceConfig` object.
Each service is keyed by the standard service name: the same name used
by the Druid `server` option.
When using inheritance, overrides replace entire services: it is not possible
to override individual instances of the service. That is, an include file might
define `coordinator`, but a test-specific file might override this with a
definition of two Coordinators.
### `properties`
```yaml
properties:
<key>: <value>
```
Optional set of properties to use to configuration the Druid components loaded
by tests. This is the test-specific form of the standard Druid `common.runtime.properties`
and `runtime.properties` files. Because the test runs as a client, the server
files are not available, and might not even make sense. (The client is not
a "service", for example.) Technically, the properties listed here are added to
Guice as the one and only `Properties` object.
Typically most components work using the default values. Tests are free to change
any of these values for a given test scenario. The properties are
the same for all tests within a category. However, they can be changed via environment
variables via the environment variable "binding" mechanism described in
[tests](tests.md).
The "JSON configuration" mechanism wants all properties to be strings. YAML
will deserialize number-like properties as numbers. To avoid confusion, all
properties are converted to strings before being passed to Druid.
When using inheritance, later properties override earlier properties. Environment
variables, if bound, override the defaults specified in this section. Command-line
settings, if provided, have the highest priority.
A number of test-specific properties are avilable:
* `druid.test.config.cloudBucket`
* `druid.test.config.cloudPath`
### `settings`
The settings section is much like the properties section, and, indeed, are converted
to properties internally. Settings are a fixed set of values that map to the config
files used in the prior tests. Keys include:
| Setting | Property | Environment Variable |
| `druid_storage_type` | - | - |
| `druid_storage_bucket` | `druid.test.config.cloudBucket` | `DRUID_STORAGE_BUCKET` |
| `druid_storage_baseKey` | `druid.test.config.cloudPath` | `DRUID_STORAGE_BASEKEY` |
| `druid_s3_accessKey` | - | `AWS_ACCESS_KEY_ID` |
| `druid_s3_secretKey` | - | AWS_SECRET_ACCESS_KEY` |
The above replaces the config file mechanism from the older tests. In general, when a
setting is fixed for a test category, list it in the `docker.yaml` configuration file.
When it varies, pass it in as an environment variable. As a result, the prior configuration
file is not needed. As a result, the prior `override.config.path` property is not supported.
### `metastoreInit`
```yaml
metastoreInit:
- sql: |
<sql query>
```
A set of MySQL statements to be run against the
metadata storage before the test starts. Queries run in the
order specified. Ensure each is idempotent to
allow running tests multiple times against the same database.
To be kind to readers, please format the statements across multiple lines.
The code will compress out extra spaces before submitting the query so
that JSON payloads are as compact as possible.
The `sql` keyword is the only one supported at present. The idea is that
there may need to be context for some queries in some tests. (To be
enhanced as query conversion proceeds.)
When using inheritance, the set of queries is the union of all queries
in all configuration files. Base statements appear first, then included
statements.
### `metastoreInitDelaySec`
```yaml
metastoreInitDelaySec: <sec>
```
The default value is 6 seconds.
The metastore init section issues queries to the MySQL DB read by the
Coordinator. For performance, the Coordinator *does not* directly query
the database: instead, it queries an in-memory cache. This leads to the
following behavior:
* The Coordinator starts, checks the DB, and records the poll time.
* The test starts and updates the DB.
* The test runs and issues a query that needs the DB contents.
* The Coordinator checks that its poll timeout has not yet occurred
and returns the (empty) contents of the cache.
* The test checks the empty contents against the expected contents,
notices the results differ, and fails the test.
To work around this, we must change _two_ settings. First, change
the following Druid configuration for the Coordinator:
```yaml
- druid_manager_segments_pollDuration=PT5S
```
Second, change the `metastoreInitDelaySec` to be a bit longer:
```yaml
metastoreInitDelaySec: 6
```
The result is that the test will sit idle for 6 seconds, but that is better
than random failures.
**Note:** a better fix would be for the Coordinator to have an API that causes
it to flush its cache. Since some tests run two coordinators, the message must be
sent to both. An even better fix would be fore the Coordinator to detect such
changes itself somehow.
### Service Object
Generic object to describe Docker Compose services.
#### `if`
Conditionally defines a service. The system defines a set of configutation tags.
At present there are only two:
* `middleManager`: the cluster runs a MiddleManager
* `indexer`: the cluster runs an Indexer
The `if` tag conditionally enables a service only if the corresponding tag is set.
Thus, for a cluster that can use either a middle manager or an indexer:
```yaml
middlemanager:
if: middleManager
instances:
- port: 8091
indexer:
if: indexer
instances:
- port: 8091
```
#### `instances`
```yaml
instances:
- <service-instance>
```
Describes the instances of the service as `ServiceInstance` objects.
Each service requires at least one instance. If more than one, then
each instance must define a `tag` that is a suffix that distinguishes
the instances.
### Service Instance Object
The service sections all allow multiple instances of each service. Service
instances define each instance of a service and provide a number of properties:
#### `tag`
When a service has more than one instance, the instances must have unique
names. The name is made up of the a base name (see below) with the tag
appended. Thus, if the service is `cooordinator` and the tag is `one`,
then the instance name is `coordinator-one`.
The tag is required when there is more than one instance of a service,
and is optional if there is only one instance. The tag corresponds to the
`DRUID_INSTANCE` environment variable passed into the container.
#### `container`
```yaml
container: <container name>
```
Name of the Docker container. If omitted, defaults to:
* `<service name>-<tag>` if a `tag` is provided (see below.)
* The name of the service (if there is only one instance).
#### `host`
```yaml
host: <host name or IP>
```
The host name or IP address on which the instance runs. This is
the host name known to the _cluster_: the name inside a Docker overlay network.
Has the same defaults as `container`.
#### `port`
```yaml
port: <port>
```
The port number of the service on the container as seen by other
services running within Docker. Required.
(TODO: If TLS is enabled, this is the TLS port.)
#### `proxyPort`
```yaml
proxyPort: <port>
```
The port number for the service as exposed on the proxy host.
Defaults to the same as `port`. You must specify a value if
you run multiple instances of the same service.
## Conversion Guide
In prior tests, a config file, and the `ConfigFileConfigProvider` class,
provided test configuration. In this version, the file described here
provides configuration. This section presents a mapping from the old to
the new form.
The `IntegrationTestingConfig` class, which the above class used to provide,
is reimplemented to provide the same information
to tests as before; only the source of the information has changed.
The new framework assumes that each Druid node is configured either for
plain text or for TLS. (If this assumption is wrong, we'll change the config
file to match.)
Many of the properties are derived from information in the configuration file.
For example, host names (within Docker) are those given in the `druid` section,
and ports (within the cluster and for the client) are given in `druid.<service>.intances.port`,
from which the code computes the URL.
The old system hard-codes the idea that there are two coordinators or overlords. The
new system allows any number of instances.
| Method | Old Property | New Format |
| ------ | ------------ | ---------- |
| Router | | |
| `getRouterHost()` | `router_host` | `'router'` |
| `getRouterUrl()` | `router_url` | `'router'` & `instances.port` |
| `getRouterTLSUrl()` | `router_tls_url` | " |
| `getPermissiveRouterUrl()` | `router_permissive_url` | " |
| `getPermissiveRouterTLSUrl()` | `router_permissive_tls_url` | " |
| `getNoClientAuthRouterUrl()` | `router_no_client_auth_url` | " |
| `getNoClientAuthRouterTLSUrl()` | `router_no_client_auth_tls_url` | " |
| `getCustomCertCheckRouterUrl()` | | " |
| `getCustomCertCheckRouterTLSUrl()` | | " |
| Broker | | |
| `getBrokerHost()` | `broker_host` | `'broker'` |
| `getBrokerUrl()` | `broker_url` | `'broker'` & `instances.port` |
| `getBrokerTLSUrl()` | `broker_tls_url` | " |
| Coordinator | | |
| `getCoordinatorHost()` | `coordinator_host` | `'coordinator'` + `tag` |
| `getCoordinatorTwoHost()` | `coordinator_two_host` | " |
| `getCoordinatorUrl()` | `coordinator_url` | host & `instances.port` |
| `getCoordinatorTLSUrl()` | `coordinator_tls_url` | " |
| `getCoordinatorTwoUrl()` | `coordinator_two_url` | " |
| `getCoordinatorTwoTLSUrl()` | `coordinator_two_tls_url` | " |
| Overlord | | |
| `getOverlordUrl()` | ? | `'overlord'` + `tag` |
| `getOverlordTwoHost()` | `overlord_two_host` | " |
| `getOverlordTwoUrl()` | `overlord_two_url` | host & `instances.port` |
| `getOverlordTLSUrl()` | ? | " |
| `getOverlordTwoTLSUrl()` | `overlord_two_tls_url` | " |
| Overlord | | |
| `getHistoricalHost()` | `historical_host` | `historical'` |
| `getHistoricalUrl()` | `historical_url` | `'historical'` & `instances.port` |
| `getHistoricalTLSUrl()` | `historical_tls_url` | " |
| Overlord | | |
| `getMiddleManagerHost()` | `middlemanager_host` | `'middlemanager'` |
| Dependencies | | |
| `getZookeeperHosts()` | `zookeeper_hosts` | `'zk'` |
| `getKafkaHost()` | `kafka_host` | '`kafka`' |
| `getSchemaRegistryHost()` | `schema_registry_host` | ? |
| `getProperty()` | From config file | From `settings` |
| `getProperties()` | " | " |
| `getUsername()` | `username` | Setting |
| `getPassword()` | `password` | Setting |
| `getCloudBucket()` | `cloud_bucket` | Setting |
| `getCloudPath()` | `cloud_path` | Setting |
| `getCloudRegion()` | `cloud_region` | Setting |
| `getS3AssumeRoleWithExternalId()` | `s3_assume_role_with_external_id` | Setting |
| `getS3AssumeRoleExternalId()` | `s3_assume_role_external_id` | Setting |
| `getS3AssumeRoleWithoutExternalId()` | `s3_assume_role_without_external_id` | Setting |
| `getAzureKey()` | `azureKey` | Setting |
| `getHadoopGcsCredentialsPath()` | `hadoopGcsCredentialsPath` | Setting |
| `getStreamEndpoint()` | `stream_endpoint` | Setting |
| `manageKafkaTopic()` | ? | ? |
| `getExtraDatasourceNameSuffix()` | ? | ? |
Pre-defined environment bindings:
| Setting | Env. Var. |
| `cloudBucket` | `DRUID_CLOUD_BUCKET` |
| `cloudPath` | `DRUID_CLOUD_PATH` |
| `s3AccessKey` | `AWS_ACCESS_KEY_ID` |
| `s3SecretKey` | `AWS_SECRET_ACCESS_KEY` |
| `azureContainer` | `AZURE_CONTAINER` |
| `azureAccount` | `AZURE_ACCOUNT` |
| `azureKey` | `AZURE_KEY` |
| `googleBucket` | `GOOGLE_BUCKET` |
| `googlePrefix` | `GOOGLE_PREFIX` |
Others can be added in `Initializer.Builder`.