diff --git a/.github/workflows/database-upgrades.yml b/.github/workflows/database-upgrades.yml new file mode 100644 index 0000000000..917bd219b4 --- /dev/null +++ b/.github/workflows/database-upgrades.yml @@ -0,0 +1,130 @@ +name: Openfire Database Upgrade Tests + +env: + CI: true + STARTER_VERSION: 21 # The version of the initial DB being installed, as per the ofVersion table + +on: + push: + branches: [ master ] + paths: + - 'distribution/src/database/**' + - 'build/ci/**' + - '.github/workflows/database-upgrades.yml' + - 'xmppserver/pom.xml' + pull_request: + paths: + - 'distribution/src/database/**' + - 'build/ci/**' + - '.github/workflows/database-upgrades.yml' + - 'xmppserver/pom.xml' + + + +jobs: + sqlserver: + name: Test SQL Server Upgrades + runs-on: ubuntu-latest + steps: + - name: Checkout Openfire + uses: actions/checkout@v2 + - name: Set environment variables + run: | + echo "CONNECTION_STRING=jdbc:sqlserver://localhost:1433;databaseName=openfire;applicationName=Openfire" >> $GITHUB_ENV + echo "CONNECTION_DRIVER=com.microsoft.sqlserver.jdbc.SQLServerDriver" >> $GITHUB_ENV + echo "CONNECTION_USERNAME=sa" >> $GITHUB_ENV + echo "CONNECTION_PASSWORD=SecurePa55w0rd" >> $GITHUB_ENV + OPENFIREVSN=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "OPENFIREVSN=$OPENFIREVSN" >> $GITHUB_ENV + - name: Download old Openfire database script + run: | + mkdir olddb + curl https://raw.githubusercontent.com/igniterealtime/Openfire/v3.9.3/src/database/openfire_sqlserver.sql > $GITHUB_WORKSPACE/olddb/openfire_sqlserver.sql + - name: Start database server and install database + run: docker-compose -f ./build/ci/compose/mssql.yml up --detach + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build openfire + run: mvn install + - name: Build & run update tester + run: | + pushd ./build/ci/updater + mvn package + java -jar ./target/updaterunner-1.0.0-jar-with-dependencies.jar + + + postgres: + name: Test Postgres Upgrades + runs-on: ubuntu-latest + steps: + - name: Checkout Openfire + uses: actions/checkout@v2 + - name: Set environment variables + run: | + echo "CONNECTION_STRING=jdbc:postgresql://localhost:5432/openfire" >> $GITHUB_ENV + echo "CONNECTION_DRIVER=org.postgresql.Driver" >> $GITHUB_ENV + echo "CONNECTION_USERNAME=openfire" >> $GITHUB_ENV + echo "CONNECTION_PASSWORD=SecurePa55w0rd" >> $GITHUB_ENV + OPENFIREVSN=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "OPENFIREVSN=$OPENFIREVSN" >> $GITHUB_ENV + - name: Download old Openfire database script + run: | + mkdir olddb + curl https://raw.githubusercontent.com/igniterealtime/Openfire/v3.9.3/src/database/openfire_postgresql.sql > $GITHUB_WORKSPACE/olddb/openfire_postgresql.sql + - name: Start database server and install database + run: docker-compose -f ./build/ci/compose/postgresql.yml up --detach + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build openfire + run: mvn install + - name: Build & run update tester + run: | + pushd ./build/ci/updater + mvn package + java -jar ./target/updaterunner-1.0.0-jar-with-dependencies.jar + + + mysql: + name: Test MySQL Upgrades + runs-on: ubuntu-latest + steps: + - name: Checkout Openfire + uses: actions/checkout@v2 + - name: Set environment variables + run: | + echo "CONNECTION_STRING=jdbc:mysql://localhost:3306/openfire?rewriteBatchedStatements=true&characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC" >> $GITHUB_ENV + echo "CONNECTION_DRIVER=com.mysql.cj.jdbc.Driver" >> $GITHUB_ENV + echo "CONNECTION_USERNAME=root" >> $GITHUB_ENV + echo "CONNECTION_PASSWORD=SecurePa55w0rd" >> $GITHUB_ENV + OPENFIREVSN=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "OPENFIREVSN=$OPENFIREVSN" >> $GITHUB_ENV + - name: Download old Openfire database script + run: | + mkdir olddb + curl https://raw.githubusercontent.com/igniterealtime/Openfire/v3.9.3/src/database/openfire_mysql.sql > $GITHUB_WORKSPACE/olddb/openfire_mysql.sql + - name: Start database server and install database + run: docker-compose -f ./build/ci/compose/mysql.yml up --detach + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build openfire + run: mvn install + - name: Build & run update tester + run: | + pushd ./build/ci/updater + mvn package + java -jar ./target/updaterunner-1.0.0-jar-with-dependencies.jar diff --git a/build/ci/compose/mssql.yml b/build/ci/compose/mssql.yml new file mode 100644 index 0000000000..2e1e782b0b --- /dev/null +++ b/build/ci/compose/mssql.yml @@ -0,0 +1,14 @@ +version: '3.7' + +services: + db: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - "1433:1433" + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=${CONNECTION_PASSWORD} + volumes: + - ${GITHUB_WORKSPACE}/olddb:/openfiredb:ro + - ./scripts:/scripts:ro + entrypoint: [ "/bin/bash", "-c", "/scripts/mssql.sh" ] diff --git a/build/ci/compose/mysql.yml b/build/ci/compose/mysql.yml new file mode 100644 index 0000000000..91ccc81982 --- /dev/null +++ b/build/ci/compose/mysql.yml @@ -0,0 +1,12 @@ +version: '3.7' + +services: + db: + image: library/mysql:5.7 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=${CONNECTION_PASSWORD} + - MYSQL_DATABASE=openfire + volumes: + - ${GITHUB_WORKSPACE}/olddb:/docker-entrypoint-initdb.d diff --git a/build/ci/compose/postgresql.yml b/build/ci/compose/postgresql.yml new file mode 100644 index 0000000000..52529e898b --- /dev/null +++ b/build/ci/compose/postgresql.yml @@ -0,0 +1,13 @@ +version: '3.7' + +services: + db: + image: library/postgres:9.6.17-alpine + ports: + - "5432:5432" + environment: + - POSTGRES_DB=openfire + - POSTGRES_USER=${CONNECTION_USERNAME} + - POSTGRES_PASSWORD=${CONNECTION_PASSWORD} + volumes: + - ${GITHUB_WORKSPACE}/olddb:/docker-entrypoint-initdb.d diff --git a/build/ci/compose/scripts/mssql.sh b/build/ci/compose/scripts/mssql.sh new file mode 100755 index 0000000000..b318d96c1d --- /dev/null +++ b/build/ci/compose/scripts/mssql.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +/opt/mssql/bin/sqlservr & +/scripts/wait-for-it.sh 127.0.0.1:1433 + +for i in {1..50}; +do + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "CREATE DATABASE openfire;" + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d openfire -i /openfiredb/openfire_sqlserver.sql + if [ $? -eq 0 ] + then + echo "openfire sql import" + break + else + echo "not ready yet..." + sleep 1 + fi +done + +sleep infinity # Keep the container running forever diff --git a/build/ci/compose/scripts/wait-for-it.sh b/build/ci/compose/scripts/wait-for-it.sh new file mode 100755 index 0000000000..44768829d8 --- /dev/null +++ b/build/ci/compose/scripts/wait-for-it.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +#Source: https://github.com/vishnubob/wait-for-it/blob/81b1373f17855a4dc21156cfe1694c31d7d1792e/wait-for-it.sh + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/build/ci/updater/README.md b/build/ci/updater/README.md new file mode 100644 index 0000000000..e55078a965 --- /dev/null +++ b/build/ci/updater/README.md @@ -0,0 +1,12 @@ +# Update Runner + +Uses Openfire's update strategy to test running updates. + +Designed to be used on test databases to prove that recent updates can & will apply. + +## Build + +* From the root, `mvn install` Openfire +* Set up properties for the Update Runner by executing `export OPENFIREVSN=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)` +* Run the Update Runner with `java -jar ./target/updaterunner-1.0.0-jar-with-dependencies.jar` + diff --git a/build/ci/updater/pom.xml b/build/ci/updater/pom.xml new file mode 100644 index 0000000000..7533d7cdb6 --- /dev/null +++ b/build/ci/updater/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + com.igniterealtime.openfire + updaterunner + 1.0.0 + jar + + ${project.groupId}:${project.artifactId} + Standalone runner of schema updates for Openfire + + + com.igniterealtime.openfire.updaterunner.Main + 1.8 + 1.8 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + + + com.igniterealtime.openfire.updaterunner.Main + + + + + jar-with-dependencies + + + + + + + org.apache.felix + maven-bundle-plugin + 4.2.0 + true + + + + + + + org.igniterealtime.openfire + xmppserver + ${env.OPENFIREVSN} + + + org.hsqldb + hsqldb + 2.4.1 + + + mysql + mysql-connector-java + 8.0.21 + + + org.postgresql + postgresql + 42.2.14 + + + net.sourceforge.jtds + jtds + 1.3.1 + + + com.microsoft.sqlserver + mssql-jdbc + 7.2.2.jre8 + + + + + + atlassian-public + https://maven.atlassian.com/repository/public + + + ej-technologies + https://maven.ej-technologies.com/repository + + + + diff --git a/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/Main.java b/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/Main.java new file mode 100644 index 0000000000..f9d58047b5 --- /dev/null +++ b/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/Main.java @@ -0,0 +1,61 @@ +package com.igniterealtime.openfire.updaterunner; + +import org.jivesoftware.database.*; +import org.jivesoftware.util.JiveGlobals; + +import java.io.File; +import java.sql.Connection; + +public class Main { + + public static void main(String[] args) { + + String connectionString = System.getenv("CONNECTION_STRING"); + String connectionDriver = System.getenv("CONNECTION_DRIVER"); + String connectionUsername = System.getenv("CONNECTION_USERNAME"); + String connectionPassword = System.getenv("CONNECTION_PASSWORD"); + + /* + DATABASE DRIVERS AND EXAMPLE QUERY STRINGS + "MySQL","com.mysql.cj.jdbc.Driver","jdbc:mysql://HOSTNAME:3306/DATABASENAME?rewriteBatchedStatements=true&characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC" + "Oracle","oracle.jdbc.driver.OracleDriver","jdbc:oracle:thin:@HOSTNAME:1521:SID" + "Microsoft SQL Server (legacy)","net.sourceforge.jtds.jdbc.Driver","jdbc:jtds:sqlserver://HOSTNAME/DATABASENAME;appName=Openfire" + "PostgreSQL","org.postgresql.Driver","jdbc:postgresql://HOSTNAME:5432/DATABASENAME" + "IBM DB2","com.ibm.db2.jcc.DB2Driver","jdbc:db2://HOSTNAME:50000/DATABASENAME" + "Microsoft SQL Server","com.microsoft.sqlserver.jdbc.SQLServerDriver","jdbc:sqlserver://HOSTNAME:1433;databaseName=DATABASENAME;applicationName=Openfire" + */ + + try { + String parent = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getParent(); + PropertiesReader reader = new PropertiesReader(parent + "/maven-archiver/pom.properties"); + + String distributionDir = new File(parent).toPath().resolve("../../../../distribution/target/distribution-base").toFile().getCanonicalPath(); + JiveGlobals.setHomeDirectory(distributionDir); + + JiveGlobals.setXMLProperty("connectionProvider.className", "org.jivesoftware.database.DefaultConnectionProvider"); + JiveGlobals.setXMLProperty("database.defaultProvider.driver", connectionDriver); + JiveGlobals.setXMLProperty("database.defaultProvider.serverURL", connectionString); + JiveGlobals.setXMLProperty("database.defaultProvider.username", connectionUsername); + JiveGlobals.setXMLProperty("database.defaultProvider.password", connectionPassword); + JiveGlobals.setXMLProperty("database.defaultProvider.testSQL", DbConnectionManager.getTestSQL(connectionDriver)); + + //Try connecting + DefaultConnectionProvider cp = new DefaultConnectionProvider(); + cp.start(); + DbConnectionManager.setConnectionProvider(cp); + Connection conn = DbConnectionManager.getConnection(); + + //Try updating explicitly. It'll already have happened once as a byproduct of the above, but we need to trap the result + SchemaManager sm = new SchemaManager(); + boolean fixed = sm.checkOpenfireSchema(conn); + if(!fixed){ + throw new Exception("Failed to upgrade database"); + } + } catch (Exception e) { + System.out.println(e.getClass() + "\n" + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + +} diff --git a/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/PropertiesReader.java b/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/PropertiesReader.java new file mode 100644 index 0000000000..1521ffda68 --- /dev/null +++ b/build/ci/updater/src/main/java/com/igniterealtime/openfire/updaterunner/PropertiesReader.java @@ -0,0 +1,29 @@ +package com.igniterealtime.openfire.updaterunner; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Properties; + +public class PropertiesReader { + private Properties properties; + + public PropertiesReader(String propertyFileName) throws IOException, URISyntaxException { + //String propFilePath = new File(parent, propertyFileName).getAbsolutePath(); + System.out.println(propertyFileName); + File f = new File(propertyFileName); + if(!f.exists()){ + throw new IOException("Property file doesn't exist!"); + } + FileInputStream is = new FileInputStream(propertyFileName); + this.properties = new Properties(); + this.properties.load(is); + } + + public String getProperty(String propertyName) { + + return this.properties.getProperty(propertyName); + } +}