diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eee87b6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM eclipse-temurin:17-jammy +RUN apt update && apt-get install -y emacs-nox net-tools netcat vim nmon python3-lxml unzip curl + +# Copy the TAK Server release file +COPY takserver-release/ /takserver-release/ +COPY scripts/ /opt/scripts/ + +# Create necessary directories +RUN mkdir -p /opt/tak/data/logs /opt/tak/data/certs + +ENTRYPOINT ["/bin/bash", "/opt/scripts/docker_entrypoint.sh"] diff --git a/EDIT_ME.env b/EDIT_ME.env new file mode 100644 index 0000000..d23efce --- /dev/null +++ b/EDIT_ME.env @@ -0,0 +1,28 @@ +# The database password +POSTGRES_PASSWORD='' + +# Certificate Authority configuration details +CA_NAME='' +CA_PASS='' +STATE='' +CITY='' +ORGANIZATION='' +ORGANIZATIONAL_UNIT='' + +# The password for the takserver instance's certificate +TAKSERVER_CERT_PASS='' + +# The username and password for the takserver administrator +ADMIN_CERT_NAME='' +ADMIN_CERT_PASS='' + +# Values that should not require modification with standard usage +POSTGRES_DB=cot +POSTGRES_USER=martiuser +POSTGRES_URL=jdbc:postgresql://takdb:5432/cot + +# Heap size configuration (in MB) +CONFIG_MAX_HEAP=512 +MESSAGING_MAX_HEAP=2048 +API_MAX_HEAP=1024 +PLUGIN_MANAGER_MAX_HEAP=512 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ac7168 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# TAK Server Docker Setup + +A self-contained Docker setup for TAK Server that automatically extracts and configures from the release files. + +## Quick Start + +1. **Build the Docker image:** + ```bash + ./build.sh + ``` + +2. **Configure environment variables:** + Edit `EDIT_ME.env` and fill in the required values: + ```bash + # Required fields to fill in: + POSTGRES_PASSWORD='your_db_password' + CA_NAME='your_ca_name' + CA_PASS='your_ca_password' + STATE='your_state' + CITY='your_city' + ORGANIZATION='your_organization' + ORGANIZATIONAL_UNIT='your_org_unit' + TAKSERVER_CERT_PASS='your_takserver_cert_password' + ADMIN_CERT_NAME='admin_username' + ADMIN_CERT_PASS='admin_password' + ``` + +3. **Start the services:** + ```bash + docker-compose up + ``` + +## Features + +- **Self-contained**: Automatically extracts TAK Server from release files +- **Single image**: Everything built into one Docker image (plus database) +- **Environment driven**: All configuration through environment variables +- **Automatic setup**: Certificates, database schema, and admin user created automatically +- **Persistent data**: Docker volumes for data persistence +- **Simple deployment**: Just build and run + +## Structure + +- `Dockerfile` - Self-contained TAK Server image +- `docker-compose.yml` - Orchestration with PostgreSQL database +- `EDIT_ME.env` - Environment variables configuration +- `build.sh` - Build script +- `scripts/docker_entrypoint.sh` - Main entrypoint with extraction and startup logic +- `scripts/coreConfigEnvHelper.py` - Configuration helper + +## Environment Variables + +### Required +- `POSTGRES_PASSWORD` - Database password +- `CA_NAME` - Certificate Authority name +- `CA_PASS` - Certificate Authority password +- `STATE` - State for certificate generation +- `CITY` - City for certificate generation +- `ORGANIZATION` - Organization for certificate generation +- `ORGANIZATIONAL_UNIT` - Organizational unit for certificate generation +- `TAKSERVER_CERT_PASS` - TAK Server certificate password +- `ADMIN_CERT_NAME` - Admin username +- `ADMIN_CERT_PASS` - Admin password + +### Optional (with defaults) +- `CONFIG_MAX_HEAP=512` - Config service heap size (MB) +- `MESSAGING_MAX_HEAP=2048` - Messaging service heap size (MB) +- `API_MAX_HEAP=1024` - API service heap size (MB) +- `PLUGIN_MANAGER_MAX_HEAP=512` - Plugin Manager heap size (MB) +- `POSTGRES_DB=cot` - Database name +- `POSTGRES_USER=martiuser` - Database user +- `POSTGRES_URL=jdbc:postgresql://takdb:5432/cot` - Database URL + +## Ports + +- `8443` - HTTPS API +- `8444` - Certificate enrollment +- `8446` - Federation +- `8089` - Web interface +- `9000` - Messaging +- `9001` - Streaming + +## Volumes + +- `takserver_data` - TAK Server data (logs, certs, config) +- `takdb_data` - PostgreSQL data + +## What the entrypoint does + +1. **Extract TAK Server** - Automatically extracts from `/takserver-release/` if not already done +2. **Validate environment** - Checks all required environment variables +3. **Initialize directories** - Creates necessary data directories +4. **Generate certificates** - Creates CA, server, and admin certificates if needed +5. **Configure TAK Server** - Updates CoreConfig.xml with environment variables +6. **Initialize database** - Waits for database and runs schema setup +7. **Start services** - Launches all TAK Server services in correct order +8. **Create admin user** - Adds the admin user after services are ready + +## Manual file editing + +If you need to customize CoreConfig.xml or other configuration files beyond what environment variables provide, you can: + +1. Start the container once to generate initial files +2. Stop the container +3. Edit files in the `takserver_data` volume +4. Restart the container + +The entrypoint will preserve existing configuration files and only regenerate what's missing. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..349f11a --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Simple build script +set -e + +echo "Building TAK Server Docker image..." + +# Check if takserver-release directory exists +if [[ ! -d "takserver-release" ]]; then + echo "Creating symlink to takserver-release..." + ln -sf ../../takserver-release takserver-release +fi + +# Build the Docker image +docker build -t takserver:latest . + +echo "Build complete!" +echo "Edit EDIT_ME.env with your configuration, then run:" +echo "docker-compose up" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0500553 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.4' + +services: + takserver: + image: takserver:latest + build: . + env_file: + - 'EDIT_ME.env' + volumes: + - 'takserver_data:/opt/tak/data' + depends_on: + - takdb + networks: + - taknet + ports: + - '8089:8089' + - '8443:8443' + - '8444:8444' + - '8446:8446' + - '9000:9000' + - '9001:9001' + + takdb: + image: postgis/postgis:15-3.3 + networks: + - taknet + environment: + - POSTGRES_DB=cot + - POSTGRES_USER=martiuser + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - 'takdb_data:/var/lib/postgresql/data' + +networks: + taknet: + +volumes: + takserver_data: + driver: local + takdb_data: + driver: local diff --git a/scripts/coreConfigEnvHelper.py b/scripts/coreConfigEnvHelper.py new file mode 100644 index 0000000..9a718a4 --- /dev/null +++ b/scripts/coreConfigEnvHelper.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import argparse +import os + +from lxml import etree + +parser = argparse.ArgumentParser('CoreConfig Configuration Helper') +parser.add_argument('source', metavar='SOURCE', type=str, help='The source CoreConfig path') +parser.add_argument('target', metavar='TARGET', type=str, help='The target CoreConfig path') + + +CORE_CONFIG_NAMESPACE = 'http://bbn.com/marti/xml/config' + + +class ConfigEntry: + def __init__(self, env_var, xpath, attribute_name, required, hide_value): + self._env_var = env_var # type: str + self._xpath = xpath # type: str + self._attribute_name = attribute_name # type: str + self._required = required # type: bool + self._hide_value = hide_value # type: bool + + @property + def attribute_name(self): + return self._attribute_name + + @property + def xpath(self): + return self._xpath + + @property + def env_var(self): + return self._env_var + + @property + def required(self): + return self._required + + @property + def hide_value(self): + return self._hide_value + + def value(self): + if self._env_var in os.environ.keys(): + return os.environ[self._env_var] + else: + return None + + +CONFIG_VALUES = [ + ConfigEntry('POSTGRES_URL', 'tak:repository/tak:connection', 'url', False, False), + ConfigEntry('POSTGRES_USER', 'tak:repository/tak:connection', 'username', False, False), + ConfigEntry('POSTGRES_PASSWORD', 'tak:repository/tak:connection', 'password', True, True), + ConfigEntry('TAKSERVER_CERT_PASS', 'tak:security/tak:tls', 'keystorePass', True, True), + ConfigEntry('CA_PASS', 'tak:security/tak:tls', 'truststorePass', True, True) +] + + +class CoreConfigHelper: + def __init__(self, source_filepath): + self._source_filepath = source_filepath + self._tree = etree.parse(open(source_filepath), etree.XMLParser()) + self._root = self._tree.getroot() + self._namespaces = { + 'tak': CORE_CONFIG_NAMESPACE + } + + def find(self, xpath): + """ + :rtype: etree.Element + """ + results = self._tree.findall(path=xpath, namespaces=self._namespaces) + + if len(results) > 1: + raise Exception('XPath expressions that return multiple elements are not currently supported!') + return results[0] + + def process_configuration(self, config_values, target_filepath): + """ + :type config_values: list[ConfigEntry] + """ + for config in config_values: + value = config.value() + if value is None: + if config.required: + raise Exception('The environment variable "' + config.env_var + '" is required!') + else: + element = self.find(config.xpath) + element.set(config.attribute_name, value) + if config.hide_value: + print(config.xpath.replace('tak:', '') + ' attribute ' + config.attribute_name + ' set to ********') + else: + print(config.xpath.replace('tak:', '') + ' attribute ' + config.attribute_name + ' set to "' + value + '"') + + self._tree.write(target_filepath, xml_declaration=True, encoding='UTF-8') + + +def main(): + args = parser.parse_args() + helper = CoreConfigHelper(args.source) + helper.process_configuration(CONFIG_VALUES, args.target) + + +if __name__ == '__main__': + main() diff --git a/scripts/docker_entrypoint.sh b/scripts/docker_entrypoint.sh new file mode 100644 index 0000000..e43ac01 --- /dev/null +++ b/scripts/docker_entrypoint.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash + +set -e + +# Environment variable defaults +CONFIG_MAX_HEAP=${CONFIG_MAX_HEAP:-512} +MESSAGING_MAX_HEAP=${MESSAGING_MAX_HEAP:-2048} +API_MAX_HEAP=${API_MAX_HEAP:-1024} +PLUGIN_MANAGER_MAX_HEAP=${PLUGIN_MANAGER_MAX_HEAP:-512} +POSTGRES_DB=${POSTGRES_DB:-cot} +POSTGRES_USER=${POSTGRES_USER:-martiuser} +POSTGRES_URL=${POSTGRES_URL:-jdbc:postgresql://takdb:5432/cot} + +TR=/opt/tak +CR=${TR}/certs +CONFIG=${TR}/data/CoreConfig.xml +TAKIGNITECONFIG=${TR}/data/TAKIgniteConfig.xml +CONFIG_PID=null +MESSAGING_PID=null +API_PID=null +PM_PID=null + +check_env_var() { + if [[ "${!1}" == "" ]]; then + echo "ERROR: Environment variable '${1}' must be set for ${2}!" + exit 1 + fi +} + +cleanup() { + echo "Shutting down TAK Server..." + if [ $CONFIG_PID != null ]; then + kill $CONFIG_PID 2>/dev/null || true + fi + if [ $MESSAGING_PID != null ]; then + kill $MESSAGING_PID 2>/dev/null || true + fi + if [ $API_PID != null ]; then + kill $API_PID 2>/dev/null || true + fi + if [ $PM_PID != null ]; then + kill $PM_PID 2>/dev/null || true + fi +} + +trap cleanup SIGINT SIGTERM + +# Extract TAK Server if not already done +if [[ ! -d "${TR}" ]] || [[ ! -f "${TR}/takserver.war" ]]; then + echo "Extracting TAK Server..." + + # Find the release zip file + RELEASE_FILE=$(find /takserver-release -name "takserver-docker-*.zip" | head -1) + + if [[ -z "$RELEASE_FILE" ]]; then + echo "ERROR: No TAK Server release file found in /takserver-release" + exit 1 + fi + + echo "Found release file: $RELEASE_FILE" + + # Extract the release file + unzip -q "$RELEASE_FILE" -d /tmp/takserver_extract + + # Find the extracted directory + EXTRACTED_DIR=$(find /tmp/takserver_extract -name "takserver-docker-*" -type d | head -1) + + if [[ -z "$EXTRACTED_DIR" ]]; then + echo "ERROR: Could not find extracted TAK Server directory" + exit 1 + fi + + echo "Copying TAK Server files..." + + # Create base directory + mkdir -p "${TR}" + + # Copy all files from the extracted directory to tak directory + cp -r "${EXTRACTED_DIR}/tak"/* "${TR}/" + + # Make scripts executable + find "${TR}" -name "*.sh" -exec chmod +x {} \; + + # Copy our custom files + cp /opt/scripts/coreConfigEnvHelper.py "${TR}/coreConfigEnvHelper.py" + + # Clean up + rm -rf /tmp/takserver_extract + + echo "TAK Server extraction complete!" +fi + +# Validate required environment variables +check_env_var POSTGRES_PASSWORD "database connection" +check_env_var CA_NAME "Certificate Authority Name" +check_env_var CA_PASS "Certificate Authority Password" +check_env_var STATE "Certificate Authority generation" +check_env_var CITY "Certificate Authority generation" +check_env_var ORGANIZATION "Certificate Authority generation" +check_env_var ORGANIZATIONAL_UNIT "Certificate Authority generation" +check_env_var ADMIN_CERT_NAME "TAK Server management certificate" +check_env_var ADMIN_CERT_PASS "TAK Server management certificate password" +check_env_var TAKSERVER_CERT_PASS "TAK Server instance certificate password" + +# Initialize data directories +mkdir -p "${TR}/data/logs" "${TR}/data/certs" + +# Seed initial certificate data if necessary +if [[ ! -d "${TR}/data/certs" ]] || [[ -z "$(ls -A "${TR}/data/certs")" ]]; then + echo "Copying initial certificate configuration..." + cp -R ${TR}/certs/* ${TR}/data/certs/ +else + echo "Using existing certificates." +fi + +# Move original certificate data and symlink to certificate data in data dir +if [[ -d "${TR}/certs" ]] && [[ ! -L "${TR}/certs" ]]; then + mv ${TR}/certs ${TR}/certs.orig + ln -s "${TR}/data/certs" "${TR}/certs" +fi + +# Seed initial CoreConfig.xml if necessary +if [[ ! -f "${CONFIG}" ]]; then + echo "Copying initial CoreConfig.xml..." + if [[ -f "${TR}/CoreConfig.xml" ]]; then + cp ${TR}/CoreConfig.xml ${CONFIG} + mv ${TR}/CoreConfig.xml ${TR}/CoreConfig.xml.orig + else + cp ${TR}/CoreConfig.example.xml ${CONFIG} + fi +else + echo "Using existing CoreConfig.xml." +fi + +# Seed initial TAKIgniteConfig.xml if necessary +if [[ ! -f "${TAKIGNITECONFIG}" ]]; then + echo "Copying initial TAKIgniteConfig.xml..." + if [[ -f "${TR}/TAKIgniteConfig.xml" ]]; then + cp ${TR}/TAKIgniteConfig.xml ${TAKIGNITECONFIG} + mv ${TR}/TAKIgniteConfig.xml ${TR}/TAKIgniteConfig.xml.orig + else + cp ${TR}/TAKIgniteConfig.example.xml ${TAKIGNITECONFIG} + fi +else + echo "Using existing TAKIgniteConfig.xml." +fi + +# Symlink the log directory +if [[ ! -L "${TR}/logs" ]]; then + ln -sf "${TR}/data/logs" "${TR}/logs" +fi + +cd ${CR} + +# Generate certificates if needed +if [[ ! -f "${CR}/files/root-ca.pem" ]]; then + echo "Generating root CA certificate..." + CAPASS=${CA_PASS} bash /opt/tak/certs/makeRootCa.sh --ca-name "${CA_NAME}" +else + echo "Using existing root CA." +fi + +if [[ ! -f "${CR}/files/intermediate-signing.jks" ]]; then + echo "Making new signing certificate..." + export CAPASS=${CA_PASS} + yes | /opt/tak/certs/makeCert.sh ca intermediate +else + echo "Using existing intermediate CA certificate." +fi + +if [[ ! -f "${CR}/files/takserver.pem" ]]; then + echo "Generating TAK Server certificate..." + CAPASS=${CA_PASS} PASS="${TAKSERVER_CERT_PASS}" bash /opt/tak/certs/makeCert.sh server takserver +else + echo "Using existing takserver certificate." +fi + +if [[ ! -f "${CR}/files/${ADMIN_CERT_NAME}.pem" ]]; then + echo "Generating admin certificate..." + CAPASS=${CA_PASS} PASS="${ADMIN_CERT_PASS}" bash /opt/tak/certs/makeCert.sh client "${ADMIN_CERT_NAME}" +else + echo "Using existing ${ADMIN_CERT_NAME} certificate." +fi + +# Set permissions +chmod -R 755 ${TR}/data/ + +# Configure CoreConfig.xml with environment variables +echo "Configuring CoreConfig.xml..." +python3 ${TR}/coreConfigEnvHelper.py "${CONFIG}" "${CONFIG}" + +# Wait for database to be ready +echo "Waiting for database to be ready..." +until nc -z $(echo $POSTGRES_URL | sed 's/.*:\/\/\([^:]*\):.*/\1/') $(echo $POSTGRES_URL | sed 's/.*:\([0-9]*\)\/.*/\1/'); do + echo "Waiting for database connection..." + sleep 5 +done + +# Initialize database schema +echo "Initializing database schema..." +java -jar ${TR}/db-utils/SchemaManager.jar -url ${POSTGRES_URL} -user ${POSTGRES_USER} -password ${POSTGRES_PASSWORD} upgrade + +cd ${TR} + +# Source environment +. ./setenv.sh + +echo "Starting TAK Server services..." + +# Start services in background +echo "Starting Config service..." +java -jar -Xmx${CONFIG_MAX_HEAP}m -Dspring.profiles.active=config takserver.war & +CONFIG_PID=$! + +echo "Starting Messaging service..." +java -jar -Xmx${MESSAGING_MAX_HEAP}m -Dspring.profiles.active=messaging takserver.war & +MESSAGING_PID=$! + +echo "Starting API service..." +java -jar -Xmx${API_MAX_HEAP}m -Dspring.profiles.active=api -Dkeystore.pkcs12.legacy takserver.war & +API_PID=$! + +echo "Starting Plugin Manager service..." +java -jar -Xmx${PLUGIN_MANAGER_MAX_HEAP}m -Dloader.path=WEB-INF/lib-provided,WEB-INF/lib,WEB-INF/classes,file:lib/ takserver-pm.jar & +PM_PID=$! + +# Wait for services to start +echo "Waiting for services to start..." +sleep 60 + +# Add admin user +echo "Adding admin user..." +TAKCL_CORECONFIG_PATH="${CONFIG}" +TAKCL_TAKIGNITECONFIG_PATH="${TAKIGNITECONFIG}" +java -jar /opt/tak/utils/UserManager.jar certmod -A "/opt/tak/certs/files/${ADMIN_CERT_NAME}.pem" + +echo "TAK Server is ready!" +echo "Admin user '${ADMIN_CERT_NAME}' has been added." + +# Wait for plugin manager to complete (this keeps the container running) +wait $PM_PID