diff --git a/.dockerignore b/.dockerignore index d171944d877..c94ff3a0bb0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,15 @@ -./* -!docker-entrypoint.sh - +# Remote targets do not read from the build context. Local targets accept only +# the runtime files emitted by the supported Gradle distribution plus the +# Mainnet configuration staged by docker.sh. Everything else stays excluded. +** +!java-tron/ +java-tron/** +!java-tron/bin/ +java-tron/bin/** +!java-tron/bin/FullNode +!java-tron/bin/FullNode.bat +!java-tron/bin/java-tron.vmoptions +!java-tron/lib/ +java-tron/lib/** +!java-tron/lib/*.jar +!java-tron/config.conf diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000000..a5055ea9648 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,517 @@ +name: Docker CI + +on: + pull_request: + branches: [master, develop, 'release_**'] + types: [opened, synchronize, reopened] + push: + branches: [master, develop] + paths: + - '.dockerignore' + - 'docker/Dockerfile' + - 'docker/arm64/Dockerfile' + - 'docker/.dockerignore' + - 'docker/docker.sh' + - 'docker/tests/**' + - 'build.gradle' + - 'settings.gradle' + - 'gradle.properties' + - 'framework/build.gradle' + - 'framework/src/main/resources/config.conf' + - 'gradle/unixStartScript.txt' + - 'gradle/java-tron.vmoptions' + - 'gradle/jdk17/java-tron.vmoptions' + - 'gradle/wrapper/**' + - '.github/workflows/docker.yml' + schedule: + - cron: '0 3 * * 1' + workflow_dispatch: + inputs: + source_mode: + description: Source mode for full image builds + required: true + default: local + type: choice + options: + - local + - remote + +permissions: + contents: read + +concurrency: + group: docker-${{ github.workflow }}-${{ github.event_name == 'schedule' && 'schedule' || github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + changes: + name: Analyze Docker changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + fast: ${{ steps.select.outputs.fast }} + amd64: ${{ steps.select.outputs.amd64 }} + arm64: ${{ steps.select.outputs.arm64 }} + source_mode: ${{ steps.select.outputs.source_mode }} + remote_amd64: ${{ steps.select.outputs.remote_amd64 }} + remote_arm64: ${{ steps.select.outputs.remote_arm64 }} + remote_source_ref: ${{ steps.select.outputs.remote_source_ref }} + steps: + - uses: actions/checkout@v5 + with: + # Pull requests check out the synthetic merge commit. Depth 2 is + # enough to diff that commit against its first parent (the base). + # Other events retain full history. + fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }} + + - name: Select required checks + id: select + env: + EVENT_NAME: ${{ github.event_name }} + HEAD_SHA: ${{ github.sha }} + BEFORE_SHA: ${{ github.event.before }} + DISPATCH_SOURCE_MODE: ${{ inputs.source_mode }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + + fast=false + amd64=false + arm64=false + source_mode=local + remote_amd64=false + remote_arm64=false + remote_source_ref=master + changed_files=$(mktemp) + trap 'rm -f "$changed_files"' EXIT + + case "$EVENT_NAME" in + schedule) + fast=true + amd64=true + arm64=true + source_mode=remote + remote_source_ref=master + ;; + workflow_dispatch) + fast=true + amd64=true + arm64=true + source_mode="${DISPATCH_SOURCE_MODE:-local}" + remote_source_ref="$REF_NAME" + ;; + pull_request) + remote_source_ref=master + if ! git rev-parse -q --verify HEAD^1 >/dev/null; then + echo "Pull request merge commit is missing its base parent." >&2 + exit 1 + fi + git diff --name-only HEAD^1 HEAD -- > "$changed_files" + ;; + push) + remote_source_ref=master + if [[ -z "$BEFORE_SHA" || "$BEFORE_SHA" =~ ^0+$ ]]; then + git diff-tree --no-commit-id --name-only -r "$HEAD_SHA" > "$changed_files" + else + git diff --name-only "$BEFORE_SHA" "$HEAD_SHA" -- > "$changed_files" + fi + ;; + *) + echo "Unsupported event: $EVENT_NAME" >&2 + exit 1 + ;; + esac + + while IFS= read -r path; do + case "$path" in + docker/tests/vmoptions-test.sh|docker/tests/docker-sh-run-smoke.sh|docker/tests/verify-runtime-image.sh) + fast=true + amd64=true + arm64=true + ;; + # Keep docker.sh-only PRs fast: mock tests cover helper behavior. + # Real architecture smoke runs for image-affecting changes and + # in the scheduled weekly build. + docker/docker.sh|docker/tests/*) + fast=true + ;; + docker/Dockerfile) + fast=true + amd64=true + remote_amd64=true + ;; + gradle/java-tron.vmoptions) + fast=true + amd64=true + ;; + docker/arm64/Dockerfile) + fast=true + arm64=true + remote_arm64=true + ;; + gradle/jdk17/java-tron.vmoptions) + fast=true + arm64=true + ;; + docker/.dockerignore|.github/workflows/docker.yml) + fast=true + amd64=true + arm64=true + remote_amd64=true + remote_arm64=true + ;; + .dockerignore|build.gradle|settings.gradle|gradle.properties|framework/build.gradle|framework/src/main/resources/config.conf|gradle/unixStartScript.txt|gradle/wrapper/*) + fast=true + amd64=true + arm64=true + ;; + esac + done < "$changed_files" + + echo "fast=$fast" >> "$GITHUB_OUTPUT" + echo "amd64=$amd64" >> "$GITHUB_OUTPUT" + echo "arm64=$arm64" >> "$GITHUB_OUTPUT" + echo "source_mode=$source_mode" >> "$GITHUB_OUTPUT" + echo "remote_amd64=$remote_amd64" >> "$GITHUB_OUTPUT" + echo "remote_arm64=$remote_arm64" >> "$GITHUB_OUTPUT" + echo "remote_source_ref=$remote_source_ref" >> "$GITHUB_OUTPUT" + + { + echo "### Docker CI selection" + echo "" + echo "- Fast checks: $fast" + echo "- Build amd64: $amd64" + echo "- Build arm64: $arm64" + echo "- Source mode: $source_mode" + echo "- Additional remote amd64 build: $remote_amd64" + echo "- Additional remote arm64 build: $remote_arm64" + echo "- Remote source ref: $remote_source_ref" + if [ -s "$changed_files" ]; then + echo "" + echo "Changed files:" + sed 's/^/- `/' "$changed_files" | sed 's/$/`/' + fi + } >> "$GITHUB_STEP_SUMMARY" + + script-check: + name: Docker script and definition checks + needs: changes + if: needs.changes.outputs.fast == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - name: Validate shell scripts + run: | + bash -n docker/docker.sh + bash -n docker/tests/docker-sh-test.sh + bash -n docker/tests/docker-lifecycle-test.sh + bash -n docker/tests/docker-sh-run-smoke.sh + bash -n docker/tests/dockerignore-test.sh + bash -n docker/tests/docker-workflow-test.sh + bash -n docker/tests/vmoptions-test.sh + bash -n docker/tests/verify-runtime-image.sh + bash -n docker/tests/unix-start-script-test.sh + shellcheck docker/docker.sh docker/tests/docker-sh-test.sh docker/tests/docker-lifecycle-test.sh docker/tests/docker-sh-run-smoke.sh docker/tests/dockerignore-test.sh docker/tests/docker-workflow-test.sh docker/tests/vmoptions-test.sh docker/tests/verify-runtime-image.sh docker/tests/unix-start-script-test.sh + + - name: Test shell helpers + run: | + bash docker/tests/docker-sh-test.sh + bash docker/tests/docker-lifecycle-test.sh + bash docker/tests/dockerignore-test.sh + bash docker/tests/docker-workflow-test.sh + bash docker/tests/unix-start-script-test.sh + + - name: Check Dockerfiles with distribution context + run: | + set -euo pipefail + context=$(mktemp -d) + trap 'rm -rf "$context"' EXIT + mkdir -p "$context/java-tron/bin" "$context/java-tron/lib" + touch "$context/java-tron/bin/FullNode" + touch "$context/java-tron/bin/java-tron.vmoptions" + touch "$context/java-tron/config.conf" + + cp docker/Dockerfile "$context/Dockerfile" + DOCKER_BUILDKIT=1 docker build --check --target local --file "$context/Dockerfile" "$context" + + cp docker/arm64/Dockerfile "$context/Dockerfile" + DOCKER_BUILDKIT=1 docker build --check --target local --file "$context/Dockerfile" "$context" + + - name: Check Dockerfiles with remote context + run: | + DOCKER_BUILDKIT=1 docker build --check --target remote --file "$PWD/docker/Dockerfile" docker + DOCKER_BUILDKIT=1 docker build --check --target remote --file "$PWD/docker/arm64/Dockerfile" docker + + build-amd64: + name: Build Docker image (amd64) + needs: changes + if: needs.changes.outputs.amd64 == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + IMAGE: java-tron:ci-amd64 + SOURCE_MODE: ${{ needs.changes.outputs.source_mode }} + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set up JDK 8 for local distribution + if: env.SOURCE_MODE == 'local' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '8' + cache: gradle + + - name: Prepare local build context + if: env.SOURCE_MODE == 'local' + run: | + set -euo pipefail + umask 077 + bash docker/docker.sh --build --source local \ + --export-context "$RUNNER_TEMP/java-tron-local-context" + + - name: Build local image + if: env.SOURCE_MODE == 'local' + uses: docker/build-push-action@v7 + with: + context: ${{ runner.temp }}/java-tron-local-context + target: local + load: true + tags: ${{ env.IMAGE }} + cache-from: type=gha,scope=java-tron-amd64-local + cache-to: type=gha,mode=max,scope=java-tron-amd64-local,ignore-error=true + + - name: Build remote image + if: env.SOURCE_MODE == 'remote' + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/Dockerfile + target: remote + pull: true + no-cache-filters: remote-builder + load: true + tags: ${{ env.IMAGE }} + build-args: | + SOURCE_REPOSITORY=${{ github.server_url }}/${{ github.repository }}.git + SOURCE_REF=${{ needs.changes.outputs.remote_source_ref }} + cache-from: type=gha,scope=java-tron-amd64-remote + cache-to: type=gha,mode=max,scope=java-tron-amd64-remote,ignore-error=true + + - name: Verify runtime image + run: bash docker/tests/verify-runtime-image.sh "$IMAGE" 'version "1\.8' + + - name: Validate startup script and VM options + run: docker run --rm --env JAVA_OPTS=-version "$IMAGE" + + - name: Test JVM options file parsing + run: bash docker/tests/vmoptions-test.sh "$IMAGE" + + - name: Smoke docker.sh --run + run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE" + + build-arm64: + name: Build Docker image (arm64) + needs: changes + if: needs.changes.outputs.arm64 == 'true' + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + env: + IMAGE: java-tron:ci-arm64 + SOURCE_MODE: ${{ needs.changes.outputs.source_mode }} + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set up JDK 17 for local distribution + if: env.SOURCE_MODE == 'local' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Prepare local build context + if: env.SOURCE_MODE == 'local' + run: | + set -euo pipefail + umask 077 + bash docker/docker.sh --build --source local \ + --export-context "$RUNNER_TEMP/java-tron-local-context" + + - name: Build local image + if: env.SOURCE_MODE == 'local' + uses: docker/build-push-action@v7 + with: + context: ${{ runner.temp }}/java-tron-local-context + target: local + load: true + tags: ${{ env.IMAGE }} + cache-from: type=gha,scope=java-tron-arm64-local + cache-to: type=gha,mode=max,scope=java-tron-arm64-local,ignore-error=true + + - name: Build remote image + if: env.SOURCE_MODE == 'remote' + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/arm64/Dockerfile + target: remote + pull: true + no-cache-filters: remote-builder + load: true + tags: ${{ env.IMAGE }} + build-args: | + SOURCE_REPOSITORY=${{ github.server_url }}/${{ github.repository }}.git + SOURCE_REF=${{ needs.changes.outputs.remote_source_ref }} + cache-from: type=gha,scope=java-tron-arm64-remote + cache-to: type=gha,mode=max,scope=java-tron-arm64-remote,ignore-error=true + + - name: Verify runtime image + run: bash docker/tests/verify-runtime-image.sh "$IMAGE" 'version "17\.' + + - name: Validate startup script and VM options + run: docker run --rm --env JAVA_OPTS=-version "$IMAGE" + + - name: Test JVM options file parsing + run: bash docker/tests/vmoptions-test.sh "$IMAGE" + + - name: Smoke docker.sh --run + run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE" + + remote-build-amd64: + name: Build remote Docker image (amd64) + needs: changes + if: needs.changes.outputs.remote_amd64 == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + IMAGE: java-tron:ci-remote-amd64 + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build remote image + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/Dockerfile + target: remote + pull: true + no-cache-filters: remote-builder + load: true + tags: ${{ env.IMAGE }} + build-args: | + SOURCE_REPOSITORY=${{ github.server_url }}/${{ github.repository }}.git + SOURCE_REF=${{ needs.changes.outputs.remote_source_ref }} + cache-from: type=gha,scope=java-tron-amd64-remote + cache-to: type=gha,mode=max,scope=java-tron-amd64-remote,ignore-error=true + + - name: Verify remote runtime image + run: bash docker/tests/verify-runtime-image.sh "$IMAGE" 'version "1\.8' + + - name: Validate remote startup script and VM options + run: docker run --rm --env JAVA_OPTS=-version "$IMAGE" + + - name: Test remote JVM options file parsing + run: bash docker/tests/vmoptions-test.sh "$IMAGE" + + - name: Smoke remote docker.sh --run + run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE" + + remote-build-arm64: + name: Build remote Docker image (arm64) + needs: changes + if: needs.changes.outputs.remote_arm64 == 'true' + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + env: + IMAGE: java-tron:ci-remote-arm64 + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build remote image + uses: docker/build-push-action@v7 + with: + context: docker + file: docker/arm64/Dockerfile + target: remote + pull: true + no-cache-filters: remote-builder + load: true + tags: ${{ env.IMAGE }} + build-args: | + SOURCE_REPOSITORY=${{ github.server_url }}/${{ github.repository }}.git + SOURCE_REF=${{ needs.changes.outputs.remote_source_ref }} + cache-from: type=gha,scope=java-tron-arm64-remote + cache-to: type=gha,mode=max,scope=java-tron-arm64-remote,ignore-error=true + + - name: Verify remote runtime image + run: bash docker/tests/verify-runtime-image.sh "$IMAGE" 'version "17\.' + + - name: Validate remote startup script and VM options + run: docker run --rm --env JAVA_OPTS=-version "$IMAGE" + + - name: Test remote JVM options file parsing + run: bash docker/tests/vmoptions-test.sh "$IMAGE" + + - name: Smoke remote docker.sh --run + run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE" + + gate: + name: Docker CI Gate + needs: [changes, script-check, build-amd64, build-arm64, remote-build-amd64, remote-build-arm64] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + CHANGES_RESULT: ${{ needs.changes.result }} + FAST_REQUIRED: ${{ needs.changes.outputs.fast }} + FAST_RESULT: ${{ needs.script-check.result }} + AMD64_REQUIRED: ${{ needs.changes.outputs.amd64 }} + AMD64_RESULT: ${{ needs.build-amd64.result }} + ARM64_REQUIRED: ${{ needs.changes.outputs.arm64 }} + ARM64_RESULT: ${{ needs.build-arm64.result }} + REMOTE_AMD64_REQUIRED: ${{ needs.changes.outputs.remote_amd64 }} + REMOTE_AMD64_RESULT: ${{ needs.remote-build-amd64.result }} + REMOTE_ARM64_REQUIRED: ${{ needs.changes.outputs.remote_arm64 }} + REMOTE_ARM64_RESULT: ${{ needs.remote-build-arm64.result }} + steps: + - name: Require selected checks + run: | + set -euo pipefail + + if [ "$CHANGES_RESULT" != success ]; then + echo "Docker change analysis did not succeed: $CHANGES_RESULT" >&2 + exit 1 + fi + + require_success() { + local required="$1" + local result="$2" + local name="$3" + if [ "$required" = true ] && [ "$result" != success ]; then + echo "$name was required but finished with result: $result" >&2 + exit 1 + fi + } + + require_success "$FAST_REQUIRED" "$FAST_RESULT" "Docker script checks" + require_success "$AMD64_REQUIRED" "$AMD64_RESULT" "amd64 image build" + require_success "$ARM64_REQUIRED" "$ARM64_RESULT" "arm64 image build" + require_success "$REMOTE_AMD64_REQUIRED" "$REMOTE_AMD64_RESULT" "amd64 remote image build" + require_success "$REMOTE_ARM64_REQUIRED" "$REMOTE_ARM64_RESULT" "arm64 remote image build" + + echo "All selected Docker CI checks succeeded." diff --git a/.gitignore b/.gitignore index 3917bb44679..c950d859f78 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,11 @@ src/main/resources/META-INF/ # output directory /output-directory/ +/docker/output-directory/ + +# Private-network configuration downloaded by docker.sh +/config/private_net_config.conf +/docker/config/private_net_config.conf /output_manager/ /output_witness/ diff --git a/README.md b/README.md index edf99c4df92..491f9a2dadd 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ ## Table of Contents +- [Quick Start](quickstart.md) - [What’s TRON?](#whats-tron) - [Building the Source Code](#building-the-source-code) - [Executables](#executables) @@ -82,6 +83,7 @@ The java-tron project comes with several runnable artifacts and helper scripts f | **`Toolkit.jar`** | Node management utility (generated in `build/libs/`): partition, prune, copy, convert DBs; shadow-fork tool. [Usage](https://tronprotocol.github.io/documentation-en/using_javatron/toolkit/#toolkit-a-java-tron-node-maintenance-suite) | | **`start.sh`** | Quick start script (x86_64, JDK 8) to download/build/run `FullNode.jar`. See the tool [guide](./shell.md). | | **`start.sh.simple`** | Quick start script template (ARM64, JDK 17). See usage notes inside the script. | +| **`docker/docker.sh`** | Bash helper for building or pulling an image and managing a single FullNode container. See the [Docker Shell Guide](docker/docker.md). | # Running java-tron @@ -134,11 +136,7 @@ tail -f ./logs/tron.log Use [TronScan](https://tronscan.org/#/), TRON's official block explorer, to view main network transactions, blocks, accounts, witness voting, and governance metrics, etc. ### 2. Join Nile test network -Utilize the `-c` flag to direct the node to the configuration file corresponding to the desired network. Since Nile Testnet may incorporate features not yet available on the Mainnet, it is **strongly advised** to compile the source code following the [Building the Source Code](https://github.com/tron-nile-testnet/nile-testnet/blob/master/README.md#building-the-source-code) instructions for the Nile Testnet. - -```bash -java -jar ./build/libs/FullNode.jar -c config-nile.conf -``` +Since Nile Testnet may incorporate features not yet available on the Mainnet, build and run a Nile node by following the [nile-testnet source-code instructions](https://github.com/tron-nile-testnet/nile-testnet/blob/master/README.md#building-the-source-code). The `docker/docker.sh` helper in this repository does not provide a Nile mode; container users should follow the maintained [`tron-docker` workflow](https://github.com/tronprotocol/tron-docker) instead. Nile resources: explorer, faucet, wallet, developer docs, and network statistics at [nileex.io](https://nileex.io/). @@ -154,14 +152,17 @@ To set up a private network for testing or development, follow the [Private Netw To operate the node as a Super Representative (SR), append the `--witness` parameter to the standard launch command. An SR node inherits every capability of a FullNode and additionally participates in block production. Refer to the [Super Representative documentation](https://tronprotocol.github.io/documentation-en/mechanism-algorithm/sr/) for eligibility requirements. -Fill in the private key of your SR account into the `localwitness` list in the configuration file. Here is an example: +For a production SR, store the block-signing key in an encrypted keystore and configure its path with `localwitnesskeystore`: +```hocon +localwitnesskeystore = [ + "localwitnesskeystore.json" +] ``` - localwitness = [ - - ] -``` -Check [Starting a Block Production Node](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#starting-a-block-production-node) for more details. + +Do not pass secrets with `--private-key` or `--password`. Command-line arguments may be exposed through process listings and shell history; with Docker, they are also retained in container metadata. Use the secret-management and password-delivery procedure selected for the production deployment instead. + +The plaintext `localwitness` setting remains available for compatibility, but should be limited to isolated test environments and must not be used for production signing keys. Check [Starting a Block Production Node](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#starting-a-block-production-node) for keystore creation and production deployment guidance. You could also test the process by connecting to a testnet or setting up a private network. ## Programmatically interfacing FullNode diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000000..4e261719dad --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,4 @@ +* +!Dockerfile +!arm64/ +!arm64/Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile index 2732f5a55ed..80779b0a1bf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,44 +1,147 @@ -FROM tronprotocol/centos7:0.2 +# syntax=docker/dockerfile:1 + +FROM ubuntu:24.04 AS java-base + +ENV NO_PROXY_CACHE="-o Acquire::BrokenProxy=true -o Acquire::http::No-Cache=true -o Acquire::http::Pipeline-Depth=0" +ENV OPENJDK8_URL="https://api.adoptium.net/v3/binary/latest/8/ga/linux/x64/jdk/hotspot/normal/eclipse" +ENV ADOPTIUM_SIGNING_FINGERPRINT="3B04D753C9050D9A5D343F39843C48A565F8F04B" +ENV JAVA_HOME="/usr/local/openjdk-8" +ENV CLASSPATH="$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar" +ENV PATH="$PATH:$JAVA_HOME/bin" + +RUN apt-get update $NO_PROXY_CACHE \ + && apt-get --quiet --yes install --no-install-recommends curl gnupg dirmngr ca-certificates \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* \ + && rm -rf /var/lib/apt/lists/* + +RUN cd /usr/local \ + && FETCH_URL="$(curl -fsS -w "%{redirect_url}" -o /dev/null "$OPENJDK8_URL")" \ + && JDK_TAR="$(curl -fsSL -w "%{filename_effective}" -O "$FETCH_URL")" \ + && curl -fsSLo "$JDK_TAR.sig" "$FETCH_URL.sig" \ + && GNUPGHOME="$(mktemp -d)" \ + && export GNUPGHOME \ + && gpg --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys "$ADOPTIUM_SIGNING_FINGERPRINT" \ + && gpg --batch --verify "$JDK_TAR.sig" "$JDK_TAR" \ + && rm -rf "$GNUPGHOME" "$JDK_TAR.sig" \ + && mkdir -p "$JAVA_HOME" \ + && tar -zxf "$JDK_TAR" -C "$JAVA_HOME" --strip-components=1 \ + && rm "$JDK_TAR" + +FROM java-base AS remote-builder +ARG SOURCE_REPOSITORY="https://github.com/tronprotocol/java-tron.git" +ARG SOURCE_REF="master" ENV TMP_DIR="/tron-build" -ENV JDK_TAR="jdk-8u202-linux-x64.tar.gz" -ENV JDK_DIR="jdk1.8.0_202" -ENV JDK_MD5="0029351f7a946f6c05b582100c7d45b7" -ENV BASE_DIR="/java-tron" +RUN apt-get update $NO_PROXY_CACHE \ + && apt-get --quiet --yes install --no-install-recommends git 7zip \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* \ + && rm -rf /var/lib/apt/lists/* -RUN set -o errexit -o nounset \ - && yum -y install git wget \ - && wget -P /usr/local https://github.com/frekele/oracle-java/releases/download/8u202-b08/$JDK_TAR \ - && echo "$JDK_MD5 /usr/local/$JDK_TAR" | md5sum -c \ - && tar -zxf /usr/local/$JDK_TAR -C /usr/local\ - && rm /usr/local/$JDK_TAR \ - && export JAVA_HOME=/usr/local/$JDK_DIR \ - && export CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar \ - && export PATH=$PATH:$JAVA_HOME/bin \ - && echo "git clone" \ - && mkdir -p $TMP_DIR \ - && cd $TMP_DIR \ - && git clone https://github.com/tronprotocol/java-tron.git \ - && cd java-tron \ - && git checkout master \ - && ./gradlew build -x test \ - && cd build/distributions \ - && 7za x -y java-tron-1.0.0.zip \ - && mv java-tron-1.0.0 $BASE_DIR \ - && rm -rf $TMP_DIR \ - && rm -rf ~/.gradle \ - && mv $JAVA_HOME/jre /usr/local \ - && rm -rf $JAVA_HOME \ - && yum clean all - -RUN wget -P $BASE_DIR/config https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/main_net_config.conf +RUN git clone --depth 1 --branch "$SOURCE_REF" -- "$SOURCE_REPOSITORY" "$TMP_DIR/java-tron" \ + && cd "$TMP_DIR/java-tron" \ + && ./gradlew :framework:distZip -x test -x check --no-daemon \ + && cd framework/build/distributions \ + && 7z x -y java-tron-1.0.0.zip \ + && mv java-tron-1.0.0 /java-tron \ + && cp "$TMP_DIR/java-tron/framework/src/main/resources/config.conf" /java-tron/config.conf +# docker.sh creates this minimal context from a distribution built on the host. +FROM scratch AS local-distribution +COPY java-tron /java-tron + +FROM ubuntu:24.04 AS runtime-base +ARG VERSION="dev" + +ENV NO_PROXY_CACHE="-o Acquire::BrokenProxy=true -o Acquire::http::No-Cache=true -o Acquire::http::Pipeline-Depth=0" +ENV BASE_DIR="/java-tron" ENV JAVA_HOME="/usr/local/jre" -ENV PATH=$PATH:$JAVA_HOME/bin +ENV PATH="$PATH:$JAVA_HOME/bin" +ENV LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4" +ENV TCMALLOC_RELEASE_RATE=10 + +RUN apt-get update $NO_PROXY_CACHE \ + && apt-get --quiet --yes install --no-install-recommends ca-certificates libtcmalloc-minimal4 zlib1g \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --gid 10001 tron \ + && useradd --uid 10001 --gid 10001 --no-create-home --home-dir "$BASE_DIR" \ + --shell /usr/sbin/nologin tron \ + && mkdir -p "$BASE_DIR" -COPY docker-entrypoint.sh $BASE_DIR/bin +# Keep in sync with docker/arm64/Dockerfile. Defined here so docker.sh can +# build from a Dockerfile-only context. +COPY <<'LOCKDOWN' /usr/local/sbin/lockdown-java-tron +#!/bin/sh +set -eu + +base=/java-tron +config="$base/config.conf" +launcher="$base/bin/FullNode" +vm_options="$base/bin/java-tron.vmoptions" + +if [ ! -d "$base/bin" ] || [ ! -f "$launcher" ] || [ -L "$launcher" ] \ + || [ ! -x "$launcher" ] || [ ! -f "$vm_options" ] \ + || [ ! -f "$config" ] || [ -L "$config" ]; then + echo "lockdown-java-tron: application files are missing under $base" >&2 + exit 1 +fi + +chown -R root:root "$base" +chmod 755 "$base" +chmod 0644 "$config" +mkdir -p "$base/output-directory" "$base/logs" +chown 10001:10001 "$base/output-directory" "$base/logs" +chmod 700 "$base/output-directory" "$base/logs" + +# Keep Docker-created diagnostics private even when the caller's host umask is +# permissive. This changes only the launcher copied into the container image. +sed -i '2i umask 077' "$launcher" + +sed -i \ + -e 's|^-Xloggc:./gc.log|-Xloggc:/java-tron/logs/gc.log|' \ + -e 's|:file=gc.log:|:file=/java-tron/logs/gc.log:|' \ + "$vm_options" + +grep -Fqx -- '-XX:+HeapDumpOnOutOfMemoryError' "$vm_options" \ + || echo '-XX:+HeapDumpOnOutOfMemoryError' >> "$vm_options" +grep -q -- '-XX:HeapDumpPath=' "$vm_options" \ + || echo '-XX:HeapDumpPath=/java-tron/logs' >> "$vm_options" +grep -q -- '-XX:ErrorFile=' "$vm_options" \ + || echo '-XX:ErrorFile=/java-tron/logs/hs_err_pid%p.log' >> "$vm_options" +LOCKDOWN +RUN chmod 0755 /usr/local/sbin/lockdown-java-tron + +COPY --from=java-base /usr/local/openjdk-8/jre /usr/local/jre WORKDIR $BASE_DIR -ENTRYPOINT ["./bin/docker-entrypoint.sh"] +ENTRYPOINT ["./bin/FullNode"] + +# Build-time metadata as defined at http://label-schema.org +ARG BUILD_DATE +ARG VCS_REF +LABEL org.label-schema.build-date=$BUILD_DATE \ + org.label-schema.name="Java-TRON" \ + org.label-schema.description="TRON protocol" \ + org.label-schema.url="https://tron.network/" \ + org.label-schema.vcs-ref=$VCS_REF \ + org.label-schema.vcs-url="https://github.com/tronprotocol/java-tron.git" \ + org.label-schema.vendor="TRON protocol" \ + org.label-schema.version=$VERSION \ + org.label-schema.schema-version="1.0" + +FROM runtime-base AS local +COPY --from=local-distribution /java-tron /java-tron +RUN /usr/local/sbin/lockdown-java-tron +USER 10001:10001 + +# Keep remote last so a plain BuildKit build retains the historical behavior. +FROM runtime-base AS remote +COPY --from=remote-builder /java-tron /java-tron +RUN /usr/local/sbin/lockdown-java-tron +USER 10001:10001 diff --git a/docker/arm64/Dockerfile b/docker/arm64/Dockerfile index 6435faf7ead..645535a9a31 100644 --- a/docker/arm64/Dockerfile +++ b/docker/arm64/Dockerfile @@ -1,33 +1,120 @@ -FROM arm64v8/eclipse-temurin:17 +# syntax=docker/dockerfile:1 +FROM ubuntu:24.04 AS remote-builder +ARG SOURCE_REPOSITORY="https://github.com/tronprotocol/java-tron.git" +ARG SOURCE_REF="master" + +ENV NO_PROXY_CACHE="-o Acquire::BrokenProxy=true -o Acquire::http::No-Cache=true -o Acquire::http::Pipeline-Depth=0" ENV TMP_DIR="/tron-build" -ENV BASE_DIR="/java-tron" +ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-arm64" +ENV PATH="$PATH:$JAVA_HOME/bin" + +RUN apt-get update $NO_PROXY_CACHE \ + && apt-get --quiet --yes install --no-install-recommends git 7zip ca-certificates openjdk-17-jdk-headless=17* \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 --branch "$SOURCE_REF" -- "$SOURCE_REPOSITORY" "$TMP_DIR/java-tron" \ + && cd "$TMP_DIR/java-tron" \ + && ./gradlew :framework:distZip -x test -x check --no-daemon \ + && cd framework/build/distributions \ + && 7z x -y java-tron-1.0.0.zip \ + && mv java-tron-1.0.0 /java-tron \ + && cp "$TMP_DIR/java-tron/framework/src/main/resources/config.conf" /java-tron/config.conf -RUN set -o errexit -o nounset \ - && apt-get update \ - && apt-get -y install git p7zip-full wget libtcmalloc-minimal4 \ - && echo "git clone" \ - && mkdir -p $TMP_DIR \ - && cd $TMP_DIR \ - && git clone https://github.com/tronprotocol/java-tron.git \ - && cd java-tron \ - && git checkout master \ - && ./gradlew clean build -x test -x check --no-daemon \ - && cd build/distributions \ - && 7za x -y java-tron-1.0.0.zip \ - && mv java-tron-1.0.0 $BASE_DIR \ - && rm -rf $TMP_DIR \ - && rm -rf ~/.gradle \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +# docker.sh creates this minimal context from a distribution built on the host. +FROM scratch AS local-distribution +COPY java-tron /java-tron +FROM ubuntu:24.04 AS runtime-base +ARG VERSION="dev" + +ENV NO_PROXY_CACHE="-o Acquire::BrokenProxy=true -o Acquire::http::No-Cache=true -o Acquire::http::Pipeline-Depth=0" +ENV BASE_DIR="/java-tron" +ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-arm64" +ENV PATH="$PATH:$JAVA_HOME/bin" ENV LD_PRELOAD="/usr/lib/aarch64-linux-gnu/libtcmalloc_minimal.so.4" ENV TCMALLOC_RELEASE_RATE=10 -RUN wget -P $BASE_DIR/config https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/main_net_config.conf +RUN apt-get update $NO_PROXY_CACHE \ + && apt-get --quiet --yes install --no-install-recommends ca-certificates libtcmalloc-minimal4 openjdk-17-jre-headless=17* \ + && apt-get clean \ + && rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --gid 10001 tron \ + && useradd --uid 10001 --gid 10001 --no-create-home --home-dir "$BASE_DIR" \ + --shell /usr/sbin/nologin tron \ + && mkdir -p "$BASE_DIR" + +# Keep in sync with docker/Dockerfile. Defined here so docker.sh can build +# from a Dockerfile-only context. +COPY <<'LOCKDOWN' /usr/local/sbin/lockdown-java-tron +#!/bin/sh +set -eu -COPY docker-entrypoint.sh $BASE_DIR/bin +base=/java-tron +config="$base/config.conf" +launcher="$base/bin/FullNode" +vm_options="$base/bin/java-tron.vmoptions" + +if [ ! -d "$base/bin" ] || [ ! -f "$launcher" ] || [ -L "$launcher" ] \ + || [ ! -x "$launcher" ] || [ ! -f "$vm_options" ] \ + || [ ! -f "$config" ] || [ -L "$config" ]; then + echo "lockdown-java-tron: application files are missing under $base" >&2 + exit 1 +fi + +chown -R root:root "$base" +chmod 755 "$base" +chmod 0644 "$config" +mkdir -p "$base/output-directory" "$base/logs" +chown 10001:10001 "$base/output-directory" "$base/logs" +chmod 700 "$base/output-directory" "$base/logs" + +# Keep Docker-created diagnostics private even when the caller's host umask is +# permissive. This changes only the launcher copied into the container image. +sed -i '2i umask 077' "$launcher" + +sed -i \ + -e 's|^-Xloggc:./gc.log|-Xloggc:/java-tron/logs/gc.log|' \ + -e 's|:file=gc.log:|:file=/java-tron/logs/gc.log:|' \ + "$vm_options" + +grep -Fqx -- '-XX:+HeapDumpOnOutOfMemoryError' "$vm_options" \ + || echo '-XX:+HeapDumpOnOutOfMemoryError' >> "$vm_options" +grep -q -- '-XX:HeapDumpPath=' "$vm_options" \ + || echo '-XX:HeapDumpPath=/java-tron/logs' >> "$vm_options" +grep -q -- '-XX:ErrorFile=' "$vm_options" \ + || echo '-XX:ErrorFile=/java-tron/logs/hs_err_pid%p.log' >> "$vm_options" +LOCKDOWN +RUN chmod 0755 /usr/local/sbin/lockdown-java-tron WORKDIR $BASE_DIR -ENTRYPOINT ["./bin/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["./bin/FullNode"] + +# Build-time metadata as defined at http://label-schema.org +ARG BUILD_DATE +ARG VCS_REF +LABEL org.label-schema.build-date=$BUILD_DATE \ + org.label-schema.name="Java-TRON" \ + org.label-schema.description="TRON protocol" \ + org.label-schema.url="https://tron.network/" \ + org.label-schema.vcs-ref=$VCS_REF \ + org.label-schema.vcs-url="https://github.com/tronprotocol/java-tron.git" \ + org.label-schema.vendor="TRON protocol" \ + org.label-schema.version=$VERSION \ + org.label-schema.schema-version="1.0" + +FROM runtime-base AS local +COPY --from=local-distribution /java-tron /java-tron +RUN /usr/local/sbin/lockdown-java-tron +USER 10001:10001 + +# Keep remote last so a plain BuildKit build retains the historical behavior. +FROM runtime-base AS remote +COPY --from=remote-builder /java-tron /java-tron +RUN /usr/local/sbin/lockdown-java-tron +USER 10001:10001 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh deleted file mode 100755 index d3c5d4c65c8..00000000000 --- a/docker/docker-entrypoint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -eo pipefail -shopt -s nullglob - -echo "./bin/FullNode $@" > command.txt -exec "./bin/FullNode" "$@" \ No newline at end of file diff --git a/docker/docker.md b/docker/docker.md index 79aa6b08e2d..6d4ae084a3d 100644 --- a/docker/docker.md +++ b/docker/docker.md @@ -1,110 +1,323 @@ # Docker Shell Guide -java-tron support containerized processes, we maintain a Docker image with latest version build from our master branch on DockerHub. To simplify the use of Docker and common docker commands, we also provide a shell script to help you better manage container services,this guide describes how to use the script tool. +This guide covers the `docker.sh` workflow maintained in the java-tron repository. The Bash helper can build an image locally, pull the `tronprotocol/java-tron` image from Docker Hub, and operate a single FullNode container. +For Docker Compose deployments, multi-node private networks, and dedicated image build and test tooling, use the [tron-docker repository](https://github.com/tronprotocol/tron-docker). The two workflows are maintained independently; their commands, configuration, and defaults are not interchangeable. ## Prerequisites -Requires a docker to be installed on the system. Docker version >=20.10.12. +- Docker Engine 23.0 or later, with BuildKit and the Buildx plugin available +- Bash +- `curl` or `wget` when configuration files or Dockerfiles need to be downloaded +- For `--source local` only: `unzip` and the architecture-specific JDK used by java-tron (JDK 8 on x86_64/amd64 or JDK 17 on arm64/aarch64) +Do not invoke the script with `sh`. The script uses Bash-specific syntax. -## Quick Start +## Quick start + +Use `docker/docker.sh` from a java-tron checkout, or download it separately: + +```shell +wget https://raw.githubusercontent.com/tronprotocol/java-tron/master/docker/docker.sh +``` + +The standalone download follows the stable `master` workflow. To test changes from `develop` or an uncommitted working tree, use `docker/docker.sh` from the corresponding java-tron checkout instead. All examples below assume that `docker.sh` is in the current directory. + +### Build the default image + +The image contains the java-tron distribution and a Java runtime. It also bakes in `/java-tron/config.conf` from the same source checkout used to build the distribution: the selected remote ref for remote builds, or the local checkout for local builds. `docker.sh --run` passes that file with `-c` for Mainnet. A plain `docker run` without `-c` reads the same file directly. + +The helper intentionally does not select the mutable `tronprotocol/java-tron:latest` tag. The image published under that tag when this fixed non-root runtime contract was introduced predates the contract and is incompatible. Build a compatible image from the remote java-tron `master` branch before the first default run: + +```shell +bash docker.sh --build +``` + +This produces `tronprotocol/java-tron:local`. To use a compatible image from a registry instead, select it explicitly for both pull and run; prefer an immutable version tag or digest for production: + +```shell +bash docker.sh --pull --image registry.example.com/java-tron:VERSION +bash docker.sh --run --image registry.example.com/java-tron:VERSION --net main +``` + +### Run a FullNode + +`docker.sh` publishes the following ports by default: + +- `127.0.0.1:8090:8090/tcp`: HTTP JSON API, accessible only from the Docker host +- `127.0.0.1:50051:50051/tcp`: gRPC API, accessible only from the Docker host +- `18888:18888/tcp` and `18888:18888/udp`: P2P communication, accessible through the host network interfaces + +HTTP and gRPC are bound to loopback by default to prevent accidental network or public exposure. P2P remains externally reachable so that the node can communicate with peers. + +> **Storage and synchronization:** The default Mainnet configuration starts a full FullNode. It does not enable Lite FullNode mode or preload a data snapshot. When `output-directory` under the selected data directory is empty, the node synchronizes the complete Mainnet database from genesis; allocate approximately 3.5–4 TB of high-performance SSD storage for this mode. The script persists this database on the host, but the host filesystem must still have sufficient capacity. To reduce initial synchronization time or use the Lite FullNode storage tier, import and configure a compatible [FullNode or Lite FullNode data snapshot](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#data-snapshot) for the selected network and java-tron version before starting the node. + +Run a Mainnet FullNode: + +```shell +bash docker.sh --run --net main +``` + +The helper manages one container named `tronprotocol-java-tron` by default. Use `--container-name` to choose another name; `--start`, `--stop`, `--log`, and `--rm` accept the same option. Running `--run` again while that container exists returns an error; use `--start` to reuse a stopped container, or `--rm` before creating a replacement. If the default local image does not exist, `--run` exits with an error instead of implicitly pulling or building; run `--build` first, or select a compatible published image explicitly with `--image`. + +Use repeatable `-p` options to change individual mappings. Defaults are retained for container ports and protocols that are not explicitly mapped, so the following command changes only the HTTP and gRPC host ports: + +```shell +bash docker.sh --run --net main \ + -p 127.0.0.1:8080:8090 \ + -p 127.0.0.1:40051:50051 +``` + +To provide a network-accessible API, explicitly replace the relevant loopback mapping. For example, the following publishes HTTP on all IPv4 interfaces: + +```shell +bash docker.sh --run --net main -p 0.0.0.0:8090:8090 +``` + +Only expose HTTP or gRPC after restricting access with a firewall, trusted reverse proxy, or equivalent network controls. + +### Nile Testnet nodes + +Since Nile Testnet may incorporate features not yet available on the Mainnet, it may require code that is not included in this java-tron checkout or its images. The `docker.sh` helper in this repository therefore does not provide a Nile network mode. + +For a Nile Docker deployment, follow the [tron-docker](https://github.com/tronprotocol/tron-docker) instructions and select the image appropriate for the current Nile release. + +Run a private-network FullNode: + +```shell +bash docker.sh --run --net private +``` + +## Configuration + +The script selects network configuration as follows: + +- `main`: uses `/java-tron/config.conf` directly from the selected image, so no host-side Mainnet configuration is created +- `private`: tron-deployment `master` [`private_net_config.conf`](https://github.com/tronprotocol/tron-deployment/blob/master/private_net_config.conf) + +The private-network configuration is stored in the host data directory. An existing local copy is retained by default; a missing or empty file is downloaded from its maintained source. + +The private-network template can change independently of a previously built image. Verify that a downloaded configuration is compatible with the image version before using it. + +Use `--update-config true` to explicitly refresh the private-network local copy before creating the container. Mainnet always uses the configuration in the selected image, so this option has no effect with `--net main`: + +```shell +bash docker.sh --run --net private --update-config true +``` + +Use `-c` to select a custom configuration file. The value must be a path inside the container, so mount the host file with `-v`: + +```shell +bash docker.sh --run --net main \ + -v /absolute/path/custom.conf:/java-tron/custom.conf:ro \ + -c /java-tron/custom.conf +``` + +## FullNode arguments + +Use `--` to end `docker.sh` option parsing and pass remaining arguments through to FullNode. `docker.sh` keeps those argument boundaries when it calls `docker run`, and the generated `bin/FullNode` script forwards each one to Java without word-splitting, including values that contain spaces. For example, to explicitly keep P2P enabled: -Shell can be obtained from the java-tron project or independently, you can get the script from [here](https://github.com/tronprotocol/java-tron/blob/develop/docker/docker.sh) or download via the wget: ```shell -$ wget https://raw.githubusercontent.com/tronprotocol/java-tron/develop/docker/docker.sh +bash docker.sh --run --net main -- --p2p-disable false +``` + +Options before `--` configure the Docker helper; options after it become `./bin/FullNode` arguments. Use the helper's `-c` option for the configuration path instead of passing a second `-c` after `--`. The start script still consumes a `-jvm '{...}'` pair before invoking Java; every other FullNode argument is forwarded intact. + +Witness options such as `-w` and `--witness-address` can also be passed after `--`. Do not pass `--private-key` or `--password`: command arguments may be visible in process listings and are retained in Docker container metadata. This helper does not by itself provide the secret delivery, key protection, monitoring, backup, and upgrade procedures required for a production Super Representative deployment. Follow the [Starting a Block Production Node](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#starting-a-block-production-node) guide, use an encrypted keystore, and provide its password through the production deployment's secret-management mechanism. + +By default, the script mounts persistent runtime directories relative to the shell's current working directory when `docker.sh` is invoked, not relative to the script file. Mainnet uses the configuration baked into the image and mounts only: + +```text +./output-directory -> /java-tron/output-directory +./logs -> /java-tron/logs ``` -### Pull the mirror image -Get the `tronprotocol/java-tron` image from the DockerHub, this image contains the full JDK environment and the host network configuration file, using the script for simple docker operations. +Private mode mounts the same two runtime directories and additionally mounts: + +```text +./config -> /java-tron/config (read-only) +``` + +The private-network configuration mount is read-only because FullNode only needs to read it. This prevents the container from persisting configuration changes onto the host. By default, Mainnet does not create or mount a host `config` directory. Additional `-v` options retain the defaults for the selected mode. A custom mount replaces a default only when it uses the same container destination; callers that explicitly replace the configuration mount control its access mode. + +The image runs FullNode as the non-root `tron` account with fixed UID and GID `10001:10001`. Application files under `/java-tron` (`bin/`, `lib/`, `java-tron.vmoptions`, and the baked-in `config.conf`) stay root-owned and are not writable by that user. Only `/java-tron/output-directory` and `/java-tron/logs` belong to `10001:10001`; their built-in modes are `0700`. The Docker image sets `umask 077` before FullNode starts, so newly created runtime files and directories default to `0600` and `0700`. The image enables heap dumps on out-of-memory errors and points JVM GC logs, heap dumps, and `hs_err` files at `/java-tron/logs`; the packaged launcher and `java-tron.vmoptions` used outside Docker are unchanged. An OOM heap dump can approach the configured maximum heap size, so provision and monitor the logs volume accordingly. + +For new or empty default `output-directory` and `logs` mounts, `docker.sh` sets the mount-point mode to `0700` and initializes its ownership with a minimal Docker Official Image pinned by digest; it does not execute the selected java-tron workload image with root privileges. The helper runs without networking, with a read-only root filesystem, and with only `CAP_CHOWN`, then `docker.sh` verifies from inside a restricted container that runtime identity `10001:10001` can write to the mounts. The pinned helper is pulled on first use if it is not already available locally. + +Existing non-empty runtime directories are never silently chmodded. If the host user can inspect one whose root mode permits access by group or other users, `docker.sh` preserves the mode and prints a warning with a `chmod 0700` remediation command. If the host user cannot inspect the directory, the helper requires mode `0700` because it cannot safely distinguish an empty private directory from an existing exposed data tree. Stop the node before changing that mode. Custom writable mounts supplied with `-v` are not modified or inspected for confidentiality; their ownership and permissions remain the caller's responsibility. + +Automatic initialization is supported by rootful Docker without user-namespace remapping and by rootless Docker. Rootless ownership appears on the host as the subordinate UID and GID selected by the Docker daemon. Rootful Docker with `userns-remap` is different: remapped container root generally cannot change ownership of a directory just created by the invoking host user. `docker.sh` detects that mode and refuses automatic initialization before running the ownership helper. Pre-create the directories with the mapped host UID and GID as described below. For non-empty mounts, the preflight check covers the mount point and its direct children only; it does not traverse the complete database tree. FullNode is started with `no-new-privileges`. Custom writable mounts supplied with `-v` must already be accessible to container UID and GID `10001:10001` through the active Docker user-namespace mapping. + +Data written by an older root-based image may require a one-time ownership migration. Stop the node before changing ownership. The following command applies only to rootful Docker without user-namespace remapping; adjust the paths for the selected data directory: + ```shell -$ sh docker.sh --pull +sudo chown -R 10001:10001 \ + /var/lib/java-tron/output-directory \ + /var/lib/java-tron/logs ``` -### Run the service -Before running the java-tron service, make sure some ports on your local machine are open,the image has the following ports automatically exposed: -- `8090`: used by the HTTP based JSON API -- `50051`: used by the GRPC based API -- `18888`: TCP and UDP, used by the P2P protocol running the network +For rootless Docker or `userns-remap`, migrate the same paths to the host UID and GID that the active Docker daemon maps from container `10001:10001`; do not use literal host IDs `10001:10001` unless that is the daemon's actual mapping. For a rootful daemon configured with the default `dockremap` user, the following example derives the mapped IDs from the first subordinate ranges and provisions new empty directories. Replace `dockremap` if `userns-remap` names a different account, and confirm the daemon's mapping before applying ownership changes: -#### Full node on the main network +```shell +remap_user=dockremap +subuid_start=$(awk -F: -v user="$remap_user" '$1 == user { print $2; exit }' /etc/subuid) +subgid_start=$(awk -F: -v user="$remap_user" '$1 == user { print $2; exit }' /etc/subgid) +test -n "$subuid_start" && test -n "$subgid_start" +mapped_uid=$((subuid_start + 10001)) +mapped_gid=$((subgid_start + 10001)) +sudo install -d -m 0700 -o "$mapped_uid" -g "$mapped_gid" \ + /var/lib/java-tron/output-directory \ + /var/lib/java-tron/logs +``` + +The helper deliberately does not recursively inspect or change a non-empty directory because scanning a multi-terabyte database during every startup would be slow and unexpected. A partially migrated tree can therefore pass the shallow preflight check but fail later when FullNode reaches a deeper file. Run the appropriate one-time recursive ownership migration for data created by a root-based image. When the shallow check detects a problem, the helper prints guidance for both namespace cases. Direct `docker run` users must prepare writable mounts themselves and should also specify `--security-opt no-new-privileges`. + +Use `--data-dir` to keep the default host runtime directories in an explicit location, preferably outside the source checkout. Relative values are resolved against the invocation directory: ```shell -$ sh docker.sh --run --net main +bash docker.sh --run --net main --data-dir /var/lib/java-tron +``` + +For this Mainnet command, the helper creates and mounts `/var/lib/java-tron/output-directory` and `/var/lib/java-tron/logs`; it does not create `/var/lib/java-tron/config`. Using `--net private` with the same data directory additionally creates `/var/lib/java-tron/config` and mounts it read-only at `/java-tron/config`. The default data directory is the current working directory, so a Mainnet `--run` from a checkout writes `output-directory/` and `logs/` into that tree, while private mode also writes `config/`. Use `--data-dir` to keep those runtime files out of the Git worktree. Persisting `logs` also keeps `tron.log` available after the container is removed. + +The data directory itself may be a symbolic link, and `docker.sh` resolves it to its physical directory before creating mounts. The selected path's existing parents, the resolved directory, and each of its ancestors must be owned by root or by the user running `docker.sh`, and none may be writable by their group or by other users. The helper enforces this before passing any managed path to `docker run`; use `chown` and `chmod go-w` to correct an unsafe path. The managed `output-directory` and `logs` paths, plus `config` in private mode, must be real directories and must not be symbolic links. To place the node data on another disk, point `--data-dir` at that disk or at a data-directory link instead of linking an individual managed path. + +## Memory and JVM options + +These memory defaults come from `docker.sh`, not from the image or the packaged `java-tron.vmoptions` file. A plain `docker run` or `bin/FullNode` invocation without the helper still uses the JVM ergonomics default of about 25% of visible memory. + +`docker.sh` applies a minimum helper profile: a `16g` container memory limit, a 2 GB initial heap, and a maximum heap of up to 60% of that container limit: + +```text +-Xms2g -XX:MaxRAMPercentage=60.0 ``` -or you can use `-p` to customize the port mapping, more custom parameters, please refer to [Options](#Options) + +JDK 8 images also receive `-XX:MaxDirectMemorySize=1g`. JDK 17 images already include that option in `java-tron.vmoptions`, so the helper does not add it again. The script inspects the image architecture to decide this, not the host `uname`. + +This is a minimum **memory** profile for lower-load deployments; it does not enable Lite FullNode mode or reduce the database storage requirement described above. For stable Mainnet operation, use at least `32g`; Super Representative nodes require at least `64g`. See the [Mainnet hardware requirements](../README.md#hardware-requirements-for-mainnet) for the complete deployment tiers. + +Use `--memory` to change the container memory limit. When the default helper JVM options are retained, the maximum heap scales with this limit: ```shell -$ sh docker.sh --run --net main -p 8080:8090 -p 40051:50051 +bash docker.sh --run --net main --memory 32g ``` -#### Full node on the nile test network +To replace the helper JVM options entirely, use `--jvm-opts`. The replacement is not merged with the defaults, so include every option you need: + ```shell -$ sh docker.sh --run --net test +bash docker.sh --run --net main --memory 32g \ + --jvm-opts "-Xms4g -Xmx18g -XX:MaxDirectMemorySize=2g" ``` -#### Full node on the private network -you can also build your own private-net and will download a configuration file from the network for your private network, which will be stored in your local `config` directory. +The packaged `java-tron.vmoptions` file remains active. It contains architecture- and JDK-specific garbage collector settings, so do not use `--jvm-opts` to copy or switch GC options between JDK 8 and JDK 17 deployments. + +Environment variables for custom wrapper scripts or derived images can be passed with repeatable `-e` or `--env` options. `MY_VARIABLE` is only a placeholder. JVM option environment variables (`JAVA_OPTS`, `FULL_NODE_OPTS`, `JAVA_TOOL_OPTIONS`, `_JAVA_OPTIONS`, and `JDK_JAVA_OPTIONS`) are rejected because they can replace or bypass the helper profile. Use `--jvm-opts` instead. + ```shell -$ sh docker.sh --run --net private +bash docker.sh --run --net main -e "MY_VARIABLE=value" ``` -#### Configuration -The script will automatically download and use the corresponding configuration file from the github repository according to the `--net` parameter. if you don't want to update the configuration file every time you start the service, please add a startup parameter. + +## Container lifecycle + +View the java-tron log: ```shell -$ sh docker.sh --run --update-config false +bash docker.sh --log ``` -Or use the `-c` parameter to specify your own configuration file, which will not automatically download a new configuration file from github repository. +For example, filter block-processing messages with: +```shell +bash docker.sh --log | grep 'PushBlock' +``` -### View logs -If you want to see the logs of the java-tron service, please use the `--log` parameter +Stop and restart the container: ```shell -$ sh docker.sh --log | grep 'PushBlock' +bash docker.sh --stop +bash docker.sh --start ``` -### Stop the service -If you want to stop the container of java-tron, you can execute +Remove the container without deleting the image or persisted host data: ```shell -$ sh docker.sh --stop +bash docker.sh --rm ``` -## Build Image +The lifecycle commands return a non-zero status when the target container does not exist, the container cannot be queried, or the underlying Docker operation fails. This allows service managers and automation scripts to detect failures reliably. + +## Build an image -If you do not want to use the default official image, you can also compile your own local image, first you need to change some parameters in the shell script to specify your own mirror info. -`DOCKER_REPOSITORY` is your repository name -`DOCKER_IMAGES` is the image name -`DOCKER_TARGET` is the version number, here is an example: +`--build` selects `Dockerfile` on x86_64/amd64 and `arm64/Dockerfile` on arm64/aarch64. For a remote or standalone `--build`, a missing Dockerfile is downloaded from the java-tron `master` branch. `--source local` uses the Dockerfile from the same checkout and fails if that file is not present. + +For backward compatibility, `--build` without source options clones and compiles the remote java-tron `master` branch. Local working-tree changes are not included: ```shell -DOCKER_REPOSITORY="your_repository" -DOCKER_IMAGES="java-tron" -DOCKER_TARGET="1.0" +bash docker.sh --build ``` -then execute the build: +Use `--source-ref` to build another remote branch or tag. `--source-repository` can select another public Git repository: ```shell -$ sh docker.sh --build +bash docker.sh --build \ + --source remote \ + --source-ref develop ``` -## Options +Each helper-driven remote build pulls refreshed base-image metadata and invalidates the `remote-builder` stage, so a moved branch such as `master` is cloned and rebuilt instead of being silently reused from a previous Docker layer. The selected ref is still a remote Git trust input rather than cryptographically pinned provenance; use a controlled repository and release ref for distributable images. -Parameters for all functions: +From a java-tron checkout, use `--source local` to compile the current working tree, including uncommitted changes: -* **`--build`** building a local mirror image -* **`--pull`** download a docker mirror from **DockerHub** -* **`--run`** run the docker mirror -* **`--log`** exporting the java-tron run log on the container -* **`--stop`** stopping a running container -* **`--rm`** remove container,only deletes the container, not the image -* **`-p`** publish a container's port to the host, format:`-p hostPort:containerPort` -* **`-c`** specify other java-tron configuration file in the container -* **`-v`** bind mount a volume for the container,format: `-v host-src:container-dest`, the `host-src` is an absolute path -* **`--net`** select the network, you can join the main-net, test-net -* **`--update-config`** update configuration file, default true +```shell +bash docker/docker.sh --build --source local +``` + +In local mode, `docker.sh` runs the Gradle `:framework:distZip` task on the host, uses the architecture-specific Dockerfile and Mainnet configuration from the same checkout, and extracts the resulting distribution into a private staging directory. The build fails instead of downloading a Dockerfile or configuration when the checkout does not contain the expected file. It also fails closed if the distribution contains symbolic links, special files, or paths outside the supported `bin` launchers and flat `lib/*.jar` layout. The generated context includes a matching allowlist `.dockerignore` as a second boundary, so only those runtime files, the checked configuration, and the local Dockerfile are available to the Docker daemon. Node databases, logs, wallets, node identities, environment files, keys, keystores, and other unexpected distribution content are rejected rather than exported or baked into the image. + +The checkout's `framework/src/main/resources/config.conf` is copied verbatim into the local image as the world-readable `/java-tron/config.conf`. Before building, `docker.sh` rejects a non-empty plaintext `localwitness` list, `event.subscribe.dbconfig`, `node.dns.dnsPrivate`, or `node.dns.accessKeySecret` value so signing keys and service credentials are not accidentally baked into an image layer. Clear those settings before building and provide sensitive signing, database, or DNS configuration through a protected runtime bind mount. This targeted check does not prove that every custom configuration field is free of sensitive data, so review `config.conf` before distributing the image. + +The two architecture-specific Dockerfiles each provide `local` and `remote` BuildKit targets. `docker.sh` selects the appropriate target and supplies its required minimal context. A plain Dockerfile build defaults to the historical `remote` target, but direct `local` target builds require callers to stage the distribution as `java-tron/` first. +`--build` and `--run` use `tronprotocol/java-tron:local` by default. This keeps the default run path aligned with the helper-built non-root image and avoids silently selecting a legacy published image. `--pull` has no implicit image and requires `--image` or `JAVA_TRON_IMAGE`: + +```shell +bash docker.sh --build +bash docker.sh --run --net main +``` + +Override the image for `--pull`, `--build`, or `--run` with `--image NAME[:TAG]` or the `JAVA_TRON_IMAGE` environment variable: + +```shell +bash docker.sh --build --source local --image java-tron:dev +bash docker.sh --run --image java-tron:dev --net main +JAVA_TRON_IMAGE=java-tron:ci-amd64 bash docker.sh --run --net private +``` + +## Options +| Option | Description | +| --- | --- | +| `-h`, `--help` | Show command usage without requiring a Docker daemon. | +| `--build` | Build an image for the host architecture. Defaults to remote `master` source for backward compatibility. Tags `tronprotocol/java-tron:local` unless `--image` is set. | +| `--source local\|remote` | Select a host-built distribution or a remote source build for `--build`. Default: `remote`. | +| `--source-ref REF` | Select the remote branch or tag for `--build`. Default: `master`. | +| `--source-repository URL` | Select the public remote Git repository for `--build`. | +| `--export-context PATH` | With `--build --source local`, prepare a new minimal build context at `PATH` without building an image. The destination must not already exist. Intended for external BuildKit frontends such as CI. | +| `--image NAME[:TAG]` | Select the image for `--pull`, `--build`, or `--run`. `JAVA_TRON_IMAGE` sets the same value. | +| `--pull` | Pull an explicitly selected image. Requires `--image NAME[:TAG]` or `JAVA_TRON_IMAGE`; the pulled image must declare runtime UID:GID `10001:10001`. | +| `--run` | Create and start a container. Default: `tronprotocol/java-tron:local`. Use `--image` to select a compatible published image. | +| `--container-name NAME` | Container name for `--run`, `--start`, `--stop`, `--log`, and `--rm`. Default: `tronprotocol-java-tron`. | +| `--start` | Start the existing stopped container. | +| `--log` | Follow the java-tron log in the container. | +| `--stop` | Stop the running container. | +| `--rm` | Remove the container without removing the image or host data. | +| `-p [HOST_IP:]HOST_PORT:CONTAINER_PORT[/PROTOCOL]` | Publish a container port. Repeat to customize multiple mappings. | +| `-c CONTAINER_PATH` | Use a configuration file at the specified path inside the container. | +| `-v HOST_PATH:CONTAINER_PATH[:OPTIONS]` | Add or replace a bind mount. The host path should be absolute. Writable custom mounts must be accessible to container UID:GID `10001:10001` through the active Docker user-namespace mapping. In private mode, a replacement for the default `/java-tron/config` mount uses the caller's access mode instead of read-only mode. | +| `-e NAME=VALUE`, `--env NAME=VALUE` | Set a container environment variable. This option can be repeated. JVM option environment variables are rejected; use `--jvm-opts`. | +| `--net main\|private` | Select the Mainnet or private-network configuration. Since Nile may incorporate features not yet available on the Mainnet, use the separate [nile-testnet](https://github.com/tron-nile-testnet/nile-testnet) codebase and follow [tron-docker](https://github.com/tronprotocol/tron-docker). | +| `--update-config true\|false` | Refresh the private-network configuration before creating the container. Default: `false`; a missing or empty file is still downloaded. This option has no effect with `--net main`. | +| `--data-dir PATH` | Store the default runtime data under this host path. Mainnet uses only `output-directory` and `logs`; private mode also uses `config`. Default: invocation directory. The managed `config` mount is read-only in the container. | +| `--memory LIMIT` | Set the container memory limit. Default: `16g`. | +| `--jvm-opts "OPTIONS"` | Replace the JVM options supplied by `docker.sh`. The image itself does not set these defaults. | +| `-- FULLNODE_ARGS...` | Pass all remaining arguments unchanged to FullNode. | diff --git a/docker/docker.sh b/docker/docker.sh index bf4961f0620..b9502093ae2 100644 --- a/docker/docker.sh +++ b/docker/docker.sh @@ -17,275 +17,1763 @@ # ############################################################################## +usage() { + cat <<'EOF' +Usage: docker.sh COMMAND [OPTIONS] + +Commands: + --pull Pull an explicitly selected java-tron image + --build [OPTIONS] Build an image for the host architecture + --run [OPTIONS] Create and start a FullNode container + --start Start the existing container + --stop Stop the existing container + --log Follow the java-tron log + --rm Remove the existing container + -h, --help Show this help message + +Build options: + --source local|remote Build a host distribution or remote source + --source-ref REF Select a remote branch or tag + --source-repository URL Select a remote Git repository + --export-context PATH Prepare a new local build context without + building an image + +Common options: + --image NAME[:TAG] Image for --pull, --build, or --run. + JAVA_TRON_IMAGE can set the same value. + Build and run default to the local image; + pull requires an explicit image. + --container-name NAME Container name for --run, --start, --stop, + --log, and --rm. Default: tronprotocol-java-tron. + +Run options: + --net main|private Select the network configuration + --update-config true|false Refresh the private configuration + --data-dir PATH Set the host runtime-data directory + --memory LIMIT Set the container memory limit + --jvm-opts "OPTIONS" Replace docker.sh default JVM options + -p MAPPING Publish a container port; repeatable + -v MAPPING Add or replace a bind mount; repeatable + -e NAME=VALUE, --env NAME=VALUE + Set an environment variable; repeatable. + JVM option environment variables are not allowed; + use --jvm-opts instead. + -c CONTAINER_PATH Use a custom configuration file + -- [FULLNODE_ARGS...] Pass remaining arguments to FullNode +EOF +} + +if [ $# -eq 0 ]; then + usage >&2 + exit 1 +fi +if [[ "$1" = "-h" || "$1" = "--help" ]]; then + usage + exit 0 +fi + BASE_DIR="/java-tron" DOCKER_REPOSITORY="tronprotocol" DOCKER_IMAGES="java-tron" -# latest or version -DOCKER_TARGET="latest" +CONTAINER_NAME="$DOCKER_REPOSITORY-$DOCKER_IMAGES" +BUILD_IMAGE_DEFAULT="$DOCKER_REPOSITORY/$DOCKER_IMAGES:local" +RUN_IMAGE_DEFAULT="$BUILD_IMAGE_DEFAULT" +IMAGE_OVERRIDE="${JAVA_TRON_IMAGE:-}" HOST_HTTP_PORT=8090 HOST_RPC_PORT=50051 HOST_LISTEN_PORT=18888 +HOST_HTTP_BIND_ADDRESS="127.0.0.1" +HOST_RPC_BIND_ADDRESS="127.0.0.1" DOCKER_HTTP_PORT=8090 DOCKER_RPC_PORT=50051 DOCKER_LISTEN_PORT=18888 -VOLUME=`pwd` -CONFIG="$VOLUME/config" -OUTPUT_DIRECTORY="$VOLUME/output-directory" +DOCKER_MEMORY="16g" +JAVA_TRON_UID=10001 +JAVA_TRON_GID=10001 +# Do not use the selected workload image as a privileged host-mount helper. +# This multi-architecture Docker Official Image is pinned by OCI index digest. +RUNTIME_INIT_IMAGE="busybox:1.37.0-musl@sha256:fc6dddc4c44b1bfe37f41cae8e67d1693828e8f42a91862816d7953e2c9d3f23" +# Helper-only defaults. The image and packaged vmoptions do not set heap size. +# JDK 8 images also receive -XX:MaxDirectMemorySize=1g at --run time. +JVM_OPTS="-Xms2g -XX:MaxRAMPercentage=60.0" + +DEFAULT_DATA_DIR=$(pwd) CONFIG_PATH="/java-tron/config/" -CONFIG_FILE="main_net_config.conf" -MAIN_NET_CONFIG_FILE="main_net_config.conf" -TEST_NET_CONFIG_FILE="test_net_config.conf" +MAIN_NET_CONFIG_PATH="$BASE_DIR/config.conf" +CONFIG_FILE="" PRIVATE_NET_CONFIG_FILE="private_net_config.conf" -# update the configuration file, if true, the configuration file will be fetched from the network every time you start -UPDATE_CONFIG=true +# Preserve an existing private configuration by default. A missing or +# empty file is downloaded; use --update-config true to refresh it. +UPDATE_CONFIG=false -LOG_FILE="/logs/tron.log" +LOG_FILE="logs/tron.log" -JAVA_TRON_REPOSITORY="https://raw.githubusercontent.com/tronprotocol/java-tron/develop/" -DOCKER_FILE="Dockerfile" -ENDPOINT_SHELL="docker-entrypoint.sh" +JAVA_TRON_DOCKER_REPOSITORY="https://raw.githubusercontent.com/tronprotocol/java-tron/master/docker" +JAVA_TRON_SOURCE_REPOSITORY="https://github.com/tronprotocol/java-tron.git" +JAVA_TRON_SOURCE_REF="master" +PRIVATE_NET_CONFIG_URL="https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/private_net_config.conf" +DOCKER_SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) -if test docker; then - docker -v -else +if ! command -v docker >/dev/null 2>&1; then echo "warning: docker must be installed, please install docker first." - exit + exit 1 +fi +docker_version_output=$(docker --version) || exit 1 +echo "$docker_version_output" +if [[ ! "$docker_version_output" =~ [Vv]ersion[[:space:]]+([0-9]+)\. ]]; then + echo "Unable to determine the Docker version from: $docker_version_output" >&2 + exit 1 +fi +if [ "${BASH_REMATCH[1]}" -lt 23 ]; then + echo "Docker 23.0 or later is required for BuildKit target builds." >&2 + exit 1 fi docker_ps() { - containerID=`docker ps -a | grep "$DOCKER_REPOSITORY-$DOCKER_IMAGES" | awk '{print $1}'` - cid=$containerID + local inspected_container + local inspected_id + local inspected_name + + if inspected_container=$(docker container inspect \ + --format '{{.Id}} {{.Name}}' "$CONTAINER_NAME" 2>/dev/null); then + inspected_id=${inspected_container%% *} + inspected_name=${inspected_container#* } + if [ -n "$inspected_id" ] && [ "$inspected_name" = "/$CONTAINER_NAME" ]; then + containerID=$inspected_id + cid=$containerID + return 0 + fi + + # Docker also accepts container-ID prefixes as inspect targets. Treat an + # inspect result with a different name as no exact name match. + containerID="" + cid="" + return 0 + fi + + # A failed inspect normally means that the exact name is absent. Verify that + # the daemon is still queryable so lifecycle commands do not hide API errors. + if ! docker container ls -aq >/dev/null 2>&1; then + echo "failed to query the java-tron container" >&2 + containerID="" + cid="" + return 1 + fi + + containerID="" + cid="" +} + +valid_container_name() { + [[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ ]] +} + +set_container_name() { + local command_name="$1" + local value="$2" + + if ! valid_container_name "$value"; then + echo "$command_name: invalid container name: $value" >&2 + return 1 + fi + CONTAINER_NAME=$value +} + +apply_container_name_args() { + local command_name="$1" + shift + + while [ $# -gt 0 ]; do + case "$1" in + --container-name) + if [ $# -lt 2 ]; then + echo "$command_name: arg $1 requires a value" + return 1 + fi + set_container_name "$command_name" "$2" || return 1 + shift 2 + ;; + *) + echo "$command_name: arg $1 is not a valid parameter" + return 1 + ;; + esac + done +} + +image_exists() { + docker image inspect "$1" >/dev/null 2>&1 +} + +selected_image() { + if [ -n "$IMAGE_OVERRIDE" ]; then + printf '%s\n' "$IMAGE_OVERRIDE" + return + fi + + case "$1" in + build) + printf '%s\n' "$BUILD_IMAGE_DEFAULT" + ;; + run) + printf '%s\n' "$RUN_IMAGE_DEFAULT" + ;; + pull) + echo "pull: no compatible default published image is configured; specify --image NAME[:TAG]" >&2 + return 1 + ;; + *) + echo "selected_image: unknown command $1" >&2 + return 1 + ;; + esac } docker_image() { - image_name=`docker images |grep "$DOCKER_REPOSITORY/$DOCKER_IMAGES" |awk {'print $1'}| awk 'NR==1'` - image=$image_name + local ref + + ref=$(selected_image run) || return 1 + if [ -n "$ref" ] && image_exists "$ref"; then + image=$ref + else + image="" + fi } -download_config() { - mkdir -p config - if test curl; then - curl -o config/$CONFIG_FILE -LO https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/$CONFIG_FILE -s - elif test wget; then - wget -P -q config/ https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/$CONFIG_FILE +file_is_usable() { + [ -f "$1" ] && [ -s "$1" ] +} + +non_symlink_file_is_usable() { + [ ! -L "$1" ] && file_is_usable "$1" +} + +download_destination_is_replaceable() { + local output="$1" + + if [ -L "$output" ] || { [ -e "$output" ] && [ ! -f "$output" ]; }; then + echo "Download destination exists but is not a non-symbolic-link regular file: $output" >&2 + return 1 + fi +} + +download_file() { + local url="$1" + local output="$2" + local output_mode="${3:-}" + local output_dir + local temporary + + output_dir=$(dirname "$output") + mkdir -p "$output_dir" || return 1 + download_destination_is_replaceable "$output" || return 1 + temporary=$(mktemp "${output}.tmp.XXXXXX") || return 1 + + if command -v curl >/dev/null 2>&1; then + if ! curl -fsSL -o "$temporary" "$url"; then + rm -f "$temporary" + return 1 + fi + elif command -v wget >/dev/null 2>&1; then + if ! wget -q -O "$temporary" "$url"; then + rm -f "$temporary" + return 1 + fi + else + echo "Unable to download $url: install curl or wget first." + rm -f "$temporary" + return 1 + fi + + if [ ! -s "$temporary" ]; then + echo "Downloaded file is empty: $url" >&2 + rm -f "$temporary" + return 1 + fi + + if [ -n "$output_mode" ] && ! chmod "$output_mode" "$temporary"; then + echo "Unable to set mode $output_mode on downloaded file: $output" >&2 + rm -f "$temporary" + return 1 + fi + + if ! download_destination_is_replaceable "$output"; then + rm -f "$temporary" + return 1 + fi + if ! mv -f "$temporary" "$output"; then + rm -f "$temporary" + return 1 + fi + if ! non_symlink_file_is_usable "$output"; then + echo "Downloaded file did not produce a non-empty regular destination: $output" >&2 + return 1 fi } +download_config() { + local config_directory="$1" + local config_file="$2" + local config_url + + case "$config_file" in + "$PRIVATE_NET_CONFIG_FILE") + config_url=$PRIVATE_NET_CONFIG_URL + ;; + *) + echo "Unsupported configuration file: $config_file" >&2 + return 1 + ;; + esac + + echo "Downloading $config_file" + download_file "$config_url" "$config_directory/$config_file" 0644 +} check_download_config() { - if [[ ! -d 'config' || ! -f "config/$CONFIG_FILE" ]]; then - mkdir -p config - if test curl; then - curl -o config/$CONFIG_FILE -LO https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/$CONFIG_FILE -s - elif test wget; then - wget -P -q config/ https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/$CONFIG_FILE + local config_directory="$1" + local config_file="$2" + local config_path="$config_directory/$config_file" + + if ! non_symlink_file_is_usable "$config_path"; then + download_destination_is_replaceable "$config_path" || return 1 + echo "$config_path is missing or empty; downloading it for the initial run." + download_config "$config_directory" "$config_file" + fi +} + +normalize_data_directory() { + local requested_directory="$1" + local requested_parent + local normalized_directory + + if [[ "$requested_directory" != /* ]]; then + requested_directory="$(pwd)/$requested_directory" + fi + while [ "$requested_directory" != / ] && [[ "$requested_directory" == */ ]]; do + requested_directory=${requested_directory%/} + done + + if [ -e "$requested_directory" ] && [ ! -d "$requested_directory" ]; then + echo "run: data directory is not a directory: $requested_directory" >&2 + return 1 + fi + if [ -L "$requested_directory" ] && [ ! -d "$requested_directory" ]; then + echo "run: data directory link does not resolve to a directory: $requested_directory" >&2 + return 1 + fi + + requested_parent=${requested_directory%/*} + [ -n "$requested_parent" ] || requested_parent=/ + assert_trusted_path_prefixes "$requested_parent" || return 1 + + secure_mkdir_p "$requested_directory" || return 1 + if [ ! -d "$requested_directory" ]; then + echo "run: data directory is not a directory: $requested_directory" >&2 + return 1 + fi + + if ! normalized_directory=$(cd -P -- "$requested_directory" >/dev/null 2>&1 && pwd -P); then + echo "run: failed to resolve data directory: $requested_directory" >&2 + return 1 + fi + printf '%s\n' "$normalized_directory" +} + +secure_mkdir_p() ( + local current_umask + local secure_umask + + current_umask=$(umask) + if [[ ! "$current_umask" =~ ^[0-7]{3,4}$ ]]; then + echo "run: could not determine a safe directory-creation mask" >&2 + return 1 + fi + secure_umask=$((8#$current_umask | 0077)) + printf -v secure_umask '%04o' "$secure_umask" + umask "$secure_umask" + mkdir -p "$1" +) + +assert_trusted_path_prefixes() { + local path="$1" + local prefix=/ + local remainder=${path#/} + local component + local normalized_prefix + + while [ -n "$remainder" ]; do + component=${remainder%%/*} + if [ "$component" = "$remainder" ]; then + remainder= + else + remainder=${remainder#*/} + fi + [ -n "$component" ] || continue + + if [ "$prefix" = / ]; then + prefix="/$component" + else + prefix="$prefix/$component" + fi + if [ -d "$prefix" ]; then + if ! normalized_prefix=$(cd -P -- "$prefix" >/dev/null 2>&1 && pwd -P); then + echo "run: failed to resolve data directory path prefix: $prefix" >&2 + return 1 + fi + assert_trusted_data_directory "$normalized_prefix" || return 1 + fi + done +} + +directory_owner_and_mode() { + local directory="$1" + local metadata + + if metadata=$(stat -f '%u %Lp' "$directory" 2>/dev/null); then + printf '%s\n' "$metadata" + return 0 + fi + stat -c '%u %a' "$directory" 2>/dev/null +} + +assert_trusted_data_directory() { + local directory="$1" + local trusted_uid + local metadata + local owner_uid + local mode + local numeric_mode + + trusted_uid=$(id -u) || { + echo "run: failed to determine the current user ID" >&2 + return 1 + } + if [ "$trusted_uid" = 0 ] && [[ "${SUDO_UID:-}" =~ ^[0-9]+$ ]]; then + trusted_uid=$SUDO_UID + fi + + while :; do + if ! metadata=$(directory_owner_and_mode "$directory"); then + echo "run: failed to inspect data directory path: $directory" >&2 + return 1 + fi + owner_uid=${metadata%% *} + mode=${metadata#* } + if [[ ! "$owner_uid" =~ ^[0-9]+$ || ! "$mode" =~ ^[0-7]{3,4}$ ]]; then + echo "run: received invalid ownership metadata for data directory path: $directory" >&2 + return 1 + fi + if [ "$owner_uid" != 0 ] && [ "$owner_uid" != "$trusted_uid" ]; then + echo "run: data directory path must be owned by root or UID $trusted_uid: $directory (owner UID $owner_uid)" >&2 + return 1 + fi + numeric_mode=$((8#$mode)) + if ((numeric_mode & 0022)); then + echo "run: data directory path must not be group- or other-writable: $directory (mode $mode)" >&2 + return 1 + fi + + [ "$directory" = / ] && break + directory=${directory%/*} + [ -n "$directory" ] || directory=/ + done +} + +assert_managed_directory() { + local directory="$1" + + if [ -L "$directory" ]; then + echo "run: managed path must not be a symbolic link: $directory" >&2 + return 1 + fi + if [ ! -d "$directory" ]; then + echo "run: managed path is not a directory: $directory" >&2 + return 1 + fi +} + +prepare_managed_directory() { + local directory="$1" + + if [ -L "$directory" ]; then + echo "run: managed path must not be a symbolic link: $directory" >&2 + return 1 + fi + if [ -e "$directory" ] && [ ! -d "$directory" ]; then + echo "run: managed path is not a directory: $directory" >&2 + return 1 + fi + + secure_mkdir_p "$directory" || return 1 + assert_managed_directory "$directory" +} + +prepare_managed_config_directory() { + local directory="$1" + + prepare_managed_directory "$directory" || return 1 + if ! chmod 0755 "$directory"; then + echo "run: failed to set configuration-directory mode 0755: $directory" >&2 + return 1 + fi + assert_managed_directory "$directory" +} + +has_port_mapping() { + local expected_port="$1" + local expected_protocol="$2" + local mapping + local container_spec + local container_port + local protocol + shift 2 + + for mapping in "$@"; do + [ "$mapping" = "-p" ] && continue + container_spec="${mapping##*:}" + protocol="tcp" + if [[ "$container_spec" == */* ]]; then + protocol="${container_spec##*/}" + container_spec="${container_spec%/*}" + fi + container_port="$container_spec" + if [ "$container_port" = "$expected_port" ] && [ "$protocol" = "$expected_protocol" ]; then + return 0 + fi + done + return 1 +} + +has_volume_mount() { + local expected_target="$1" + local mapping + shift + + for mapping in "$@"; do + [ "$mapping" = "-v" ] && continue + if [[ "$mapping" == *":$expected_target" || "$mapping" == *":$expected_target:"* ]]; then + return 0 fi + done + return 1 +} + +image_architecture() { + local image_ref="$1" + + docker image inspect -f '{{.Architecture}}' "$image_ref" +} + +validate_image_user() { + local image_ref="$1" + local image_user + + if ! image_user=$(docker image inspect -f '{{.Config.User}}' "$image_ref"); then + echo "run: failed to inspect image user: $image_ref" >&2 + return 1 + fi + if [ "$image_user" != "$JAVA_TRON_UID:$JAVA_TRON_GID" ]; then + echo "run: image $image_ref must run as UID:GID $JAVA_TRON_UID:$JAVA_TRON_GID; found '${image_user:-root}'" >&2 + echo "Pull or build an updated non-root java-tron image before retrying." >&2 + return 1 + fi +} + +verify_private_config_readable() { + local image_ref="$1" + local config_directory="$2" + local config_file="$3" + local container_config="$CONFIG_PATH$config_file" + + assert_managed_directory "$config_directory" || return 1 + if docker run --rm \ + --user "$JAVA_TRON_UID:$JAVA_TRON_GID" \ + --security-opt no-new-privileges \ + --network none \ + --read-only \ + --cap-drop ALL \ + --entrypoint sh \ + -v "$config_directory:/java-tron/config:ro" \ + "$image_ref" \ + -ec 'test ! -L "$1" && test -f "$1" && test -s "$1" && test -r "$1"' \ + sh "$container_config"; then + assert_managed_directory "$config_directory" + return + fi + + echo "run: private configuration must be readable by java-tron UID:GID $JAVA_TRON_UID:$JAVA_TRON_GID: $config_directory/$config_file" >&2 + echo "Set the directory mode to 0755 and the public configuration-template mode to 0644, then retry." >&2 + return 1 +} + +append_jdk8_direct_memory() { + local image_ref="$1" + local architecture + + if ! architecture=$(image_architecture "$image_ref"); then + echo "run: failed to inspect image architecture: $image_ref" >&2 + return 1 + fi + + case "$architecture" in + amd64|386) + jvm_opts="$jvm_opts -XX:MaxDirectMemorySize=1g" + ;; + esac +} + +docker_namespace_mode() { + local security_options + + if ! security_options=$(docker info --format '{{json .SecurityOptions}}'); then + echo "run: failed to inspect Docker user-namespace configuration" >&2 + return 1 + fi + + if [[ "$security_options" == *'name=rootless'* ]]; then + printf '%s\n' rootless + elif [[ "$security_options" == *'name=userns'* ]]; then + printf '%s\n' rootful-userns-remap + else + printf '%s\n' rootful fi } +prepare_runtime_directories() { + local image_ref="$1" + local host_directory + local container_directory + local first_entry + local host_inspection_succeeded + local metadata + local owner_uid + local mode + local numeric_mode + local namespace_mode + local rootful_userns_remap=false + local directory_existed + local -a mount_args=() + local -a host_directories=() + local -a container_directories=() + local -a initialize_mount_args=() + local -a initialize_host_directories=() + local -a initialize_directories=() + shift + + while [ $# -gt 0 ]; do + host_directory="$1" + container_directory="$2" + shift 2 + + directory_existed=false + if [ -d "$host_directory" ] && [ ! -L "$host_directory" ]; then + directory_existed=true + fi + prepare_managed_directory "$host_directory" || return 1 + host_inspection_succeeded=false + first_entry="" + if first_entry=$(find "$host_directory" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null); then + host_inspection_succeeded=true + else + if ! metadata=$(directory_owner_and_mode "$host_directory"); then + echo "run: failed to inspect runtime directory: $host_directory" >&2 + return 1 + fi + owner_uid=${metadata%% *} + mode=${metadata#* } + if [[ ! "$owner_uid" =~ ^[0-9]+$ || ! "$mode" =~ ^[0-7]{3,4}$ ]]; then + echo "run: received invalid metadata for runtime directory: $host_directory" >&2 + return 1 + fi + numeric_mode=$((8#$mode)) + if ((numeric_mode & 0077)); then + echo "run: host-unreadable runtime directory must use mode 0700: $host_directory (mode $mode)" >&2 + printf 'Stop the node and restrict it before retrying: chmod 0700 %q\n' \ + "$host_directory" >&2 + return 1 + fi + + # A previous initialization can leave a private (0700) directory owned + # by either the runtime UID or its user-namespace-mapped host UID. The + # host cannot enumerate it, so validate effective access from inside the + # restricted container below without widening its mode. + assert_managed_directory "$host_directory" || return 1 + fi + + if [ "$host_inspection_succeeded" = true ] && [ -z "$first_entry" ]; then + if ! chmod 0700 "$host_directory"; then + echo "run: failed to set empty runtime-directory mode 0700: $host_directory" >&2 + return 1 + fi + elif [ "$host_inspection_succeeded" = true ] \ + && [ "$directory_existed" = true ] && [ -n "$first_entry" ]; then + if ! metadata=$(directory_owner_and_mode "$host_directory"); then + echo "run: failed to inspect runtime-directory mode: $host_directory" >&2 + return 1 + fi + mode=${metadata#* } + if [[ ! "$mode" =~ ^[0-7]{3,4}$ ]]; then + echo "run: received invalid mode metadata for runtime directory: $host_directory" >&2 + return 1 + fi + numeric_mode=$((8#$mode)) + if ((numeric_mode & 0077)); then + echo "run: warning: existing non-empty runtime directory is accessible by group or other users; preserving mode $mode: $host_directory" >&2 + printf 'Restrict it while the node is stopped: chmod 0700 %q\n' "$host_directory" >&2 + fi + fi + + mount_args+=("-v" "$host_directory:$container_directory") + host_directories+=("$host_directory") + container_directories+=("$container_directory") + if [ "$host_inspection_succeeded" = true ] && [ -z "$first_entry" ]; then + initialize_mount_args+=("-v" "$host_directory:$container_directory") + initialize_host_directories+=("$host_directory") + initialize_directories+=("$container_directory") + fi + done + + if [ ${#initialize_directories[@]} -gt 0 ]; then + for host_directory in "${initialize_host_directories[@]}"; do + assert_managed_directory "$host_directory" || return 1 + done + + namespace_mode=$(docker_namespace_mode) || return 1 + if [ "$namespace_mode" = rootful-userns-remap ]; then + # Remapped root generally cannot chown a directory created by the host + # user. Skip the privileged attempt and let the runtime-identity + # preflight below accept correctly pre-provisioned mapped ownership. + rootful_userns_remap=true + else + if ! docker run --rm \ + --pull missing \ + --user 0:0 \ + --security-opt no-new-privileges \ + --network none \ + --read-only \ + --cap-drop ALL \ + --cap-add CHOWN \ + --entrypoint chown \ + "${initialize_mount_args[@]}" \ + "$RUNTIME_INIT_IMAGE" \ + "$JAVA_TRON_UID:$JAVA_TRON_GID" \ + "${initialize_directories[@]}"; then + echo "run: failed to initialize runtime-directory ownership" >&2 + return 1 + fi + fi + fi + + for host_directory in "${host_directories[@]}"; do + assert_managed_directory "$host_directory" || return 1 + done + + if docker run --rm \ + --user "$JAVA_TRON_UID:$JAVA_TRON_GID" \ + --security-opt no-new-privileges \ + --network none \ + --read-only \ + --cap-drop ALL \ + --entrypoint sh \ + "${mount_args[@]}" \ + "$image_ref" \ + -ec ' + for path do + test -w "$path" || exit 1 + if ! first_unwritable=$(find "$path" -mindepth 1 -maxdepth 1 ! -writable -print -quit); then + exit 1 + fi + test -z "$first_unwritable" || exit 1 + done + ' sh "${container_directories[@]}"; then + return 0 + fi + + if [ "$rootful_userns_remap" = true ]; then + echo "run: rootful Docker userns-remap cannot automatically initialize host-user-owned runtime directories." >&2 + echo "Pre-create empty directories with the host UID:GID mapped from container $JAVA_TRON_UID:$JAVA_TRON_GID, then retry." >&2 + printf 'Affected directories:' >&2 + for host_directory in "${initialize_host_directories[@]}"; do + printf ' %q' "$host_directory" >&2 + done + printf '\n' >&2 + echo "See docker.md for a mapped-ID provisioning example." >&2 + fi + echo "run: runtime directories must be writable by java-tron UID:GID $JAVA_TRON_UID:$JAVA_TRON_GID." >&2 + echo "Stop the node and migrate existing data with the same Docker daemon and user namespace before retrying." >&2 + echo "For rootless Docker or userns-remap, use the host UID:GID mapped from container $JAVA_TRON_UID:$JAVA_TRON_GID by that daemon." >&2 + echo "For rootful Docker without user-namespace remapping:" >&2 + printf ' sudo chown -R %s:%s' "$JAVA_TRON_UID" "$JAVA_TRON_GID" >&2 + for host_directory in "${host_directories[@]}"; do + printf ' %q' "$host_directory" >&2 + done + printf '\n' >&2 + return 1 +} + run() { - docker_image + local docker_memory="$DOCKER_MEMORY" + local jvm_opts="$JVM_OPTS" + local jvm_opts_replaced=false + local data_dir="$DEFAULT_DATA_DIR" + local config_directory + local output_directory + local logs_directory + local env_name + local -a volume_args=() + local -a port_args=() + local -a environment_args=() + local -a tron_args=() + local -a fullnode_args=() + local -a default_runtime_directories=() + local custom_config=false + local default_config_mount=false + local manages_data_directory=false + local runtime_directory_index + + while [ $# -gt 0 ]; do + case "$1" in + -v) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + volume_args+=("-v" "$2") + shift 2 + ;; + -p) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + port_args+=("-p" "$2") + shift 2 + ;; + -e|--env) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + env_name="${2%%=*}" + case "$env_name" in + JAVA_OPTS|FULL_NODE_OPTS|JAVA_TOOL_OPTIONS|_JAVA_OPTIONS|JDK_JAVA_OPTIONS) + echo "run: $1 $env_name is not supported; use --jvm-opts to set JVM options" >&2 + return 1 + ;; + esac + environment_args+=("--env" "$2") + shift 2 + ;; + -c) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + tron_args=("-c" "$2") + UPDATE_CONFIG=false + custom_config=true + shift 2 + ;; + --net) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + if [[ "$2" = "main" ]]; then + CONFIG_FILE="" + elif [[ "$2" = "private" ]]; then + CONFIG_FILE=$PRIVATE_NET_CONFIG_FILE + else + echo "run: network $2 is not valid; expected main or private" + return 1 + fi + shift 2 + ;; + --update-config) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + if [[ "$2" != "true" && "$2" != "false" ]]; then + echo "run: arg $1 must be true or false" + return 1 + fi + UPDATE_CONFIG=$2 + shift 2 + ;; + --memory) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + docker_memory=$2 + shift 2 + ;; + --data-dir) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + data_dir=$2 + shift 2 + ;; + --jvm-opts) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + jvm_opts=$2 + jvm_opts_replaced=true + shift 2 + ;; + --image) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + IMAGE_OVERRIDE=$2 + shift 2 + ;; + --container-name) + if [ $# -lt 2 ]; then + echo "run: arg $1 requires a value" + return 1 + fi + set_container_name run "$2" || return 1 + shift 2 + ;; + --) + shift + fullnode_args=("$@") + break + ;; + *) + echo "run: arg $1 is not a valid parameter" + return 1 + ;; + esac + done + + docker_ps || return 1 + if [ -n "$cid" ]; then + echo "container $CONTAINER_NAME already exists (ID: $cid)." >&2 + echo "Use --start to reuse it, or --rm before creating a new container." >&2 + return 1 + fi + + docker_image || return 1 + + if [ -z "$image" ]; then + if [ -n "$IMAGE_OVERRIDE" ]; then + echo "run: image not found: $IMAGE_OVERRIDE" >&2 + return 1 + fi + echo "run: compatible local image not found: $RUN_IMAGE_DEFAULT" >&2 + echo "Build it with: bash docker.sh --build" >&2 + echo "Or select a compatible image with: bash docker.sh --run --image NAME[:TAG]" >&2 + return 1 + fi + + validate_image_user "$image" || return 1 + + if [ "$custom_config" = false ] && [ -n "$CONFIG_FILE" ] \ + && ! has_volume_mount "/java-tron/config" "${volume_args[@]}"; then + default_config_mount=true + fi + + if [ "$default_config_mount" = true ] \ + || ! has_volume_mount "/java-tron/output-directory" "${volume_args[@]}" \ + || ! has_volume_mount "/java-tron/logs" "${volume_args[@]}"; then + manages_data_directory=true + fi - if [ ! $image ] ; then - echo 'warning: no java-tron mirror image, do you need to get the mirror image?[y/n]' - read need + if [ "$manages_data_directory" = true ]; then + data_dir=$(normalize_data_directory "$data_dir") || return 1 + assert_trusted_data_directory "$data_dir" || return 1 + config_directory="$data_dir/config" + output_directory="$data_dir/output-directory" + logs_directory="$data_dir/logs" + fi - if [[ $need == 'y' || $need == 'yes' ]]; then - pull + if [ "$default_config_mount" = true ]; then + prepare_managed_config_directory "$config_directory" || return 1 + fi + + if [ "$default_config_mount" = true ]; then + if [ "$UPDATE_CONFIG" = true ]; then + download_config "$config_directory" "$CONFIG_FILE" || return 1 else - echo "warning: no mirror image found, go ahead and download a mirror." - exit + check_download_config "$config_directory" "$CONFIG_FILE" || return 1 fi fi - volume="" - parameter="" - tron_parameter="" - if [ $# -gt 0 ]; then - while [ -n "$1" ]; do - case "$1" in - -v) - volume="$volume -v $2" - shift 2 - ;; - -p) - parameter="$parameter -p $2" - shift 2 - ;; - -c) - tron_parameter="$tron_parameter -c $2" - UPDATE_CONFIG=false - shift 2 - ;; - --net) - if [[ "$2" = "main" ]]; then - CONFIG_FILE=$MAIN_NET_CONFIG_FILE - elif [[ "$2" = "test" ]]; then - CONFIG_FILE=$TEST_NET_CONFIG_FILE - elif [[ "$2" = "private" ]]; then - CONFIG_FILE=$PRIVATE_NET_CONFIG_FILE - fi - shift 2 - ;; - --update-config) - UPDATE_CONFIG=$2 - shift 2 + if [ "$default_config_mount" = true ]; then + volume_args+=("-v" "$config_directory:/java-tron/config:ro") + verify_private_config_readable "$image" "$config_directory" "$CONFIG_FILE" || return 1 + fi + if ! has_volume_mount "/java-tron/output-directory" "${volume_args[@]}"; then + default_runtime_directories+=("$output_directory" "/java-tron/output-directory") + volume_args+=("-v" "$output_directory:/java-tron/output-directory") + fi + if ! has_volume_mount "/java-tron/logs" "${volume_args[@]}"; then + default_runtime_directories+=("$logs_directory" "/java-tron/logs") + volume_args+=("-v" "$logs_directory:/java-tron/logs") + fi + + if [ ${#default_runtime_directories[@]} -gt 0 ]; then + prepare_runtime_directories "$image" "${default_runtime_directories[@]}" || return 1 + fi + + if ! has_port_mapping "$DOCKER_HTTP_PORT" "tcp" "${port_args[@]}"; then + port_args+=("-p" "$HOST_HTTP_BIND_ADDRESS:$HOST_HTTP_PORT:$DOCKER_HTTP_PORT") + fi + if ! has_port_mapping "$DOCKER_RPC_PORT" "tcp" "${port_args[@]}"; then + port_args+=("-p" "$HOST_RPC_BIND_ADDRESS:$HOST_RPC_PORT:$DOCKER_RPC_PORT") + fi + if ! has_port_mapping "$DOCKER_LISTEN_PORT" "tcp" "${port_args[@]}"; then + port_args+=("-p" "$HOST_LISTEN_PORT:$DOCKER_LISTEN_PORT") + fi + if ! has_port_mapping "$DOCKER_LISTEN_PORT" "udp" "${port_args[@]}"; then + port_args+=("-p" "$HOST_LISTEN_PORT:$DOCKER_LISTEN_PORT/udp") + fi + + if [ ${#tron_args[@]} -eq 0 ]; then + if [ -n "$CONFIG_FILE" ]; then + tron_args=("-c" "$CONFIG_PATH$CONFIG_FILE") + else + tron_args=("-c" "$MAIN_NET_CONFIG_PATH") + fi + fi + + if [ "$jvm_opts_replaced" = false ]; then + append_jdk8_direct_memory "$image" || return 1 + fi + + if [ "$default_config_mount" = true ]; then + assert_managed_directory "$config_directory" || return 1 + fi + for ((runtime_directory_index=0; + runtime_directory_index<${#default_runtime_directories[@]}; + runtime_directory_index+=2)); do + assert_managed_directory "${default_runtime_directories[$runtime_directory_index]}" || return 1 + done + + docker run -d --name "$CONTAINER_NAME" \ + --user "$JAVA_TRON_UID:$JAVA_TRON_GID" \ + "${volume_args[@]}" \ + "${port_args[@]}" \ + --memory "$docker_memory" \ + --env "JAVA_OPTS=$jvm_opts" \ + "${environment_args[@]}" \ + --security-opt no-new-privileges \ + --restart always \ + "$image" \ + "${tron_args[@]}" \ + "${fullnode_args[@]}" +} + +validate_local_image_config() { + local config_path="$1" + local sensitive_setting + + # config.conf is copied verbatim into the image. Recognize the supported + # HOCON forms for settings that can directly contain signing keys or + # service credentials, while ignoring line comments outside quoted strings. + if ! sensitive_setting=$(awk ' + function uncomment(value, output, position, character, next_character, quoted, escaped) { + output = "" + quoted = 0 + escaped = 0 + for (position = 1; position <= length(value); position++) { + character = substr(value, position, 1) + next_character = substr(value, position + 1, 1) + if (quoted) { + output = output character + if (escaped) { + escaped = 0 + } else if (character == "\\") { + escaped = 1 + } else if (character == "\"") { + quoted = 0 + } + } else if (character == "\"") { + quoted = 1 + output = output character + } else if (character == "#" || (character == "/" && next_character == "/")) { + return output + } else { + output = output character + } + } + return output + } + + function witness_fragment_has_value(value, compacted) { + compacted = value + gsub(/[[:space:],]/, "", compacted) + while (sub(/""/, "", compacted)) { + } + return compacted != "" + } + + function scalar_secret_value_is_nonempty(value, remainder) { + sub(/^[[:space:]]*/, "", value) + if (substr(value, 1, 2) != "\"\"") { + return 1 + } + remainder = substr(value, 3) + sub(/^[[:space:]]*/, "", remainder) + return remainder != "" && substr(remainder, 1, 1) !~ /[,}]/ + } + + { + line = uncomment($0) + + if (in_witness_list) { + closing_bracket = index(line, "]") + fragment = closing_bracket ? substr(line, 1, closing_bracket - 1) : line + if (witness_fragment_has_value(fragment)) { + sensitive_setting = "localwitness" + exit + } + if (!closing_bracket) { + next + } + in_witness_list = 0 + line = substr(line, closing_bracket + 1) + remainder = line + sub(/^[[:space:]]*/, "", remainder) + if (remainder != "" && substr(remainder, 1, 1) !~ /[,}]/) { + sensitive_setting = "localwitness" + exit + } + } + + database_line = line + dns_private_line = line + dns_access_secret_line = line + while (match(line, /(^|[[:space:]{,.])("localwitness"|localwitness)[[:space:]]*([+]?=|:)/)) { + value = substr(line, RSTART + RLENGTH) + sub(/^[[:space:]]*/, "", value) + if (substr(value, 1, 1) != "[") { + sensitive_setting = "localwitness" + exit + } + value = substr(value, 2) + closing_bracket = index(value, "]") + fragment = closing_bracket ? substr(value, 1, closing_bracket - 1) : value + if (witness_fragment_has_value(fragment)) { + sensitive_setting = "localwitness" + exit + } + if (!closing_bracket) { + in_witness_list = 1 + line = "" + } else { + line = substr(value, closing_bracket + 1) + remainder = line + sub(/^[[:space:]]*/, "", remainder) + if (remainder != "" && substr(remainder, 1, 1) !~ /[,}]/) { + sensitive_setting = "localwitness" + exit + } + } + } + + while (match(database_line, /(^|[[:space:]{,.])("dbconfig"|dbconfig)[[:space:]]*([+]?=|:)/)) { + value = substr(database_line, RSTART + RLENGTH) + if (scalar_secret_value_is_nonempty(value)) { + sensitive_setting = "event.subscribe.dbconfig" + exit + } + database_line = substr(value, 3) + } + + while (match(dns_private_line, /(^|[[:space:]{,.])("dnsPrivate"|dnsPrivate)[[:space:]]*([+]?=|:)/)) { + value = substr(dns_private_line, RSTART + RLENGTH) + if (scalar_secret_value_is_nonempty(value)) { + sensitive_setting = "node.dns.dnsPrivate" + exit + } + dns_private_line = substr(value, 3) + } + + while (match(dns_access_secret_line, /(^|[[:space:]{,.])("accessKeySecret"|accessKeySecret)[[:space:]]*([+]?=|:)/)) { + value = substr(dns_access_secret_line, RSTART + RLENGTH) + if (scalar_secret_value_is_nonempty(value)) { + sensitive_setting = "node.dns.accessKeySecret" + exit + } + dns_access_secret_line = substr(value, 3) + } + } + + END { + if (sensitive_setting != "") { + print sensitive_setting + } + } + ' "$config_path"); then + echo "build: failed to inspect local configuration: $config_path" >&2 + return 1 + fi + + if [ -n "$sensitive_setting" ]; then + echo "build: refusing to bake non-empty plaintext $sensitive_setting into the image: $config_path" >&2 + echo "Clear the setting and provide sensitive signing, database, or DNS credentials through a protected runtime mount." >&2 + return 1 + fi +} + +# shellcheck disable=SC2329 # Called by functions that are invoked from EXIT traps. +remove_local_build_tree() { + local target_path="$1" + local find_root="$target_path" + + if [ ! -e "$target_path" ] && [ ! -L "$target_path" ]; then + return 0 + fi + + # Prefix relative paths so find cannot interpret a leading dash as an + # expression. -P prevents an archive-created symlink from redirecting chmod. + case "$find_root" in + /*) + ;; + *) + find_root="./$find_root" + ;; + esac + + # ZIP directory modes are preserved by unzip. Restore owner access one + # directory at a time, before descent, so mode 000/0500 entries can be + # removed without following symbolic links outside the private tree. + find -P "$find_root" -type d -exec chmod u+rwx {} \; || true + + if ! rm -rf -- "$target_path"; then + echo "build: failed to remove private build tree: $target_path" >&2 + return 1 + fi + if [ -e "$target_path" ] || [ -L "$target_path" ]; then + echo "build: private build tree still exists after cleanup: $target_path" >&2 + return 1 + fi +} + +write_local_build_dockerignore() { + local output_path="$1" + + cat > "$output_path" <<'EOF' +# Remote targets do not read from the build context. Local targets accept only +# the runtime files emitted by the supported Gradle distribution plus the +# Mainnet configuration staged by docker.sh. Everything else stays excluded. +** +!java-tron/ +java-tron/** +!java-tron/bin/ +java-tron/bin/** +!java-tron/bin/FullNode +!java-tron/bin/FullNode.bat +!java-tron/bin/java-tron.vmoptions +!java-tron/lib/ +java-tron/lib/** +!java-tron/lib/*.jar +!java-tron/config.conf +EOF +} + +validate_local_distribution_tree() { + local staging_root="$1" + local manifest_path="$2" + local distribution_name="java-tron-1.0.0" + local distribution_root="$staging_root/$distribution_name" + local entry + local relative + local library_name + + if [ ! -d "$distribution_root" ] || [ -L "$distribution_root" ]; then + echo "build: the distribution does not contain a regular $distribution_name directory" >&2 + return 1 + fi + + if ! find -P "$staging_root" -mindepth 1 -print0 > "$manifest_path"; then + echo "build: failed to inspect the extracted local distribution" >&2 + return 1 + fi + + while IFS= read -r -d '' entry; do + relative=${entry#"$staging_root"/} + + if [ -L "$entry" ]; then + printf 'build: refusing symbolic link in local distribution: %q\n' \ + "$relative" >&2 + return 1 + fi + + if [ -d "$entry" ]; then + case "$relative" in + "$distribution_name"|"$distribution_name/bin"|"$distribution_name/lib") ;; *) - echo "run: arg $1 is not a valid parameter" - exit + printf 'build: refusing unexpected directory in local distribution: %q\n' \ + "$relative" >&2 + return 1 ;; esac - done - if [ $UPDATE_CONFIG = true ]; then - download_config + continue fi - if [ -z "$volume" ]; then - volume=" -v $CONFIG:/java-tron/config -v $OUTPUT_DIRECTORY:/java-tron/output-directory" + if [ ! -f "$entry" ]; then + printf 'build: refusing non-regular file in local distribution: %q\n' \ + "$relative" >&2 + return 1 fi - if [ -z "$parameter" ]; then - parameter=" -p $HOST_HTTP_PORT:$DOCKER_HTTP_PORT -p $HOST_RPC_PORT:$DOCKER_RPC_PORT -p $HOST_LISTEN_PORT:$DOCKER_LISTEN_PORT" + case "$relative" in + "$distribution_name/bin/FullNode"|\ + "$distribution_name/bin/FullNode.bat"|\ + "$distribution_name/bin/java-tron.vmoptions") + ;; + "$distribution_name/lib/"*.jar) + library_name=${relative#"$distribution_name/lib/"} + if [ -z "$library_name" ] || [[ "$library_name" = */* ]]; then + printf 'build: refusing unexpected library path in local distribution: %q\n' \ + "$relative" >&2 + return 1 + fi + ;; + *) + printf 'build: refusing unexpected or sensitive file in local distribution: %q\n' \ + "$relative" >&2 + return 1 + ;; + esac + done < "$manifest_path" +} + +prepare_local_build_context() ( + local source_root="$1" + local dockerfile_path="$2" + local context_root="$3" + local distribution="$source_root/framework/build/distributions/java-tron-1.0.0.zip" + local config_path="$source_root/framework/src/main/resources/config.conf" + local distribution_staging="" + local distribution_manifest="" + local distribution_root + + # shellcheck disable=SC2329 # Invoked indirectly by the EXIT trap. + cleanup_local_distribution_staging() { + local original_status=$? + local cleanup_status=0 + + trap - EXIT + if [ -n "$distribution_staging" ]; then + remove_local_build_tree "$distribution_staging" || cleanup_status=$? + fi + if [ -n "$distribution_manifest" ]; then + if ! rm -f -- "$distribution_manifest"; then + echo "build: failed to remove distribution manifest: $distribution_manifest" >&2 + cleanup_status=1 + fi fi - if [ -z "$tron_parameter" ]; then - tron_parameter=" -c $CONFIG_PATH$CONFIG_FILE" + if [ "$original_status" -ne 0 ]; then + exit "$original_status" fi + if [ "$cleanup_status" -ne 0 ]; then + exit "$cleanup_status" + fi + exit 0 + } + trap cleanup_local_distribution_staging EXIT - # Using custom parameters - docker run -d -it --name "$DOCKER_REPOSITORY-$DOCKER_IMAGES" \ - $volume \ - $parameter \ - --restart always \ - "$DOCKER_REPOSITORY/$DOCKER_IMAGES:$DOCKER_TARGET" \ - $tron_parameter - else - if [ $UPDATE_CONFIG = true ]; then - download_config + if ! command -v unzip >/dev/null 2>&1; then + echo "build: unzip is required for --source local" >&2 + return 1 + fi + + if [ ! -f "$config_path" ]; then + echo "build: local configuration does not exist: $config_path" >&2 + return 1 + fi + if ! validate_local_image_config "$config_path"; then + return 1 + fi + + echo "Building the java-tron distribution from local source: $source_root" + if ! (cd -- "$source_root" && ./gradlew :framework:distZip -x test -x check --no-daemon); then + echo "build: failed to create the local java-tron distribution" >&2 + return 1 + fi + if [ ! -f "$distribution" ]; then + echo "build: expected distribution does not exist: $distribution" >&2 + return 1 + fi + + distribution_staging=$(mktemp -d "$context_root/.java-tron-dist.XXXXXX") \ + || return 1 + distribution_manifest=$(mktemp "$context_root/.java-tron-dist-manifest.XXXXXX") \ + || return 1 + + if ! unzip -q -o "$distribution" -d "$distribution_staging"; then + echo "build: failed to extract $distribution" >&2 + return 1 + fi + if ! validate_local_distribution_tree \ + "$distribution_staging" "$distribution_manifest"; then + return 1 + fi + distribution_root="$distribution_staging/java-tron-1.0.0" + mv "$distribution_root" "$context_root/java-tron" || return 1 + + if [ ! -x "$context_root/java-tron/bin/FullNode" ] \ + || [ ! -f "$context_root/java-tron/bin/java-tron.vmoptions" ]; then + echo "build: the staged distribution is missing FullNode or java-tron.vmoptions" >&2 + return 1 + fi + + cp "$config_path" "$context_root/java-tron/config.conf" || return 1 + cp "$dockerfile_path" "$context_root/Dockerfile" || return 1 + write_local_build_dockerignore "$context_root/.dockerignore" || return 1 +) + +build_local_image() ( + local source_root="$1" + local dockerfile_path="$2" + local build_context + + # shellcheck disable=SC2329 # Invoked indirectly by the EXIT trap. + cleanup_temporary_build_context() { + local original_status=$? + local cleanup_status=0 + + trap - EXIT + remove_local_build_tree "$build_context" || cleanup_status=$? + if [ "$original_status" -ne 0 ]; then + exit "$original_status" + fi + if [ "$cleanup_status" -ne 0 ]; then + exit "$cleanup_status" + fi + exit 0 + } + + build_context=$(mktemp -d) || return 1 + trap cleanup_temporary_build_context EXIT + + prepare_local_build_context "$source_root" "$dockerfile_path" "$build_context" \ + || return 1 + + echo "Building the local image from a temporary distribution-only context." + DOCKER_BUILDKIT=1 docker build \ + --target local \ + --file "$build_context/Dockerfile" \ + -t "$(selected_image build)" \ + "$build_context" +) + +export_local_build_context() ( + local source_root="$1" + local dockerfile_path="$2" + local build_context="$3" + local context_created=false + + # shellcheck disable=SC2329 # Invoked indirectly by the EXIT trap. + cleanup_failed_export() { + local status=$? + local cleanup_status=0 + + trap - EXIT + if [ "$status" -ne 0 ] && [ "$context_created" = true ]; then + remove_local_build_tree "$build_context" || cleanup_status=$? fi - # Default parameters - docker run -d -it --name "$DOCKER_REPOSITORY-$DOCKER_IMAGES" \ - -v $CONFIG:/java-tron/config \ - -v $OUTPUT_DIRECTORY:/java-tron/output-directory \ - -p $HOST_HTTP_PORT:$DOCKER_HTTP_PORT \ - -p $HOST_RPC_PORT:$DOCKER_RPC_PORT \ - -p $HOST_LISTEN_PORT:$DOCKER_LISTEN_PORT \ - --restart always \ - "$DOCKER_REPOSITORY/$DOCKER_IMAGES:$DOCKER_TARGET" \ - -c "$CONFIG_PATH$CONFIG_FILE" + if [ "$status" -ne 0 ]; then + exit "$status" + fi + if [ "$cleanup_status" -ne 0 ]; then + exit "$cleanup_status" + fi + exit 0 + } + trap cleanup_failed_export EXIT + + if [ -e "$build_context" ] || [ -L "$build_context" ]; then + echo "build: export context already exists: $build_context" >&2 + return 1 fi -} + if ! mkdir -m 700 -- "$build_context"; then + echo "build: failed to create export context: $build_context" >&2 + return 1 + fi + context_created=true + + prepare_local_build_context "$source_root" "$dockerfile_path" "$build_context" \ + || return 1 + + echo "Prepared local Docker build context: $build_context" +) + +build_remote_image() ( + local dockerfile_path="$1" + local source_repository="$2" + local source_ref="$3" + local build_context + + build_context=$(mktemp -d) || return 1 + trap 'rm -rf "$build_context"' EXIT + cp "$dockerfile_path" "$build_context/Dockerfile" || return 1 + + echo "Building remote java-tron source '$source_ref' from $source_repository." + echo "Local working-tree changes are not included; use --source local to include them." + DOCKER_BUILDKIT=1 docker build \ + --pull \ + --no-cache-filter remote-builder \ + --target remote \ + --file "$build_context/Dockerfile" \ + --build-arg "SOURCE_REPOSITORY=$source_repository" \ + --build-arg "SOURCE_REF=$source_ref" \ + -t "$(selected_image build)" \ + "$build_context" +) build() { - echo 'docker build' - if [ ! -f "Dockerfile" ]; then - echo 'warning: Dockerfile not exists.' - if test curl; then - DOWNLOAD_CMD="curl -LJO " - elif test wget; then - DOWNLOAD_CMD="wget " + local architecture + local dockerfile_path + local dockerfile_relative + local script_dir + local source_root + local source_mode="remote" + local source_ref="$JAVA_TRON_SOURCE_REF" + local source_repository="$JAVA_TRON_SOURCE_REPOSITORY" + local source_ref_set=false + local source_repository_set=false + local export_context="" + + while [ $# -gt 0 ]; do + case "$1" in + --source) + if [ $# -lt 2 ]; then + echo "build: arg $1 requires a value" + return 1 + fi + if [[ "$2" != "local" && "$2" != "remote" ]]; then + echo "build: source $2 is not valid; expected local or remote" + return 1 + fi + source_mode=$2 + shift 2 + ;; + --source-ref) + if [ $# -lt 2 ]; then + echo "build: arg $1 requires a value" + return 1 + fi + source_ref=$2 + source_ref_set=true + shift 2 + ;; + --source-repository) + if [ $# -lt 2 ]; then + echo "build: arg $1 requires a value" + return 1 + fi + source_repository=$2 + source_repository_set=true + shift 2 + ;; + --export-context) + if [ $# -lt 2 ]; then + echo "build: arg $1 requires a value" + return 1 + fi + export_context=$2 + shift 2 + ;; + --image) + if [ $# -lt 2 ]; then + echo "build: arg $1 requires a value" + return 1 + fi + IMAGE_OVERRIDE=$2 + shift 2 + ;; + *) + echo "build: arg $1 is not a valid parameter" + return 1 + ;; + esac + done + + if [ "$source_mode" = "local" ] && { [ "$source_ref_set" = true ] || [ "$source_repository_set" = true ]; }; then + echo "build: --source-ref and --source-repository can only be used with --source remote" + return 1 + fi + if [ -n "$export_context" ] && [ "$source_mode" != "local" ]; then + echo "build: --export-context can only be used with --source local" + return 1 + fi + + script_dir=$DOCKER_SCRIPT_DIR + architecture=$(uname -m) + case "$architecture" in + x86_64|amd64) + dockerfile_relative="Dockerfile" + ;; + arm64|aarch64) + dockerfile_relative="arm64/Dockerfile" + ;; + *) + echo "Unsupported architecture: $architecture; expected x86_64, amd64, arm64, or aarch64." + return 1 + ;; + esac + + if [ "$source_mode" = "local" ]; then + if [ -x "$(pwd)/gradlew" ]; then + source_root=$(pwd) + elif [ -x "$script_dir/gradlew" ]; then + source_root=$script_dir + elif [ -x "$script_dir/../gradlew" ]; then + source_root=$(cd -- "$script_dir/.." >/dev/null 2>&1 && pwd) + else + echo "build: unable to find a java-tron checkout for local source" + echo "Run this command from the repository root or use docker/docker.sh from a checkout." + return 1 + fi + + dockerfile_path="$source_root/docker/$dockerfile_relative" + if ! file_is_usable "$dockerfile_path"; then + echo "build: local Dockerfile does not exist: $dockerfile_path" >&2 + return 1 + fi + if [ -n "$export_context" ]; then + export_local_build_context "$source_root" "$dockerfile_path" "$export_context" else - echo "Dockerfile cannot be downloaded, you need to install 'curl' or 'wget'!" - exit + build_local_image "$source_root" "$dockerfile_path" fi - # download Dockerfile - `$DOWNLOAD_CMD "$JAVA_TRON_REPOSITORY$DOCKER_FILE"` - `$DOWNLOAD_CMD "$JAVA_TRON_REPOSITORY$ENDPOINT_SHELL"` - chmod u+rwx $ENDPOINT_SHELL + else + dockerfile_path="$script_dir/$dockerfile_relative" + if ! file_is_usable "$dockerfile_path"; then + echo "$dockerfile_relative does not exist; downloading it." + download_file "$JAVA_TRON_DOCKER_REPOSITORY/$dockerfile_relative" "$dockerfile_path" || return 1 + fi + build_remote_image "$dockerfile_path" "$source_repository" "$source_ref" fi - docker build -t "$DOCKER_REPOSITORY/$DOCKER_IMAGES:$DOCKER_TARGET" . } pull() { - echo "docker pull $DOCKER_REPOSITORY/$DOCKER_IMAGES:$DOCKER_TARGET" - docker pull "$DOCKER_REPOSITORY/$DOCKER_IMAGES:$DOCKER_TARGET" + local image_name + + while [ $# -gt 0 ]; do + case "$1" in + --image) + if [ $# -lt 2 ]; then + echo "pull: arg $1 requires a value" + return 1 + fi + IMAGE_OVERRIDE=$2 + shift 2 + ;; + *) + echo "pull: arg $1 is not a valid parameter" + return 1 + ;; + esac + done + + image_name=$(selected_image pull) || return 1 + echo "docker pull $image_name" + docker pull "$image_name" || return 1 + validate_image_user "$image_name" } start() { - docker_ps - if [ $cid ]; then + apply_container_name_args start "$@" || return 1 + docker_ps || return 1 + if [ -n "$cid" ]; then echo "containerID: $cid" - echo "docker stop $cid" - docker start $cid - docker ps + echo "docker start $cid" + docker start "$cid" || return 1 + docker ps || return 1 else - echo "container not running!" + echo "container does not exist!" >&2 + return 1 fi } stop() { - docker_ps - if [ $cid ]; then + apply_container_name_args stop "$@" || return 1 + docker_ps || return 1 + if [ -n "$cid" ]; then echo "containerID: $cid" echo "docker stop $cid" - docker stop $cid - docker ps + docker stop "$cid" || return 1 + docker ps || return 1 else - echo "container not running!" + echo "container does not exist!" >&2 + return 1 fi } rm_container() { - stop - if [ $cid ]; then - echo "containerID: $cid" - echo "docker rm $cid" - docker rm $cid - docker_ps - else - echo "image not exists!" - fi + apply_container_name_args rm "$@" || return 1 + stop || return 1 + echo "containerID: $cid" + echo "docker rm $cid" + docker rm "$cid" || return 1 + docker_ps || return 1 } log() { - docker_ps + apply_container_name_args log "$@" || return 1 + docker_ps || return 1 - if [ $cid ]; then + if [ -n "$cid" ]; then echo "containerID: $cid" - docker exec -it $cid tail -100f $BASE_DIR/$LOG_FILE + docker exec "$cid" tail -100f "$BASE_DIR/$LOG_FILE" || return 1 else - echo "container not exists!" + echo "container does not exist!" >&2 + return 1 fi } case "$1" in --pull) - pull ${@: 2} + pull "${@:2}" exit ;; --start) - start ${@: 2} + start "${@:2}" exit ;; --stop) - stop ${@: 2} + stop "${@:2}" exit ;; --build) - build ${@: 2} + build "${@:2}" exit ;; --run) - run ${@: 2} + run "${@:2}" exit ;; --rm) - rm_container ${@: 2} + rm_container "${@:2}" exit ;; --log) - log ${@: 2} + log "${@:2}" exit ;; *) echo "arg: $1 is not a valid parameter" - exit + exit 1 ;; esac diff --git a/docker/tests/docker-lifecycle-test.sh b/docker/tests/docker-lifecycle-test.sh new file mode 100755 index 00000000000..ca98be8b91c --- /dev/null +++ b/docker/tests/docker-lifecycle-test.sh @@ -0,0 +1,229 @@ +#!/bin/bash +set -euo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +REPOSITORY_ROOT=$(cd -- "$TEST_DIR/../.." >/dev/null 2>&1 && pwd) +DOCKER_SCRIPT="$REPOSITORY_ROOT/docker/docker.sh" +TEST_TMP=$(mktemp -d) +MOCK_BIN="$TEST_TMP/bin" +DOCKER_LOG="$TEST_TMP/docker-args" + +cleanup() { + rm -rf "$TEST_TMP" +} +trap cleanup EXIT + +mkdir -p "$MOCK_BIN" + +cat > "$MOCK_BIN/docker" <<'MOCK_DOCKER' +#!/bin/bash +set -euo pipefail + +printf '%s\n' "$*" >> "$DOCKER_MOCK_LOG" + +case "${1:-}" in + --version) + echo "Docker version 23.0.0, build mock" + ;; + container) + case "${2:-}" in + inspect) + if [ "${MOCK_QUERY_STATUS:-0}" -ne 0 ]; then + exit "$MOCK_QUERY_STATUS" + fi + + requested_name="${@: -1}" + if [ "${MOCK_CONTAINER_EXISTS:-false}" = true ] && + [ "$requested_name" = "${MOCK_CONTAINER_NAME:-tronprotocol-java-tron}" ]; then + printf 'deadbeef /%s\n' "$requested_name" + else + exit 1 + fi + ;; + ls) + if [ "${3:-}" != "-aq" ]; then + echo "Unexpected docker container ls command: $*" >&2 + exit 99 + fi + if [ "${MOCK_QUERY_STATUS:-0}" -ne 0 ]; then + exit "$MOCK_QUERY_STATUS" + fi + if [ "${MOCK_CONTAINER_EXISTS:-false}" = true ]; then + echo "deadbeef" + fi + ;; + *) + echo "Unexpected docker container command: $*" >&2 + exit 99 + ;; + esac + ;; + ps) + if [ "${2:-}" = "-aq" ]; then + if [ "${MOCK_QUERY_STATUS:-0}" -ne 0 ]; then + exit "$MOCK_QUERY_STATUS" + fi + name_filter="" + previous="" + for argument in "$@"; do + if [ "$previous" = "--filter" ]; then + name_filter=${argument#name=} + fi + previous=$argument + done + if [ "${MOCK_CONTAINER_EXISTS:-false}" = true ] \ + && { [ -z "$name_filter" ] \ + || [[ "/${MOCK_CONTAINER_NAME:-tronprotocol-java-tron}" =~ $name_filter ]]; }; then + echo "deadbeef" + fi + else + exit "${MOCK_PS_STATUS:-0}" + fi + ;; + start) + exit "${MOCK_START_STATUS:-0}" + ;; + stop) + exit "${MOCK_STOP_STATUS:-0}" + ;; + rm) + exit "${MOCK_RM_STATUS:-0}" + ;; + exec) + exit "${MOCK_EXEC_STATUS:-0}" + ;; + *) + echo "Unexpected docker command: $*" >&2 + exit 99 + ;; +esac +MOCK_DOCKER + +chmod +x "$MOCK_BIN/docker" + +run_lifecycle() { + local operation="$1" + shift + + : > "$DOCKER_LOG" + env \ + PATH="$MOCK_BIN:$PATH" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + "$@" \ + bash "$DOCKER_SCRIPT" "$operation" +} + +run_named_lifecycle() { + local operation="$1" + local container_name="$2" + shift 2 + + : > "$DOCKER_LOG" + env \ + PATH="$MOCK_BIN:$PATH" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + "$@" \ + bash "$DOCKER_SCRIPT" "$operation" --container-name "$container_name" +} + +expect_status() { + local expected="$1" + shift + local actual + + set +e + "$@" >/dev/null 2>&1 + actual=$? + set -e + + if [ "$actual" -ne "$expected" ]; then + echo "Expected exit status $expected, got $actual: $*" >&2 + sed 's/^/ docker /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +assert_call_count() { + local pattern="$1" + local expected="$2" + local actual + + actual=$(grep -Ec -- "$pattern" "$DOCKER_LOG" || true) + if [ "$actual" -ne "$expected" ]; then + echo "Expected $expected calls matching '$pattern', got $actual" >&2 + sed 's/^/ docker /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +expect_status 1 run_lifecycle --start \ + MOCK_CONTAINER_EXISTS=true MOCK_START_STATUS=42 +assert_call_count '^container inspect --format .* tronprotocol-java-tron$' 1 +assert_call_count '^ps$' 0 + +expect_status 1 run_lifecycle --stop \ + MOCK_CONTAINER_EXISTS=true MOCK_STOP_STATUS=43 +assert_call_count '^container inspect --format .* tronprotocol-java-tron$' 1 +assert_call_count '^ps$' 0 + +expect_status 1 run_lifecycle --start MOCK_CONTAINER_EXISTS=false +expect_status 1 run_lifecycle --stop MOCK_CONTAINER_EXISTS=false +expect_status 1 run_lifecycle --log MOCK_CONTAINER_EXISTS=false +assert_call_count '^container inspect --format .* tronprotocol-java-tron$' 1 +assert_call_count '^container ls -aq$' 1 + +expect_status 1 run_lifecycle --start MOCK_QUERY_STATUS=51 +assert_call_count '^container inspect --format .* tronprotocol-java-tron$' 1 +assert_call_count '^container ls -aq$' 1 +assert_call_count '^start ' 0 + +expect_status 1 run_lifecycle --start \ + MOCK_CONTAINER_EXISTS=true MOCK_PS_STATUS=52 +expect_status 1 run_lifecycle --stop \ + MOCK_CONTAINER_EXISTS=true MOCK_PS_STATUS=53 + +expect_status 1 run_lifecycle --log \ + MOCK_CONTAINER_EXISTS=true MOCK_EXEC_STATUS=44 + +expect_status 1 run_lifecycle --rm \ + MOCK_CONTAINER_EXISTS=true MOCK_RM_STATUS=45 +expect_status 1 run_lifecycle --rm MOCK_CONTAINER_EXISTS=false + +# Docker's name filter treats '.' as a regular-expression wildcard. Exact-name +# lookup must not operate on nodeXone when the requested container is node.one. +expect_status 0 run_named_lifecycle --stop node.one \ + MOCK_CONTAINER_EXISTS=true MOCK_CONTAINER_NAME=node.one +assert_call_count '^stop deadbeef$' 1 + +expect_status 1 run_named_lifecycle --stop node.one \ + MOCK_CONTAINER_EXISTS=true MOCK_CONTAINER_NAME=nodeXone +assert_call_count '^container inspect --format .* node[.]one$' 1 +assert_call_count '^container ls -aq$' 1 +assert_call_count '^stop ' 0 + +expect_status 1 run_named_lifecycle --log node.one \ + MOCK_CONTAINER_EXISTS=true MOCK_CONTAINER_NAME=nodeXone +assert_call_count '^exec ' 0 + +expect_status 1 run_named_lifecycle --rm node.one \ + MOCK_CONTAINER_EXISTS=true MOCK_CONTAINER_NAME=nodeXone +assert_call_count '^stop ' 0 +assert_call_count '^rm ' 0 + +expect_status 0 run_lifecycle --start MOCK_CONTAINER_EXISTS=true +assert_call_count '^start deadbeef$' 1 +assert_call_count '^ps$' 1 + +expect_status 0 run_lifecycle --stop MOCK_CONTAINER_EXISTS=true +assert_call_count '^stop deadbeef$' 1 +assert_call_count '^ps$' 1 + +expect_status 0 run_lifecycle --log MOCK_CONTAINER_EXISTS=true +assert_call_count '^exec deadbeef tail -100f /java-tron/logs/tron.log$' 1 +assert_call_count '^exec -' 0 + +expect_status 0 run_lifecycle --rm MOCK_CONTAINER_EXISTS=true +assert_call_count '^stop deadbeef$' 1 +assert_call_count '^rm deadbeef$' 1 + +echo "docker.sh lifecycle tests passed" diff --git a/docker/tests/docker-sh-run-smoke.sh b/docker/tests/docker-sh-run-smoke.sh new file mode 100644 index 00000000000..a6d44cdebfa --- /dev/null +++ b/docker/tests/docker-sh-run-smoke.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# Short integration smoke: run docker.sh --run against a local image. +# Does not wait for RPC or chain sync. +set -euo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +REPOSITORY_ROOT=$(cd -- "$TEST_DIR/../.." >/dev/null 2>&1 && pwd) +DOCKER_SCRIPT="$REPOSITORY_ROOT/docker/docker.sh" +CONTAINER_NAME="java-tron-smoke-$$-$RANDOM" +IMAGE="${1:-}" + +if [ -z "$IMAGE" ]; then + echo "Usage: $0 IMAGE" >&2 + exit 1 +fi + +DATA_DIR=$(mktemp -d "$REPOSITORY_ROOT/.docker-sh-run-smoke.XXXXXX") + +remove_runtime_owned_directory() { + local directory="$1" + + [ -e "$directory" ] || return 0 + if rm -rf -- "$directory" 2>/dev/null && [ ! -e "$directory" ]; then + return 0 + fi + if [ ! -d "$directory" ]; then + echo "Runtime-test path is not a directory: $directory" >&2 + return 1 + fi + + # Delete files as the same container identity that created them. Numeric + # host IDs must not be passed through Docker because rootless/userns daemons + # would map those IDs a second time. The host can remove the empty bind root + # using its permissions on DATA_DIR. + if ! docker run --rm \ + --user 10001:10001 \ + --network none \ + --read-only \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --entrypoint find \ + --mount "type=bind,src=$directory,dst=/cleanup" \ + "$IMAGE" /cleanup -mindepth 1 -depth -delete; then + echo "Failed to empty container-owned test directory: $directory" >&2 + return 1 + fi + if ! rmdir -- "$directory"; then + echo "Failed to remove empty test directory: $directory" >&2 + return 1 + fi +} + +cleanup() { + local test_status=$? + local cleanup_status=0 + + trap - EXIT + set +e + + if ! bash "$DOCKER_SCRIPT" --rm --container-name "$CONTAINER_NAME" >/dev/null 2>&1; then + echo "Failed to remove smoke-test container: $CONTAINER_NAME" >&2 + cleanup_status=1 + fi + + if [ -d "$DATA_DIR" ]; then + if ! remove_runtime_owned_directory "$DATA_DIR/output-directory"; then + cleanup_status=1 + fi + if ! remove_runtime_owned_directory "$DATA_DIR/logs"; then + cleanup_status=1 + fi + if ! rm -rf -- "$DATA_DIR"; then + echo "Failed to remove smoke-test directory: $DATA_DIR" >&2 + cleanup_status=1 + fi + fi + + if [ "$test_status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then + test_status=$cleanup_status + fi + exit "$test_status" +} +trap cleanup EXIT + +run_node() ( + # Exercise the restrictive mode that previously made a second Linux run + # unreadable to the invoking host user after ownership moved to UID 10001. + umask 077 + bash "$DOCKER_SCRIPT" --run --image "$IMAGE" \ + --container-name "$CONTAINER_NAME" \ + --net private \ + --memory 2g \ + --jvm-opts "-Xms256m -XX:MaxRAMPercentage=40.0" \ + --data-dir "$DATA_DIR" \ + -- --p2p-disable true +) + +assert_node_stable() { + if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME")" != true ]; then + echo "docker.sh --run did not leave a running container" >&2 + docker inspect "$CONTAINER_NAME" >&2 || true + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 + fi + + sleep 12 + + if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME")" != true ]; then + echo "The FullNode container exited during the smoke window" >&2 + docker inspect "$CONTAINER_NAME" >&2 || true + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 + fi + + restarts=$(docker inspect -f '{{.RestartCount}}' "$CONTAINER_NAME") + oom=$(docker inspect -f '{{.State.OOMKilled}}' "$CONTAINER_NAME") + if [ "$restarts" != 0 ] || [ "$oom" != false ]; then + echo "The FullNode container restarted or was OOM-killed (restarts=$restarts oom=$oom)" >&2 + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 + fi + + logs=$(docker logs "$CONTAINER_NAME" 2>&1 || true) + if grep -Eiq 'Could not create the Java Virtual Machine|Unrecognized VM option|Error: Could not find or load main class' <<< "$logs"; then + echo "FullNode failed to start:" >&2 + printf '%s\n' "$logs" >&2 + exit 1 + fi +} + +run_node +assert_node_stable +bash "$DOCKER_SCRIPT" --rm --container-name "$CONTAINER_NAME" >/dev/null + +# Reuse the same private configuration, database and log directories. On a +# native Linux runner these paths are now owned by UID 10001 and remain 0700. +run_node +assert_node_stable + +echo "docker.sh --run first-run and reuse smoke passed for $IMAGE" diff --git a/docker/tests/docker-sh-test.sh b/docker/tests/docker-sh-test.sh new file mode 100644 index 00000000000..b9ac30c5615 --- /dev/null +++ b/docker/tests/docker-sh-test.sh @@ -0,0 +1,1814 @@ +#!/bin/bash +set -euo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +REPOSITORY_ROOT=$(cd -- "$TEST_DIR/../.." >/dev/null 2>&1 && pwd) +DOCKER_SCRIPT="$REPOSITORY_ROOT/docker/docker.sh" +TEST_TMP=$(mktemp -d "$REPOSITORY_ROOT/.docker-sh-test.XXXXXX") +TEST_TMP_PHYSICAL=$(cd -P -- "$TEST_TMP" >/dev/null 2>&1 && pwd -P) +MOCK_BIN="$TEST_TMP/bin" +SOURCE_ROOT="$TEST_TMP/source" +SOURCE_WITHOUT_DOCKERFILE="$TEST_TMP/source-without-dockerfile" +STANDALONE_DIR="$TEST_TMP/standalone" +DOCKER_LOG="$TEST_TMP/docker-args" +DOCKER_RUN_LOG="$TEST_TMP/docker-run-history" +DOCKER_CONTEXT_LOG="$TEST_TMP/docker-context" +DOCKER_ENV_LOG="$TEST_TMP/docker-env" +DOWNLOAD_LOG="$TEST_TMP/downloads" +GRADLE_LOG="$TEST_TMP/gradle-args" +UNZIP_LOG="$TEST_TMP/unzip-destinations" +RUNTIME_INIT_IMAGE="busybox:1.37.0-musl@sha256:fc6dddc4c44b1bfe37f41cae8e67d1693828e8f42a91862816d7953e2c9d3f23" + +cleanup() { + rm -rf "$TEST_TMP" +} +trap cleanup EXIT + +mkdir -p "$MOCK_BIN" "$SOURCE_ROOT/docker/arm64" \ + "$SOURCE_ROOT/framework/src/main/resources" "$TEST_TMP/config" \ + "$SOURCE_WITHOUT_DOCKERFILE" \ + "$TEST_TMP/external-data/config" "$TEST_TMP/relative-data/config" +cp "$REPOSITORY_ROOT/docker/Dockerfile" "$SOURCE_ROOT/docker/Dockerfile" +cp "$REPOSITORY_ROOT/docker/arm64/Dockerfile" "$SOURCE_ROOT/docker/arm64/Dockerfile" +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'SAFE_LOCAL_CONFIG' +localwitness = [ +] +event.subscribe = { + dbconfig = "" +} +node.dns = { + dnsPrivate = "" + accessKeySecret = "" +} +# localwitness = ["commented-example-key"] +# dbconfig = "commented|example|credentials" +SAFE_LOCAL_CONFIG +touch "$SOURCE_ROOT/.env" + +cat > "$MOCK_BIN/docker" <<'MOCK_DOCKER' +#!/bin/bash +set -euo pipefail + +if [ "${MOCK_DOCKER_FORBIDDEN:-false}" = true ]; then + echo "docker must not be called for help" >&2 + exit 97 +fi + +if [ "${1:-}" = "--version" ]; then + echo "Docker version ${MOCK_DOCKER_VERSION:-23.0.0}, build mock" + exit 0 +fi + +case "${1:-}" in + info) + if [ "${MOCK_DOCKER_INFO_STATUS:-0}" -ne 0 ]; then + exit "$MOCK_DOCKER_INFO_STATUS" + fi + printf '%s\n' "${MOCK_DOCKER_SECURITY_OPTIONS:-[\"name=seccomp,profile=builtin\"]}" + ;; + build) + printf '%s\n' "$@" > "$DOCKER_MOCK_LOG" + printf '%s\n' "${DOCKER_BUILDKIT:-}" > "$DOCKER_MOCK_ENV_LOG" + context="${!#}" + ( + cd -- "$context" + find . ! -type d -print | LC_ALL=C sort + ) > "$DOCKER_MOCK_CONTEXT_LOG" + ;; + pull) + printf '%s\n' "$@" > "$DOCKER_MOCK_LOG" + exit "${MOCK_PULL_STATUS:-0}" + ;; + image) + if [ "${2:-}" != "inspect" ]; then + echo "Unexpected docker image command: $*" >&2 + exit 1 + fi + if [ "${MOCK_IMAGE_MISSING:-false}" = true ]; then + echo "Error: No such image" >&2 + exit 1 + fi + image_ref="${!#}" + if [ -n "${MOCK_ABSENT_IMAGE:-}" ] && [ "$image_ref" = "$MOCK_ABSENT_IMAGE" ]; then + echo "Error: No such image" >&2 + exit 1 + fi + if [ "${3:-}" = "-f" ]; then + case "${4:-}" in + *Architecture*) + echo "${MOCK_IMAGE_ARCH:-amd64}" + ;; + *Config.User*) + echo "${MOCK_IMAGE_USER:-10001:10001}" + ;; + esac + fi + ;; + container) + case "${2:-}" in + inspect) + if [ "${MOCK_CONTAINER_QUERY_STATUS:-0}" -ne 0 ]; then + exit "$MOCK_CONTAINER_QUERY_STATUS" + fi + requested_name="${!#}" + if [ "${MOCK_CONTAINER_EXISTS:-false}" = true ] \ + && [ "$requested_name" = "${MOCK_CONTAINER_NAME:-tronprotocol-java-tron}" ]; then + printf 'deadbeef /%s\n' "$requested_name" + else + exit 1 + fi + ;; + ls) + if [ "${3:-}" != "-aq" ]; then + echo "Unexpected docker container ls command: $*" >&2 + exit 1 + fi + exit "${MOCK_CONTAINER_QUERY_STATUS:-0}" + ;; + *) + echo "Unexpected docker container command: $*" >&2 + exit 1 + ;; + esac + ;; + ps) + if [ "${2:-}" != "-aq" ]; then + echo "Unexpected docker ps command: $*" >&2 + exit 1 + fi + if [ "${MOCK_CONTAINER_EXISTS:-false}" = true ]; then + echo "deadbeef" + fi + ;; + run) + printf '%s\n' "$@" > "$DOCKER_MOCK_LOG" + printf '%s\n' "$@" >> "$DOCKER_MOCK_RUN_LOG" + arguments=("$@") + volumes=() + previous="" + entrypoint="" + for argument in "$@"; do + if [ "$previous" = "--entrypoint" ]; then + entrypoint=$argument + fi + if [ "$previous" = "-v" ]; then + volumes+=("$argument") + fi + previous="$argument" + done + if [ "$entrypoint" = "chown" ]; then + exit "${MOCK_PERMISSION_STATUS:-0}" + fi + if [ "$entrypoint" = "sh" ]; then + if [[ "$*" == *"test -r"* ]]; then + exit "${MOCK_CONFIG_READ_STATUS:-0}" + fi + if [ "${MOCK_EXECUTE_RUNTIME_CHECK:-false}" = true ]; then + runtime_script="" + runtime_path_start=0 + for ((argument_index=0; argument_index<${#arguments[@]}; argument_index++)); do + if [ "${arguments[$argument_index]}" = "-ec" ]; then + runtime_script=${arguments[$((argument_index + 1))]} + runtime_path_start=$((argument_index + 3)) + break + fi + done + if [[ "$runtime_script" == *"first_unwritable"* ]]; then + runtime_paths=() + for ((argument_index=runtime_path_start; + argument_index<${#arguments[@]}; + argument_index++)); do + translated_path=${arguments[$argument_index]} + for volume in "${volumes[@]}"; do + host_path=${volume%%:*} + target_and_options=${volume#*:} + container_path=${target_and_options%%:*} + if [ "$translated_path" = "$container_path" ]; then + translated_path=$host_path + break + fi + done + runtime_paths+=("$translated_path") + done + MOCK_FIND_CONTEXT=runtime /bin/sh -ec "$runtime_script" \ + sh "${runtime_paths[@]}" + exit $? + fi + fi + exit "${MOCK_PERMISSION_STATUS:-0}" + fi + exit "${MOCK_RUN_STATUS:-0}" + ;; + *) + echo "Unexpected docker command: $*" >&2 + exit 1 + ;; +esac +MOCK_DOCKER + +cat > "$MOCK_BIN/uname" <<'MOCK_UNAME' +#!/bin/bash +set -euo pipefail + +if [ "${1:-}" = "-m" ]; then + echo "${MOCK_ARCH:-x86_64}" + exit 0 +fi + +exec /usr/bin/uname "$@" +MOCK_UNAME + +cat > "$MOCK_BIN/unzip" <<'MOCK_UNZIP' +#!/bin/bash +set -euo pipefail + +destination="" +while [ $# -gt 0 ]; do + if [ "$1" = "-d" ]; then + destination=$2 + shift 2 + else + shift + fi +done +test -n "$destination" +if [ -n "${UNZIP_MOCK_LOG:-}" ]; then + printf '%s\n' "$destination" > "$UNZIP_MOCK_LOG" +fi +mkdir -p "$destination/java-tron-1.0.0/bin" "$destination/java-tron-1.0.0/lib" +touch "$destination/java-tron-1.0.0/bin/FullNode" +touch "$destination/java-tron-1.0.0/bin/FullNode.bat" +touch "$destination/java-tron-1.0.0/bin/java-tron.vmoptions" +touch "$destination/java-tron-1.0.0/lib/java-tron.jar" +chmod +x "$destination/java-tron-1.0.0/bin/FullNode" + +case "${MOCK_UNZIP_FIXTURE:-safe}" in + safe) + ;; + witness-key) + printf 'private-key\n' > "$destination/java-tron-1.0.0/witness.key" + ;; + key-backup) + printf 'private-key\n' > "$destination/java-tron-1.0.0/witness.key.bak" + ;; + keystore) + printf 'keystore\n' > "$destination/java-tron-1.0.0/localwitness.jks" + ;; + wallet) + mkdir -p "$destination/java-tron-1.0.0/Wallet" + printf 'wallet\n' > "$destination/java-tron-1.0.0/Wallet/account.json" + ;; + lowercase-wallet) + mkdir -p "$destination/java-tron-1.0.0/wallet" + printf 'wallet\n' > "$destination/java-tron-1.0.0/wallet/account.json" + ;; + node-id) + printf 'node-id\n' > "$destination/java-tron-1.0.0/nodeId.properties" + ;; + database) + mkdir -p "$destination/java-tron-1.0.0/database" + printf 'database\n' > "$destination/java-tron-1.0.0/database/block.data" + ;; + logs) + mkdir -p "$destination/java-tron-1.0.0/logs" + printf 'log\n' > "$destination/java-tron-1.0.0/logs/tron.log" + ;; + symbolic-link) + ln -s /etc/passwd "$destination/java-tron-1.0.0/lib/linked.jar" + ;; + fifo) + mkfifo "$destination/java-tron-1.0.0/lib/stream.jar" + ;; + locked-directory-0500) + mkdir -p "$destination/java-tron-1.0.0/Wallet" + printf 'wallet\n' > "$destination/java-tron-1.0.0/Wallet/account.json" + chmod 0500 "$destination/java-tron-1.0.0/Wallet" + ;; + locked-directory-000) + mkdir -p "$destination/java-tron-1.0.0/Wallet" + printf 'wallet\n' > "$destination/java-tron-1.0.0/Wallet/account.json" + chmod 000 "$destination/java-tron-1.0.0/Wallet" + ;; + *) + echo "Unknown mock distribution fixture: $MOCK_UNZIP_FIXTURE" >&2 + exit 1 + ;; +esac +MOCK_UNZIP + +cat > "$MOCK_BIN/curl" <<'MOCK_CURL' +#!/bin/bash +set -euo pipefail + +output="" +url="" +while [ $# -gt 0 ]; do + if [ "$1" = "-o" ]; then + output=$2 + shift 2 + else + if [[ "$1" != -* ]]; then + url=$1 + fi + shift + fi +done +test -n "$output" +mkdir -p "$(dirname "$output")" +if [ -n "${DOWNLOAD_MOCK_LOG:-}" ]; then + printf '%s|%s\n' "$url" "$output" >> "$DOWNLOAD_MOCK_LOG" +fi +if [ "${MOCK_CURL_FAIL:-false}" = true ]; then + printf 'partial-download\n' > "$output" + exit 1 +fi +if [ "${MOCK_CURL_EMPTY:-false}" = true ]; then + : > "$output" + exit 0 +fi +printf 'downloaded-content\n' > "$output" +MOCK_CURL + +cat > "$MOCK_BIN/find" <<'MOCK_FIND' +#!/bin/bash +set -euo pipefail + +if [ "${MOCK_FIND_CONTEXT:-host}" = runtime ]; then + denied_path="${MOCK_RUNTIME_FIND_DENIED_PATH:-}" +else + denied_path="${MOCK_HOST_FIND_DENIED_PATH:-}" +fi +if [ -n "$denied_path" ] && [ "${1:-}" = "$denied_path" ]; then + echo "find: $denied_path: Permission denied" >&2 + exit 1 +fi +if [ "${MOCK_FIND_CONTEXT:-host}" = runtime ]; then + exit 0 +fi +exec /usr/bin/find "$@" +MOCK_FIND + +cat > "$MOCK_BIN/stat" <<'MOCK_STAT' +#!/bin/bash +set -euo pipefail + +target=${!#} +if [ -n "${MOCK_RUNTIME_OWNER_PATH:-}" ] \ + && [ "$target" = "$MOCK_RUNTIME_OWNER_PATH" ]; then + printf '%s %s\n' \ + "${MOCK_RUNTIME_HOST_OWNER_UID:-10001}" \ + "${MOCK_RUNTIME_OWNER_MODE:-700}" + exit 0 +fi +exec /usr/bin/stat "$@" +MOCK_STAT + +cat > "$SOURCE_ROOT/gradlew" <<'MOCK_GRADLEW' +#!/bin/bash +set -euo pipefail + +printf '%s\n' "$@" >> "$GRADLE_MOCK_LOG" +mkdir -p framework/build/distributions +touch framework/build/distributions/java-tron-1.0.0.zip +MOCK_GRADLEW + +chmod +x "$MOCK_BIN/docker" "$MOCK_BIN/uname" "$MOCK_BIN/unzip" \ + "$MOCK_BIN/curl" "$MOCK_BIN/find" "$MOCK_BIN/stat" \ + "$SOURCE_ROOT/gradlew" +cp "$SOURCE_ROOT/gradlew" "$SOURCE_WITHOUT_DOCKERFILE/gradlew" + +assert_argument() { + local expected="$1" + if ! grep -Fqx -- "$expected" "$DOCKER_LOG"; then + echo "Missing docker argument: $expected" >&2 + echo "Recorded arguments:" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +assert_no_argument() { + local unexpected="$1" + if grep -Fqx -- "$unexpected" "$DOCKER_LOG"; then + echo "Unexpected docker argument: $unexpected" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +assert_argument_count() { + local expected="$1" + local count="$2" + local actual + actual=$(grep -Fxc -- "$expected" "$DOCKER_LOG" || true) + if [ "$actual" -ne "$count" ]; then + echo "Expected docker argument '$expected' $count times, got $actual" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +assert_run_argument_count() { + local expected="$1" + local count="$2" + local actual + actual=$(grep -Fxc -- "$expected" "$DOCKER_RUN_LOG" || true) + if [ "$actual" -ne "$count" ]; then + echo "Expected docker run argument '$expected' $count times, got $actual" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 + fi +} + +assert_chown_uses_pinned_helper() { + local workload_image="$1" + + if ! awk -v helper="$RUNTIME_INIT_IMAGE" -v workload="$workload_image" ' + function check_invocation() { + if (!is_chown) { + return + } + chown_count++ + if (!has_helper || has_workload) { + invalid = 1 + } + } + $0 == "run" { + check_invocation() + is_chown = 0 + has_helper = 0 + has_workload = 0 + next + } + $0 == "chown" { is_chown = 1 } + $0 == helper { has_helper = 1 } + $0 == workload { has_workload = 1 } + END { + check_invocation() + exit invalid || chown_count != 1 + } + ' "$DOCKER_RUN_LOG"; then + echo "Runtime ownership was not initialized exclusively by the pinned helper image" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 + fi +} + +file_mode() { + local path="$1" + local mode + + if mode=$(stat -f '%Lp' "$path" 2>/dev/null); then + printf '%s\n' "$mode" + return 0 + fi + stat -c '%a' "$path" +} + +assert_mode() { + local expected="$1" + local path="$2" + local actual + + actual=$(file_mode "$path") + if [ "$actual" != "$expected" ]; then + echo "Expected mode $expected for $path, got $actual" >&2 + exit 1 + fi +} + +assert_trailing_arguments() { + local expected + local actual + expected=$(printf '%s\n' "$@") + actual=$(tail -n "$#" "$DOCKER_LOG") + if [ "$actual" != "$expected" ]; then + echo "Unexpected trailing docker arguments:" >&2 + echo "Expected:" >&2 + printf ' %s\n' "$@" >&2 + echo "Actual:" >&2 + printf '%s\n' "$actual" | sed 's/^/ /' >&2 + exit 1 + fi +} + +assert_context_file() { + local expected="$1" + if ! grep -Fqx -- "$expected" "$DOCKER_CONTEXT_LOG"; then + echo "Missing Docker context file: $expected" >&2 + sed 's/^/ /' "$DOCKER_CONTEXT_LOG" >&2 + exit 1 + fi +} + +assert_context_only_dockerfile() { + if [ "$(cat "$DOCKER_CONTEXT_LOG")" != "./Dockerfile" ]; then + echo "Remote build context contains unexpected files:" >&2 + sed 's/^/ /' "$DOCKER_CONTEXT_LOG" >&2 + exit 1 + fi +} + +assert_temporary_context() { + local actual + actual=$(tail -n 1 "$DOCKER_LOG") + if [ "$actual" = "$REPOSITORY_ROOT" ] || [ "$actual" = "$REPOSITORY_ROOT/docker" ]; then + echo "Docker build used a repository directory as its context: $actual" >&2 + exit 1 + fi +} + +assert_buildkit_enabled() { + if [ "$(cat "$DOCKER_ENV_LOG")" != "1" ]; then + echo "docker.sh did not enable BuildKit" >&2 + exit 1 + fi +} + +run_build() { + local architecture="$1" + local working_directory="$2" + shift 2 + : > "$DOCKER_LOG" + : > "$DOCKER_CONTEXT_LOG" + : > "$DOCKER_ENV_LOG" + : > "$GRADLE_LOG" + : > "$UNZIP_LOG" + ( + cd -- "$working_directory" + PATH="$MOCK_BIN:$PATH" \ + MOCK_ARCH="$architecture" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + DOCKER_MOCK_CONTEXT_LOG="$DOCKER_CONTEXT_LOG" \ + DOCKER_MOCK_ENV_LOG="$DOCKER_ENV_LOG" \ + GRADLE_MOCK_LOG="$GRADLE_LOG" \ + UNZIP_MOCK_LOG="$UNZIP_LOG" \ + MOCK_UNZIP_FIXTURE="${MOCK_UNZIP_FIXTURE:-safe}" \ + bash "$DOCKER_SCRIPT" --build "$@" + ) +} + +run_export() { + local architecture="$1" + local working_directory="$2" + local output_context="$3" + shift 3 + : > "$DOCKER_LOG" + : > "$DOCKER_CONTEXT_LOG" + : > "$DOCKER_ENV_LOG" + : > "$GRADLE_LOG" + : > "$UNZIP_LOG" + ( + cd -- "$working_directory" + PATH="$MOCK_BIN:$PATH" \ + MOCK_ARCH="$architecture" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + DOCKER_MOCK_CONTEXT_LOG="$DOCKER_CONTEXT_LOG" \ + DOCKER_MOCK_ENV_LOG="$DOCKER_ENV_LOG" \ + GRADLE_MOCK_LOG="$GRADLE_LOG" \ + UNZIP_MOCK_LOG="$UNZIP_LOG" \ + MOCK_UNZIP_FIXTURE="${MOCK_UNZIP_FIXTURE:-safe}" \ + bash "$DOCKER_SCRIPT" --build --source local \ + --export-context "$output_context" "$@" + ) +} + +run_standalone_build() { + local architecture="$1" + local working_directory="$2" + shift 2 + mkdir -p "$STANDALONE_DIR" + cp "$DOCKER_SCRIPT" "$STANDALONE_DIR/docker.sh" + : > "$DOCKER_LOG" + : > "$DOCKER_CONTEXT_LOG" + : > "$DOCKER_ENV_LOG" + : > "$DOWNLOAD_LOG" + : > "$GRADLE_LOG" + ( + cd -- "$working_directory" + PATH="$MOCK_BIN:$PATH" \ + MOCK_ARCH="$architecture" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + DOCKER_MOCK_CONTEXT_LOG="$DOCKER_CONTEXT_LOG" \ + DOCKER_MOCK_ENV_LOG="$DOCKER_ENV_LOG" \ + GRADLE_MOCK_LOG="$GRADLE_LOG" \ + DOWNLOAD_MOCK_LOG="$DOWNLOAD_LOG" \ + MOCK_CURL_FAIL="${MOCK_CURL_FAIL:-false}" \ + MOCK_CURL_EMPTY="${MOCK_CURL_EMPTY:-false}" \ + bash "$STANDALONE_DIR/docker.sh" --build "$@" + ) +} + +run_node() { + : > "$DOCKER_LOG" + : > "$DOCKER_RUN_LOG" + : > "$DOWNLOAD_LOG" + ( + cd -- "$TEST_TMP" + PATH="$MOCK_BIN:$PATH" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + DOCKER_MOCK_RUN_LOG="$DOCKER_RUN_LOG" \ + DOCKER_MOCK_CONTEXT_LOG="$DOCKER_CONTEXT_LOG" \ + DOCKER_MOCK_ENV_LOG="$DOCKER_ENV_LOG" \ + DOWNLOAD_MOCK_LOG="$DOWNLOAD_LOG" \ + MOCK_RUN_STATUS="${MOCK_RUN_STATUS:-0}" \ + MOCK_PERMISSION_STATUS="${MOCK_PERMISSION_STATUS:-0}" \ + MOCK_CONFIG_READ_STATUS="${MOCK_CONFIG_READ_STATUS:-0}" \ + MOCK_EXECUTE_RUNTIME_CHECK="${MOCK_EXECUTE_RUNTIME_CHECK:-false}" \ + MOCK_HOST_FIND_DENIED_PATH="${MOCK_HOST_FIND_DENIED_PATH:-}" \ + MOCK_RUNTIME_FIND_DENIED_PATH="${MOCK_RUNTIME_FIND_DENIED_PATH:-}" \ + MOCK_RUNTIME_OWNER_PATH="${MOCK_RUNTIME_OWNER_PATH:-}" \ + MOCK_RUNTIME_HOST_OWNER_UID="${MOCK_RUNTIME_HOST_OWNER_UID:-10001}" \ + MOCK_RUNTIME_OWNER_MODE="${MOCK_RUNTIME_OWNER_MODE:-700}" \ + MOCK_DOCKER_SECURITY_OPTIONS="${MOCK_DOCKER_SECURITY_OPTIONS:-[\"name=seccomp,profile=builtin\"]}" \ + MOCK_DOCKER_INFO_STATUS="${MOCK_DOCKER_INFO_STATUS:-0}" \ + MOCK_CONTAINER_EXISTS="${MOCK_CONTAINER_EXISTS:-false}" \ + MOCK_CONTAINER_NAME="${MOCK_CONTAINER_NAME:-tronprotocol-java-tron}" \ + MOCK_CONTAINER_QUERY_STATUS="${MOCK_CONTAINER_QUERY_STATUS:-0}" \ + MOCK_IMAGE_ARCH="${MOCK_IMAGE_ARCH:-amd64}" \ + MOCK_IMAGE_USER="${MOCK_IMAGE_USER:-10001:10001}" \ + MOCK_IMAGE_MISSING="${MOCK_IMAGE_MISSING:-false}" \ + MOCK_ABSENT_IMAGE="${MOCK_ABSENT_IMAGE:-}" \ + JAVA_TRON_IMAGE="${JAVA_TRON_IMAGE:-}" \ + MOCK_CURL_FAIL="${MOCK_CURL_FAIL:-false}" \ + MOCK_CURL_EMPTY="${MOCK_CURL_EMPTY:-false}" \ + bash "$DOCKER_SCRIPT" --run "$@" + ) +} + +run_pull() { + : > "$DOCKER_LOG" + ( + cd -- "$REPOSITORY_ROOT" + PATH="$MOCK_BIN:$PATH" \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + MOCK_IMAGE_USER="${MOCK_IMAGE_USER:-10001:10001}" \ + MOCK_PULL_STATUS="${MOCK_PULL_STATUS:-0}" \ + JAVA_TRON_IMAGE="${JAVA_TRON_IMAGE:-}" \ + bash "$DOCKER_SCRIPT" --pull "$@" + ) +} + +expect_run_failure() { + local expected_message="$1" + shift + local output + + if output=$(run_node "$@" 2>&1); then + echo "Expected command to fail: --run $*" >&2 + exit 1 + fi + if [[ "$output" != *"$expected_message"* ]]; then + echo "Expected run failure message '$expected_message', got:" >&2 + echo "$output" >&2 + exit 1 + fi +} + +expect_failure() { + local expected_message="$1" + shift + local output + if output=$( + cd -- "$REPOSITORY_ROOT" + PATH="$MOCK_BIN:$PATH" \ + MOCK_ARCH=x86_64 \ + DOCKER_MOCK_LOG="$DOCKER_LOG" \ + DOCKER_MOCK_CONTEXT_LOG="$DOCKER_CONTEXT_LOG" \ + DOCKER_MOCK_ENV_LOG="$DOCKER_ENV_LOG" \ + bash "$DOCKER_SCRIPT" --build "$@" 2>&1 + ); then + echo "Expected command to fail: --build $*" >&2 + exit 1 + fi + if [[ "$output" != *"$expected_message"* ]]; then + echo "Expected failure message '$expected_message', got:" >&2 + echo "$output" >&2 + exit 1 + fi +} + +expect_local_config_failure() { + local expected_setting="$1" + local output + + if output=$(run_build x86_64 "$SOURCE_ROOT" --source local 2>&1); then + echo "A local image build containing $expected_setting unexpectedly succeeded" >&2 + exit 1 + fi + if [[ "$output" != *"refusing to bake non-empty plaintext $expected_setting"* ]]; then + echo "The $expected_setting rejection was unclear:" >&2 + echo "$output" >&2 + exit 1 + fi + if [ -s "$GRADLE_LOG" ]; then + echo "Gradle ran before $expected_setting was rejected" >&2 + sed 's/^/ /' "$GRADLE_LOG" >&2 + exit 1 + fi + if [ -s "$DOCKER_LOG" ]; then + echo "Docker ran before $expected_setting was rejected" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 + fi +} + +help_output=$( + PATH="$MOCK_BIN:$PATH" MOCK_DOCKER_FORBIDDEN=true \ + bash "$DOCKER_SCRIPT" --help +) +if [[ "$help_output" != *"Usage: docker.sh COMMAND [OPTIONS]"* ]]; then + echo "--help did not print usage" >&2 + exit 1 +fi + +set +e +no_arg_output=$( + PATH="$MOCK_BIN:$PATH" MOCK_DOCKER_FORBIDDEN=true \ + bash "$DOCKER_SCRIPT" 2>&1 +) +no_arg_status=$? +set -e +if [ "$no_arg_status" -ne 1 ] || [[ "$no_arg_output" != *"Usage: docker.sh COMMAND [OPTIONS]"* ]]; then + echo "Invoking docker.sh without arguments did not return usage and status 1" >&2 + exit 1 +fi + +if default_pull_output=$(run_pull 2>&1); then + echo "A pull without an explicit image unexpectedly succeeded" >&2 + exit 1 +fi +if [[ "$default_pull_output" != *"no compatible default published image is configured"* ]] \ + || [[ "$default_pull_output" != *"specify --image"* ]]; then + echo "The missing pull image did not produce actionable guidance:" >&2 + echo "$default_pull_output" >&2 + exit 1 +fi +if [ -s "$DOCKER_LOG" ]; then + echo "docker pull was called before a pull image was selected" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 +fi + +run_pull --image example/java-tron:nonroot >/dev/null +assert_argument "pull" +assert_argument "example/java-tron:nonroot" + +if failed_pull_output=$( + MOCK_PULL_STATUS=55 run_pull --image example/java-tron:unavailable 2>&1 +); then + echo "A failed registry pull unexpectedly succeeded" >&2 + exit 1 +fi +if [[ "$failed_pull_output" != *"docker pull example/java-tron:unavailable"* ]]; then + echo "The failed registry pull did not identify its image:" >&2 + echo "$failed_pull_output" >&2 + exit 1 +fi + +if incompatible_pull_output=$( + MOCK_IMAGE_USER=root run_pull --image example/java-tron:legacy 2>&1 +); then + echo "A pulled root image unexpectedly passed validation" >&2 + exit 1 +fi +if [[ "$incompatible_pull_output" != *"must run as UID:GID 10001:10001"* ]]; then + echo "The incompatible pulled image did not fail with the runtime-user contract:" >&2 + echo "$incompatible_pull_output" >&2 + exit 1 +fi + +JAVA_TRON_IMAGE=example/java-tron:from-env run_pull >/dev/null +assert_argument "example/java-tron:from-env" + +remote_output=$(run_build x86_64 "$REPOSITORY_ROOT") +assert_argument "--pull" +assert_argument "--no-cache-filter" +assert_argument "remote-builder" +assert_argument "--target" +assert_argument "remote" +assert_argument "tronprotocol/java-tron:local" +assert_argument "SOURCE_REPOSITORY=https://github.com/tronprotocol/java-tron.git" +assert_argument "SOURCE_REF=master" +assert_no_argument "SOURCE_MODE=remote" +assert_temporary_context +assert_context_only_dockerfile +assert_buildkit_enabled +if [[ "$remote_output" != *"Local working-tree changes are not included"* ]]; then + echo "The backward-compatible remote build notice is missing." >&2 + exit 1 +fi + +run_standalone_build x86_64 "$TEST_TMP" >/dev/null +if ! grep -Fq -- \ + "https://raw.githubusercontent.com/tronprotocol/java-tron/master/docker/Dockerfile|$STANDALONE_DIR/Dockerfile.tmp." \ + "$DOWNLOAD_LOG"; then + echo "The standalone build did not download its Dockerfile from master" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +if [ ! -s "$STANDALONE_DIR/Dockerfile" ]; then + echo "The standalone build did not replace the Dockerfile atomically" >&2 + exit 1 +fi +if compgen -G "$STANDALONE_DIR/Dockerfile.tmp.*" >/dev/null; then + echo "The standalone build left a temporary Dockerfile behind" >&2 + exit 1 +fi +assert_argument "SOURCE_REF=master" +assert_context_only_dockerfile +assert_buildkit_enabled + +rm -f "$STANDALONE_DIR/Dockerfile" +if MOCK_CURL_FAIL=true run_standalone_build x86_64 "$TEST_TMP" >/dev/null 2>&1; then + echo "A failed Dockerfile download unexpectedly succeeded" >&2 + exit 1 +fi +if [ -e "$STANDALONE_DIR/Dockerfile" ]; then + echo "A failed Dockerfile download left a destination file" >&2 + exit 1 +fi +if compgen -G "$STANDALONE_DIR/Dockerfile.tmp.*" >/dev/null; then + echo "A failed Dockerfile download left a temporary file" >&2 + exit 1 +fi + +rm -f "$STANDALONE_DIR/Dockerfile" +run_standalone_build x86_64 "$SOURCE_ROOT" --source local >/dev/null +if grep -Fq -- "/tronprotocol/java-tron/master/docker/Dockerfile" "$DOWNLOAD_LOG"; then + echo "The standalone local build downloaded a Dockerfile instead of using the checkout" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +if grep -Fq -- "/tronprotocol/java-tron/master/framework/src/main/resources/config.conf" \ + "$DOWNLOAD_LOG"; then + echo "The standalone local build downloaded config.conf instead of using the checkout" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +assert_argument "--target" +assert_argument "local" +assert_context_file "./Dockerfile" +assert_context_file "./java-tron/bin/FullNode" +assert_buildkit_enabled + +if missing_dockerfile_output=$(run_standalone_build \ + x86_64 "$SOURCE_WITHOUT_DOCKERFILE" --source local 2>&1); then + echo "A local build without a checkout Dockerfile unexpectedly succeeded" >&2 + exit 1 +fi +if [[ "$missing_dockerfile_output" != *"local Dockerfile does not exist"* ]]; then + echo "The missing local Dockerfile failure was unclear:" >&2 + echo "$missing_dockerfile_output" >&2 + exit 1 +fi + +run_build x86_64 "$SOURCE_ROOT" --source local >/dev/null +assert_no_argument "--pull" +assert_no_argument "--no-cache-filter" +assert_argument "--target" +assert_argument "local" +assert_no_argument "SOURCE_MODE=local" +assert_temporary_context +assert_context_file "./Dockerfile" +assert_context_file "./.dockerignore" +assert_context_file "./java-tron/bin/FullNode" +assert_context_file "./java-tron/bin/FullNode.bat" +assert_context_file "./java-tron/bin/java-tron.vmoptions" +assert_context_file "./java-tron/config.conf" +assert_context_file "./java-tron/lib/java-tron.jar" +if grep -Fqx -- "./.env" "$DOCKER_CONTEXT_LOG"; then + echo "The local source .env file leaked into the Docker build context." >&2 + exit 1 +fi +assert_buildkit_enabled + +for unsafe_build_fixture in witness-key locked-directory-000; do + if unsafe_build_output=$(MOCK_UNZIP_FIXTURE="$unsafe_build_fixture" \ + run_build x86_64 "$SOURCE_ROOT" --source local 2>&1); then + echo "An unsafe '$unsafe_build_fixture' local image build succeeded" >&2 + exit 1 + fi + if [[ "$unsafe_build_output" != *"build: refusing"* ]] \ + && [[ "$unsafe_build_output" != *"failed to inspect the extracted local distribution"* ]]; then + echo "The '$unsafe_build_fixture' local distribution rejection was unclear:" >&2 + echo "$unsafe_build_output" >&2 + exit 1 + fi + if [ ! -s "$GRADLE_LOG" ] || [ -s "$DOCKER_LOG" ]; then + echo "The '$unsafe_build_fixture' distribution was not rejected between Gradle and Docker" >&2 + exit 1 + fi + rejected_build_staging=$(tail -n 1 "$UNZIP_LOG") + rejected_build_context=$(dirname -- "$rejected_build_staging") + if [ -e "$rejected_build_staging" ] || [ -L "$rejected_build_staging" ] \ + || [ -e "$rejected_build_context" ] || [ -L "$rejected_build_context" ]; then + echo "The '$unsafe_build_fixture' local build left its temporary context behind" >&2 + exit 1 + fi +done + +for unsafe_fixture in \ + witness-key \ + key-backup \ + keystore \ + wallet \ + lowercase-wallet \ + node-id \ + database \ + logs \ + symbolic-link \ + fifo \ + locked-directory-0500 \ + locked-directory-000; do + rejected_distribution_context="$TEST_TMP/rejected-$unsafe_fixture-context" + if rejected_distribution_output=$(MOCK_UNZIP_FIXTURE="$unsafe_fixture" \ + run_export x86_64 "$SOURCE_ROOT" "$rejected_distribution_context" 2>&1); then + echo "An unsafe '$unsafe_fixture' distribution export unexpectedly succeeded" >&2 + exit 1 + fi + if [[ "$rejected_distribution_output" != *"build: refusing"* ]] \ + && [[ "$rejected_distribution_output" != *"failed to inspect the extracted local distribution"* ]]; then + echo "The '$unsafe_fixture' distribution rejection was unclear:" >&2 + echo "$rejected_distribution_output" >&2 + exit 1 + fi + if [ -e "$rejected_distribution_context" ] \ + || [ -L "$rejected_distribution_context" ]; then + echo "A rejected '$unsafe_fixture' export left its context behind" >&2 + exit 1 + fi + if [ ! -s "$GRADLE_LOG" ] || [ -s "$DOCKER_LOG" ]; then + echo "The '$unsafe_fixture' export was not rejected between Gradle and Docker" >&2 + exit 1 + fi +done + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'PLAINTEXT_WITNESS_CONFIG' +localwitness = [ + "0123456789abcdef" +] +event.subscribe = { + dbconfig = "" +} +PLAINTEXT_WITNESS_CONFIG +expect_local_config_failure "localwitness" +rejected_export="$TEST_TMP/rejected-secret-context" +if export_output=$(run_export x86_64 "$SOURCE_ROOT" "$rejected_export" 2>&1); then + echo "A context export containing plaintext localwitness unexpectedly succeeded" >&2 + exit 1 +fi +if [[ "$export_output" != *"refusing to bake non-empty plaintext localwitness"* ]]; then + echo "The context-export secret rejection was unclear:" >&2 + echo "$export_output" >&2 + exit 1 +fi +if [ -e "$rejected_export" ] || [ -L "$rejected_export" ]; then + echo "A failed sensitive context export left its destination behind" >&2 + exit 1 +fi +if [ -s "$GRADLE_LOG" ] || [ -s "$DOCKER_LOG" ]; then + echo "A sensitive context export ran Gradle or Docker before rejection" >&2 + exit 1 +fi + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'INLINE_WITNESS_CONFIG' +localwitness: ["0123456789abcdef"] +event.subscribe = { dbconfig = "" } +INLINE_WITNESS_CONFIG +expect_local_config_failure "localwitness" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'DATABASE_CREDENTIAL_CONFIG' +localwitness = [] +event.subscribe.dbconfig = "events|db-user|db-password" +DATABASE_CREDENTIAL_CONFIG +expect_local_config_failure "event.subscribe.dbconfig" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'DNS_PRIVATE_CONFIG' +localwitness = [] +event.subscribe.dbconfig = "" +node.dns = { + dnsPrivate = "0123456789abcdef" + accessKeySecret = "" +} +DNS_PRIVATE_CONFIG +expect_local_config_failure "node.dns.dnsPrivate" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'DNS_ACCESS_SECRET_CONFIG' +localwitness = [] +event.subscribe.dbconfig = "" +node.dns.dnsPrivate = "" +node.dns.accessKeySecret = "cloud-dns-secret" +DNS_ACCESS_SECRET_CONFIG +expect_local_config_failure "node.dns.accessKeySecret" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'SUBSTITUTED_DNS_SECRET_CONFIG' +localwitness = [] +event.subscribe.dbconfig = "" +node.dns.dnsPrivate = "" ${?DNS_PRIVATE_KEY} +node.dns.accessKeySecret = "" +SUBSTITUTED_DNS_SECRET_CONFIG +expect_local_config_failure "node.dns.dnsPrivate" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'APPENDED_WITNESS_CONFIG' +localwitness = [] +localwitness += ["0123456789abcdef"] +event.subscribe = { dbconfig = "" } +APPENDED_WITNESS_CONFIG +expect_local_config_failure "localwitness" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'SUBSTITUTED_WITNESS_CONFIG' +localwitness = [] ${?WITNESS_KEYS} +event.subscribe = { dbconfig = "" } +SUBSTITUTED_WITNESS_CONFIG +expect_local_config_failure "localwitness" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'SUBSTITUTED_DATABASE_CONFIG' +localwitness = [] +event.subscribe.dbconfig = "" ${?EVENT_DATABASE_CREDENTIALS} +SUBSTITUTED_DATABASE_CONFIG +expect_local_config_failure "event.subscribe.dbconfig" + +cat > "$SOURCE_ROOT/framework/src/main/resources/config.conf" <<'SAFE_LOCAL_CONFIG' +localwitness = [ + "" +] +event.subscribe = { dbconfig = "" } +node.dns = { dnsPrivate = "", accessKeySecret = "" } +# localwitness = ["commented-example-key"] +# dbconfig = "commented|example|credentials" +# dnsPrivate = "commented-private-key" +# accessKeySecret = "commented-cloud-secret" +SAFE_LOCAL_CONFIG +run_build x86_64 "$SOURCE_ROOT" --source local >/dev/null +assert_context_file "./java-tron/config.conf" + +exported_context="$TEST_TMP/exported-local-context" +run_export x86_64 "$SOURCE_ROOT" "$exported_context" >/dev/null +for exported_file in \ + .dockerignore \ + Dockerfile \ + java-tron/bin/FullNode \ + java-tron/bin/FullNode.bat \ + java-tron/bin/java-tron.vmoptions \ + java-tron/config.conf \ + java-tron/lib/java-tron.jar; do + if [ ! -f "$exported_context/$exported_file" ]; then + echo "Exported local context is missing: $exported_file" >&2 + exit 1 + fi +done +if ! cmp -s "$REPOSITORY_ROOT/.dockerignore" "$exported_context/.dockerignore"; then + echo "Exported local context does not use the repository Docker allowlist" >&2 + diff -u "$REPOSITORY_ROOT/.dockerignore" \ + "$exported_context/.dockerignore" >&2 || true + exit 1 +fi +if [ "$(file_mode "$exported_context")" != 700 ]; then + echo "Exported local context is not mode 700" >&2 + exit 1 +fi +if [ -s "$DOCKER_LOG" ]; then + echo "Context-only export unexpectedly invoked docker build" >&2 + sed 's/^/ /' "$DOCKER_LOG" >&2 + exit 1 +fi +if second_export_output=$(run_export \ + x86_64 "$SOURCE_ROOT" "$exported_context" 2>&1); then + echo "A context export unexpectedly replaced an existing destination" >&2 + exit 1 +fi +if [[ "$second_export_output" != *"export context already exists"* ]]; then + echo "The existing export-context rejection was unclear:" >&2 + echo "$second_export_output" >&2 + exit 1 +fi +if [ -s "$GRADLE_LOG" ] || [ -s "$DOCKER_LOG" ]; then + echo "An existing export destination was rejected too late" >&2 + exit 1 +fi + +cp "$REPOSITORY_ROOT/framework/src/main/resources/config.conf" \ + "$SOURCE_ROOT/framework/src/main/resources/config.conf" +run_build x86_64 "$SOURCE_ROOT" --source local >/dev/null +assert_context_file "./java-tron/config.conf" + +run_build x86_64 "$REPOSITORY_ROOT" \ + --source remote \ + --source-ref develop \ + --source-repository https://example.com/java-tron.git >/dev/null +assert_argument "remote" +assert_argument "SOURCE_REPOSITORY=https://example.com/java-tron.git" +assert_argument "SOURCE_REF=develop" +assert_context_only_dockerfile + +run_build aarch64 "$SOURCE_ROOT" --source local >/dev/null +assert_argument "local" +assert_context_file "./Dockerfile" +assert_context_file "./java-tron/bin/FullNode" + +expect_failure "requires a value" --source +expect_failure "expected local or remote" --source invalid +expect_failure "can only be used with --source remote" --source local --source-ref develop +expect_failure "can only be used with --source local" \ + --source remote --export-context "$TEST_TMP/remote-export-context" +expect_failure "is not a valid parameter" --unknown +expect_failure "requires a value" --image + +run_build x86_64 "$REPOSITORY_ROOT" --image example/java-tron:dev >/dev/null +assert_argument "example/java-tron:dev" +assert_no_argument "tronprotocol/java-tron:local" + +if output=$( + cd -- "$REPOSITORY_ROOT" + PATH="$MOCK_BIN:$PATH" MOCK_DOCKER_VERSION=22.0.0 \ + bash "$DOCKER_SCRIPT" --build 2>&1 +); then + echo "Expected Docker 22 to be rejected" >&2 + exit 1 +fi +if [[ "$output" != *"Docker 23.0 or later is required"* ]]; then + echo "The Docker minimum-version failure is unclear:" >&2 + echo "$output" >&2 + exit 1 +fi + +run_node >/dev/null +assert_argument "-d" +assert_no_argument "-it" +assert_argument "127.0.0.1:8090:8090" +assert_argument "127.0.0.1:50051:50051" +assert_argument "18888:18888" +assert_argument "18888:18888/udp" +assert_no_argument "8090:8090" +assert_no_argument "50051:50051" +assert_argument "16g" +assert_argument "JAVA_OPTS=-Xms2g -XX:MaxRAMPercentage=60.0 -XX:MaxDirectMemorySize=1g" +assert_argument "--user" +assert_argument "10001:10001" +assert_argument "--security-opt" +assert_argument "no-new-privileges" +assert_no_argument "--cap-drop" +assert_no_argument "$TEST_TMP_PHYSICAL/config:/java-tron/config:ro" +assert_no_argument "$TEST_TMP_PHYSICAL/config:/java-tron/config" +assert_argument "$TEST_TMP_PHYSICAL/output-directory:/java-tron/output-directory" +assert_argument "$TEST_TMP_PHYSICAL/logs:/java-tron/logs" +assert_argument "/java-tron/config.conf" +assert_argument "--name" +assert_argument "tronprotocol-java-tron" +assert_argument "tronprotocol/java-tron:local" +assert_no_argument "tronprotocol/java-tron:latest" +assert_argument_count "-p" 4 +assert_argument_count "-v" 2 +assert_argument_count "--env" 1 +assert_run_argument_count "--network" 2 +assert_run_argument_count "none" 2 +assert_run_argument_count "--read-only" 2 +assert_run_argument_count "--cap-drop" 2 +assert_run_argument_count "ALL" 2 +assert_run_argument_count "--cap-add" 1 +assert_run_argument_count "CHOWN" 1 +assert_run_argument_count "--pull" 1 +assert_run_argument_count "missing" 1 +assert_run_argument_count "$RUNTIME_INIT_IMAGE" 1 +assert_chown_uses_pinned_helper "tronprotocol/java-tron:local" +if [ -s "$DOWNLOAD_LOG" ]; then + echo "--update-config false unexpectedly downloaded an existing configuration" >&2 + exit 1 +fi +MOCK_IMAGE_ARCH=arm64 run_node >/dev/null +assert_argument "JAVA_OPTS=-Xms2g -XX:MaxRAMPercentage=60.0" +assert_no_argument "JAVA_OPTS=-Xms2g -XX:MaxRAMPercentage=60.0 -XX:MaxDirectMemorySize=1g" +MOCK_IMAGE_ARCH=amd64 + +ROOTFUL_USERNS_DATA="$TEST_TMP/rootful-userns-data" +MOCK_DOCKER_SECURITY_OPTIONS='["name=seccomp,profile=builtin","name=userns"]' \ +MOCK_PERMISSION_STATUS=53 \ + expect_run_failure "rootful Docker userns-remap cannot automatically initialize" \ + --data-dir "$ROOTFUL_USERNS_DATA" -c /java-tron/custom.conf +assert_run_argument_count "CHOWN" 0 +assert_run_argument_count "$RUNTIME_INIT_IMAGE" 0 +if grep -Fqx -- "-d" "$DOCKER_RUN_LOG"; then + echo "A rootful userns-remap initialization reached the detached node run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi + +ROOTFUL_USERNS_PREPROVISIONED_DATA="$TEST_TMP/rootful-userns-preprovisioned-data" +MOCK_DOCKER_SECURITY_OPTIONS='["name=seccomp,profile=builtin","name=userns"]' \ + run_node --data-dir "$ROOTFUL_USERNS_PREPROVISIONED_DATA" \ + -c /java-tron/custom.conf >/dev/null +assert_run_argument_count "CHOWN" 0 +assert_run_argument_count "$RUNTIME_INIT_IMAGE" 0 +assert_argument "-d" + +ROOTLESS_DATA="$TEST_TMP/rootless-data" +MOCK_DOCKER_SECURITY_OPTIONS='["name=seccomp,profile=builtin","name=userns","name=rootless"]' \ + run_node --data-dir "$ROOTLESS_DATA" -c /java-tron/custom.conf >/dev/null +assert_run_argument_count "$RUNTIME_INIT_IMAGE" 1 + +DOCKER_INFO_FAILURE_DATA="$TEST_TMP/docker-info-failure-data" +MOCK_DOCKER_INFO_STATUS=52 \ + expect_run_failure "failed to inspect Docker user-namespace configuration" \ + --data-dir "$DOCKER_INFO_FAILURE_DATA" -c /java-tron/custom.conf +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A failed namespace inspection reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi + +CUSTOM_IMAGE_INIT_DATA="$TEST_TMP/custom-image-init-data" +run_node --image example/untrusted-java-tron:ci \ + --data-dir "$CUSTOM_IMAGE_INIT_DATA" -c /java-tron/custom.conf >/dev/null +assert_chown_uses_pinned_helper "example/untrusted-java-tron:ci" + +MOCK_IMAGE_USER=root expect_run_failure \ + "must run as UID:GID 10001:10001" -c /java-tron/custom.conf + +run_node --data-dir "$TEST_TMP/external-data" >/dev/null +assert_no_argument "$TEST_TMP_PHYSICAL/external-data/config:/java-tron/config:ro" +assert_no_argument "$TEST_TMP_PHYSICAL/external-data/config:/java-tron/config" +assert_argument "$TEST_TMP_PHYSICAL/external-data/output-directory:/java-tron/output-directory" +assert_argument "$TEST_TMP_PHYSICAL/external-data/logs:/java-tron/logs" +assert_no_argument "$TEST_TMP_PHYSICAL/config:/java-tron/config" +assert_no_argument "$TEST_TMP_PHYSICAL/output-directory:/java-tron/output-directory" + +run_node --data-dir relative-data >/dev/null +assert_no_argument "$TEST_TMP_PHYSICAL/relative-data/config:/java-tron/config:ro" +assert_no_argument "$TEST_TMP_PHYSICAL/relative-data/config:/java-tron/config" +assert_argument "$TEST_TMP_PHYSICAL/relative-data/output-directory:/java-tron/output-directory" +assert_argument "$TEST_TMP_PHYSICAL/relative-data/logs:/java-tron/logs" + +DATA_DIR_TARGET="$TEST_TMP/data-dir-target" +DATA_DIR_LINK="$TEST_TMP/data-dir-link" +mkdir -p "$DATA_DIR_TARGET" +ln -s "$DATA_DIR_TARGET" "$DATA_DIR_LINK" +run_node --data-dir "$DATA_DIR_LINK" -c /java-tron/custom.conf >/dev/null +assert_no_argument "$TEST_TMP_PHYSICAL/data-dir-target/config:/java-tron/config:ro" +assert_argument "$TEST_TMP_PHYSICAL/data-dir-target/output-directory:/java-tron/output-directory" +assert_argument "$TEST_TMP_PHYSICAL/data-dir-target/logs:/java-tron/logs" +assert_no_argument "$DATA_DIR_LINK/output-directory:/java-tron/output-directory" + +GROUP_WRITABLE_DATA="$TEST_TMP/group-writable-data" +mkdir -p "$GROUP_WRITABLE_DATA" +chmod 0770 "$GROUP_WRITABLE_DATA" +expect_run_failure "data directory path must not be group- or other-writable" \ + --data-dir "$GROUP_WRITABLE_DATA" -c /java-tron/custom.conf +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A group-writable data directory reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi +run_node --data-dir "$GROUP_WRITABLE_DATA" -c /java-tron/custom.conf \ + -v /host/config.conf:/java-tron/config:ro \ + -v /host/output:/java-tron/output-directory \ + -v /host/logs:/java-tron/logs >/dev/null +assert_argument "/host/config.conf:/java-tron/config:ro" +assert_argument "/host/output:/java-tron/output-directory" +assert_argument "/host/logs:/java-tron/logs" + +WORLD_WRITABLE_PARENT="$TEST_TMP/world-writable-parent" +mkdir -p "$WORLD_WRITABLE_PARENT/data" +chmod 0777 "$WORLD_WRITABLE_PARENT" +chmod 0700 "$WORLD_WRITABLE_PARENT/data" +expect_run_failure "data directory path must not be group- or other-writable" \ + --data-dir "$WORLD_WRITABLE_PARENT/data" -c /java-tron/custom.conf +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A data directory beneath a world-writable ancestor reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi +chmod 0700 "$WORLD_WRITABLE_PARENT" + +WORLD_WRITABLE_LINK_PARENT="$TEST_TMP/world-writable-link-parent" +SAFE_LINK_TARGET="$TEST_TMP/safe-link-target" +mkdir -p "$WORLD_WRITABLE_LINK_PARENT" "$SAFE_LINK_TARGET" +chmod 0777 "$WORLD_WRITABLE_LINK_PARENT" +ln -s "$SAFE_LINK_TARGET" "$WORLD_WRITABLE_LINK_PARENT/data-link" +expect_run_failure "data directory path must not be group- or other-writable" \ + --data-dir "$WORLD_WRITABLE_LINK_PARENT/data-link/" -c /java-tron/custom.conf +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A data-directory link beneath a world-writable parent reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi +chmod 0700 "$WORLD_WRITABLE_LINK_PARENT" + +LEAF_LINK_TARGET="$TEST_TMP/managed-leaf-target" +mkdir -p "$LEAF_LINK_TARGET/existing-entry" +for managed_leaf in output-directory logs; do + MANAGED_LINK_DATA="$TEST_TMP/managed-link-$managed_leaf" + mkdir -p "$MANAGED_LINK_DATA" + if [ "$managed_leaf" = logs ]; then + mkdir -p "$MANAGED_LINK_DATA/output-directory" + fi + ln -s "$LEAF_LINK_TARGET" "$MANAGED_LINK_DATA/$managed_leaf" + expect_run_failure "managed path must not be a symbolic link" \ + --data-dir "$MANAGED_LINK_DATA" -c /java-tron/custom.conf + if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A managed leaf symlink reached docker run: $managed_leaf" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 + fi +done + +MANAGED_CONFIG_LINK_DATA="$TEST_TMP/managed-link-config" +mkdir -p "$MANAGED_CONFIG_LINK_DATA" +ln -s "$LEAF_LINK_TARGET" "$MANAGED_CONFIG_LINK_DATA/config" +expect_run_failure "managed path must not be a symbolic link" \ + --data-dir "$MANAGED_CONFIG_LINK_DATA" --net private +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A managed configuration symlink reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi + +DANGLING_LINK_DATA="$TEST_TMP/managed-link-dangling" +mkdir -p "$DANGLING_LINK_DATA" +ln -s "$TEST_TMP/missing-managed-target" "$DANGLING_LINK_DATA/output-directory" +expect_run_failure "managed path must not be a symbolic link" \ + --data-dir "$DANGLING_LINK_DATA" -c /java-tron/custom.conf +if [ -s "$DOCKER_RUN_LOG" ]; then + echo "A dangling managed leaf symlink reached docker run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi + +PARTIAL_DATA_DIR="$TEST_TMP/partial-runtime-data" +mkdir -p "$PARTIAL_DATA_DIR/output-directory" "$PARTIAL_DATA_DIR/logs" +touch "$PARTIAL_DATA_DIR/output-directory/existing-database-entry" +run_node --data-dir "$PARTIAL_DATA_DIR" -c /java-tron/custom.conf >/dev/null +assert_run_argument_count \ + "$TEST_TMP_PHYSICAL/partial-runtime-data/output-directory:/java-tron/output-directory" 2 +assert_run_argument_count \ + "$TEST_TMP_PHYSICAL/partial-runtime-data/logs:/java-tron/logs" 3 + +run_node -c /java-tron/custom.conf \ + -p 8090:8090 \ + -p 50051:50051 \ + -p 28888:18888 \ + -v /host/config.conf:/java-tron/config:ro \ + -v /host/logs:/java-tron/logs \ + -v /host/extra:/extra:ro \ + -e TZ=UTC \ + --env FEATURE_FLAG=enabled \ + --memory 32g \ + --jvm-opts "-Xms4g -Xmx18g -XX:MaxDirectMemorySize=2g" \ + -- --p2p-disable false --log-config "/java-tron/log configs/logback.xml" >/dev/null +assert_argument "8090:8090" +assert_argument "50051:50051" +assert_argument "28888:18888" +assert_argument "18888:18888/udp" +assert_no_argument "127.0.0.1:8090:8090" +assert_no_argument "127.0.0.1:50051:50051" +assert_no_argument "18888:18888" +assert_argument "/host/config.conf:/java-tron/config:ro" +assert_argument "/host/logs:/java-tron/logs" +assert_argument "/host/extra:/extra:ro" +assert_no_argument "$TEST_TMP_PHYSICAL/config:/java-tron/config" +assert_no_argument "$TEST_TMP_PHYSICAL/logs:/java-tron/logs" +assert_argument "$TEST_TMP_PHYSICAL/output-directory:/java-tron/output-directory" +assert_argument "TZ=UTC" +assert_argument "FEATURE_FLAG=enabled" +assert_argument "32g" +assert_argument "JAVA_OPTS=-Xms4g -Xmx18g -XX:MaxDirectMemorySize=2g" +assert_argument "/java-tron/custom.conf" +assert_trailing_arguments \ + "tronprotocol/java-tron:local" \ + "-c" \ + "/java-tron/custom.conf" \ + "--p2p-disable" \ + "false" \ + "--log-config" \ + "/java-tron/log configs/logback.xml" +assert_argument_count "-p" 4 +assert_argument_count "-v" 4 +assert_argument_count "--env" 3 +if [ -s "$DOWNLOAD_LOG" ]; then + echo "A custom configuration unexpectedly triggered a download" >&2 + exit 1 +fi + +CUSTOM_PRIVATE_CONFIG_DATA="$TEST_TMP/custom-private-config-data" +MOCK_CURL_FAIL=true run_node \ + --net private \ + --update-config true \ + --data-dir "$CUSTOM_PRIVATE_CONFIG_DATA" \ + -v /host/private-config:/java-tron/config:ro >/dev/null +assert_argument "/host/private-config:/java-tron/config:ro" +assert_argument "$CUSTOM_PRIVATE_CONFIG_DATA/output-directory:/java-tron/output-directory" +assert_argument "$CUSTOM_PRIVATE_CONFIG_DATA/logs:/java-tron/logs" +assert_argument "/java-tron/config/private_net_config.conf" +if [ -s "$DOWNLOAD_LOG" ]; then + echo "A custom private configuration mount unexpectedly triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +if [ -e "$CUSTOM_PRIVATE_CONFIG_DATA/config" ]; then + echo "A custom private configuration mount created an unused default configuration" >&2 + exit 1 +fi + +DEFAULT_UMASK_DATA="$TEST_TMP/default-umask-runtime-data" +( + umask 022 + run_node --data-dir "$DEFAULT_UMASK_DATA" -c /java-tron/custom.conf >/dev/null +) +assert_mode 700 "$DEFAULT_UMASK_DATA/output-directory" +assert_mode 700 "$DEFAULT_UMASK_DATA/logs" + +EMPTY_EXISTING_RUNTIME_DATA="$TEST_TMP/empty-existing-runtime-data" +mkdir -p "$EMPTY_EXISTING_RUNTIME_DATA/output-directory" \ + "$EMPTY_EXISTING_RUNTIME_DATA/logs" +chmod 0755 "$EMPTY_EXISTING_RUNTIME_DATA/output-directory" \ + "$EMPTY_EXISTING_RUNTIME_DATA/logs" +run_node --data-dir "$EMPTY_EXISTING_RUNTIME_DATA" \ + -c /java-tron/custom.conf >/dev/null +assert_mode 700 "$EMPTY_EXISTING_RUNTIME_DATA/output-directory" +assert_mode 700 "$EMPTY_EXISTING_RUNTIME_DATA/logs" + +NONEMPTY_EXISTING_RUNTIME_DATA="$TEST_TMP/nonempty-existing-runtime-data" +mkdir -p "$NONEMPTY_EXISTING_RUNTIME_DATA/output-directory" \ + "$NONEMPTY_EXISTING_RUNTIME_DATA/logs" +touch "$NONEMPTY_EXISTING_RUNTIME_DATA/output-directory/existing-database-entry" \ + "$NONEMPTY_EXISTING_RUNTIME_DATA/logs/existing-log-entry" +chmod 0755 "$NONEMPTY_EXISTING_RUNTIME_DATA/output-directory" \ + "$NONEMPTY_EXISTING_RUNTIME_DATA/logs" +if ! permission_warning=$(run_node --data-dir "$NONEMPTY_EXISTING_RUNTIME_DATA" \ + -c /java-tron/custom.conf 2>&1); then + echo "An existing group/other-accessible runtime directory was rejected" >&2 + echo "$permission_warning" >&2 + exit 1 +fi +for existing_runtime_directory in output-directory logs; do + warning_path="$NONEMPTY_EXISTING_RUNTIME_DATA/$existing_runtime_directory" + if [[ "$permission_warning" != *"existing non-empty runtime directory is accessible by group or other users; preserving mode 755: $warning_path"* ]] \ + || [[ "$permission_warning" != *"chmod 0700 $warning_path"* ]]; then + echo "Missing runtime-directory confidentiality warning for $warning_path" >&2 + echo "$permission_warning" >&2 + exit 1 + fi + assert_mode 755 "$warning_path" +done + +CUSTOM_RUNTIME_PERMISSIONS="$TEST_TMP/custom-runtime-permissions" +mkdir -p "$CUSTOM_RUNTIME_PERMISSIONS" +chmod 0755 "$CUSTOM_RUNTIME_PERMISSIONS" +run_node -c /java-tron/custom.conf \ + -v "$CUSTOM_RUNTIME_PERMISSIONS:/java-tron/logs" >/dev/null +assert_mode 755 "$CUSTOM_RUNTIME_PERMISSIONS" + +STRICT_UMASK_DATA="$TEST_TMP/strict-umask-runtime-data" +( + umask 077 + run_node --data-dir "$STRICT_UMASK_DATA" -c /java-tron/custom.conf >/dev/null +) +assert_mode 700 "$STRICT_UMASK_DATA/output-directory" +assert_mode 700 "$STRICT_UMASK_DATA/logs" +touch "$STRICT_UMASK_DATA/output-directory/existing-database-entry" +touch "$STRICT_UMASK_DATA/logs/existing-log-entry" +( + umask 077 + run_node --data-dir "$STRICT_UMASK_DATA" -c /java-tron/custom.conf >/dev/null +) +assert_mode 700 "$STRICT_UMASK_DATA/output-directory" +assert_mode 700 "$STRICT_UMASK_DATA/logs" + +RUNTIME_OWNED_PRIVATE_DATA="$TEST_TMP/runtime-owned-private-data" +mkdir -p "$RUNTIME_OWNED_PRIVATE_DATA/output-directory" +touch "$RUNTIME_OWNED_PRIVATE_DATA/output-directory/existing-database-entry" +chmod 0700 "$RUNTIME_OWNED_PRIVATE_DATA/output-directory" +MOCK_HOST_FIND_DENIED_PATH="$RUNTIME_OWNED_PRIVATE_DATA/output-directory" \ +MOCK_RUNTIME_OWNER_PATH="$RUNTIME_OWNED_PRIVATE_DATA/output-directory" \ +MOCK_RUNTIME_HOST_OWNER_UID=10001 \ +MOCK_EXECUTE_RUNTIME_CHECK=true \ + run_node --data-dir "$RUNTIME_OWNED_PRIVATE_DATA" \ + -c /java-tron/custom.conf \ + -v /host/logs:/java-tron/logs >/dev/null +assert_run_argument_count "CHOWN" 0 +assert_mode 700 "$RUNTIME_OWNED_PRIVATE_DATA/output-directory" + +USERNS_MAPPED_PRIVATE_DATA="$TEST_TMP/userns-mapped-private-data" +mkdir -p "$USERNS_MAPPED_PRIVATE_DATA/output-directory" +touch "$USERNS_MAPPED_PRIVATE_DATA/output-directory/existing-database-entry" +chmod 0700 "$USERNS_MAPPED_PRIVATE_DATA/output-directory" +MOCK_HOST_FIND_DENIED_PATH="$USERNS_MAPPED_PRIVATE_DATA/output-directory" \ +MOCK_RUNTIME_OWNER_PATH="$USERNS_MAPPED_PRIVATE_DATA/output-directory" \ +MOCK_RUNTIME_HOST_OWNER_UID=231073 \ +MOCK_EXECUTE_RUNTIME_CHECK=true \ + run_node --data-dir "$USERNS_MAPPED_PRIVATE_DATA" \ + -c /java-tron/custom.conf \ + -v /host/logs:/java-tron/logs >/dev/null +assert_run_argument_count "CHOWN" 0 +assert_mode 700 "$USERNS_MAPPED_PRIVATE_DATA/output-directory" + +USERNS_RUNTIME_DENIED_DATA="$TEST_TMP/userns-runtime-denied-data" +mkdir -p "$USERNS_RUNTIME_DENIED_DATA/output-directory" +touch "$USERNS_RUNTIME_DENIED_DATA/output-directory/existing-database-entry" +MOCK_HOST_FIND_DENIED_PATH="$USERNS_RUNTIME_DENIED_DATA/output-directory" \ +MOCK_RUNTIME_FIND_DENIED_PATH="$USERNS_RUNTIME_DENIED_DATA/output-directory" \ +MOCK_RUNTIME_OWNER_PATH="$USERNS_RUNTIME_DENIED_DATA/output-directory" \ +MOCK_RUNTIME_HOST_OWNER_UID=231074 \ +MOCK_EXECUTE_RUNTIME_CHECK=true \ + expect_run_failure "runtime directories must be writable" \ + --data-dir "$USERNS_RUNTIME_DENIED_DATA" \ + -c /java-tron/custom.conf \ + -v /host/logs:/java-tron/logs + +HOST_UNREADABLE_ACCESSIBLE_MODE_DATA="$TEST_TMP/host-unreadable-accessible-mode-data" +mkdir -p "$HOST_UNREADABLE_ACCESSIBLE_MODE_DATA/output-directory" +touch "$HOST_UNREADABLE_ACCESSIBLE_MODE_DATA/output-directory/existing-database-entry" +MOCK_HOST_FIND_DENIED_PATH="$HOST_UNREADABLE_ACCESSIBLE_MODE_DATA/output-directory" \ +MOCK_RUNTIME_OWNER_PATH="$HOST_UNREADABLE_ACCESSIBLE_MODE_DATA/output-directory" \ +MOCK_RUNTIME_OWNER_MODE=711 \ + expect_run_failure "host-unreadable runtime directory must use mode 0700" \ + --data-dir "$HOST_UNREADABLE_ACCESSIBLE_MODE_DATA" \ + -c /java-tron/custom.conf \ + -v /host/logs:/java-tron/logs + +RUNTIME_FIND_FAILURE_DATA="$TEST_TMP/runtime-find-failure-data" +mkdir -p "$RUNTIME_FIND_FAILURE_DATA/output-directory" +touch "$RUNTIME_FIND_FAILURE_DATA/output-directory/existing-database-entry" +MOCK_RUNTIME_FIND_DENIED_PATH="$RUNTIME_FIND_FAILURE_DATA/output-directory" \ +MOCK_EXECUTE_RUNTIME_CHECK=true \ + expect_run_failure "runtime directories must be writable" \ + --data-dir "$RUNTIME_FIND_FAILURE_DATA" \ + -c /java-tron/custom.conf \ + -v /host/logs:/java-tron/logs + +PRESERVED_RUNTIME_MODES_DATA="$TEST_TMP/preserved-runtime-modes-data" +mkdir -p "$PRESERVED_RUNTIME_MODES_DATA/output-directory" \ + "$PRESERVED_RUNTIME_MODES_DATA/logs" +touch "$PRESERVED_RUNTIME_MODES_DATA/output-directory/existing-database-entry" +touch "$PRESERVED_RUNTIME_MODES_DATA/logs/existing-log-entry" +chmod 0700 "$PRESERVED_RUNTIME_MODES_DATA/output-directory" \ + "$PRESERVED_RUNTIME_MODES_DATA/logs" +run_node --data-dir "$PRESERVED_RUNTIME_MODES_DATA" \ + -c /java-tron/custom.conf >/dev/null +assert_mode 700 "$PRESERVED_RUNTIME_MODES_DATA/output-directory" +assert_mode 700 "$PRESERVED_RUNTIME_MODES_DATA/logs" + +rmdir "$TEST_TMP/config" +( + umask 077 + run_node --net private --update-config false >/dev/null +) +assert_argument "/java-tron/config/private_net_config.conf" +if ! grep -Fq -- \ + "https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/private_net_config.conf|$TEST_TMP_PHYSICAL/config/private_net_config.conf.tmp." \ + "$DOWNLOAD_LOG"; then + echo "--update-config false did not download a missing configuration" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +if [ "$(cat "$TEST_TMP/config/private_net_config.conf")" != "downloaded-content" ]; then + echo "The missing configuration was not written to the destination" >&2 + exit 1 +fi +assert_mode 755 "$TEST_TMP/config" +assert_mode 644 "$TEST_TMP/config/private_net_config.conf" +assert_run_argument_count \ + "$TEST_TMP_PHYSICAL/config:/java-tron/config:ro" 2 +assert_run_argument_count \ + "test ! -L \"\$1\" && test -f \"\$1\" && test -s \"\$1\" && test -r \"\$1\"" 1 + +RETAINED_CONFIG_CONTENT=$(cat "$TEST_TMP/config/private_net_config.conf") +run_node --net private --update-config false >/dev/null +if [ "$(cat "$TEST_TMP/config/private_net_config.conf")" != \ + "$RETAINED_CONFIG_CONTENT" ]; then + echo "An existing private configuration was unexpectedly replaced" >&2 + exit 1 +fi +if [ -s "$DOWNLOAD_LOG" ]; then + echo "An existing private configuration unexpectedly triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi + +DIRECTORY_CONFIG_DATA="$TEST_TMP/directory-private-config" +mkdir -p \ + "$DIRECTORY_CONFIG_DATA/config/private_net_config.conf" +expect_run_failure \ + "Download destination exists but is not a non-symbolic-link regular file" \ + --data-dir "$DIRECTORY_CONFIG_DATA" --net private --update-config false +if find "$DIRECTORY_CONFIG_DATA/config/private_net_config.conf" \ + -mindepth 1 -print -quit | grep -q .; then + echo "A downloaded temporary file was moved into the configuration directory" >&2 + exit 1 +fi +if [ -s "$DOWNLOAD_LOG" ]; then + echo "A directory at the private configuration path triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi + +SYMLINK_CONFIG_DATA="$TEST_TMP/symlink-private-config" +SYMLINK_CONFIG_TARGET="$TEST_TMP/symlink-private-config-target" +mkdir -p "$SYMLINK_CONFIG_DATA/config" +printf 'linked-private-config\n' > "$SYMLINK_CONFIG_TARGET" +ln -s "$SYMLINK_CONFIG_TARGET" \ + "$SYMLINK_CONFIG_DATA/config/private_net_config.conf" +expect_run_failure \ + "Download destination exists but is not a non-symbolic-link regular file" \ + --data-dir "$SYMLINK_CONFIG_DATA" --net private --update-config false +if [ "$(cat "$SYMLINK_CONFIG_TARGET")" != "linked-private-config" ]; then + echo "The rejected private-configuration symlink target was modified" >&2 + exit 1 +fi +if [ ! -L "$SYMLINK_CONFIG_DATA/config/private_net_config.conf" ]; then + echo "The rejected private-configuration symlink was replaced" >&2 + exit 1 +fi +if [ -s "$DOWNLOAD_LOG" ]; then + echo "A symlink at the private configuration path triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi + +FIFO_CONFIG_DATA="$TEST_TMP/fifo-private-config" +mkdir -p "$FIFO_CONFIG_DATA/config" +mkfifo "$FIFO_CONFIG_DATA/config/private_net_config.conf" +expect_run_failure \ + "Download destination exists but is not a non-symbolic-link regular file" \ + --data-dir "$FIFO_CONFIG_DATA" --net private --update-config true +if [ ! -p "$FIFO_CONFIG_DATA/config/private_net_config.conf" ]; then + echo "The rejected private-configuration FIFO was replaced" >&2 + exit 1 +fi +if [ -s "$DOWNLOAD_LOG" ]; then + echo "A FIFO at the private configuration path triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi + +chmod 0600 "$TEST_TMP/config/private_net_config.conf" +( + umask 077 + run_node --net private --update-config true >/dev/null +) +assert_mode 755 "$TEST_TMP/config" +assert_mode 644 "$TEST_TMP/config/private_net_config.conf" + +UNREADABLE_CONFIG_DATA="$TEST_TMP/unreadable-private-config" +mkdir -p "$UNREADABLE_CONFIG_DATA/config" +printf 'existing-private-config\n' > \ + "$UNREADABLE_CONFIG_DATA/config/private_net_config.conf" +chmod 0755 "$UNREADABLE_CONFIG_DATA/config" +chmod 0600 "$UNREADABLE_CONFIG_DATA/config/private_net_config.conf" +MOCK_CONFIG_READ_STATUS=49 expect_run_failure \ + "private configuration must be readable by java-tron UID:GID 10001:10001" \ + --data-dir "$UNREADABLE_CONFIG_DATA" --net private +if grep -Fqx -- "-d" "$DOCKER_RUN_LOG"; then + echo "An unreadable private configuration reached the detached node run" >&2 + sed 's/^/ /' "$DOCKER_RUN_LOG" >&2 + exit 1 +fi + +MISSING_MAIN_DATA="$TEST_TMP/missing-main-data" +run_node --data-dir "$MISSING_MAIN_DATA" --net main --update-config false >/dev/null +assert_argument "/java-tron/config.conf" +assert_no_argument "$TEST_TMP_PHYSICAL/missing-main-data/config:/java-tron/config:ro" +if [ -s "$DOWNLOAD_LOG" ]; then + echo "The missing Mainnet configuration unexpectedly triggered a download" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi +if [ -e "$MISSING_MAIN_DATA/config/main_net_config.conf" ]; then + echo "The Mainnet run left a host-side main_net_config.conf" >&2 + exit 1 +fi + +run_node --image example/java-tron:ci --net main --update-config true >/dev/null +assert_argument "example/java-tron:ci" +assert_argument "/java-tron/config.conf" +if [ -s "$DOWNLOAD_LOG" ]; then + echo "--update-config true unexpectedly downloaded a Mainnet configuration" >&2 + sed 's/^/ /' "$DOWNLOAD_LOG" >&2 + exit 1 +fi + +CONFIG_FILE_FOR_TEST="$TEST_TMP/config/private_net_config.conf" +rm -f "$CONFIG_FILE_FOR_TEST" +if MOCK_CURL_FAIL=true run_node --net private --update-config false >/dev/null 2>&1; then + echo "A failed initial configuration download unexpectedly succeeded" >&2 + exit 1 +fi +if [ -e "$CONFIG_FILE_FOR_TEST" ]; then + echo "A failed initial download left a destination configuration file" >&2 + exit 1 +fi +if compgen -G "$CONFIG_FILE_FOR_TEST.tmp.*" >/dev/null; then + echo "A failed initial download left a temporary configuration file" >&2 + exit 1 +fi + +if MOCK_CURL_EMPTY=true run_node --net private --update-config false \ + >/dev/null 2>&1; then + echo "An empty initial configuration download unexpectedly succeeded" >&2 + exit 1 +fi +if [ -e "$CONFIG_FILE_FOR_TEST" ]; then + echo "An empty initial download left a destination configuration file" >&2 + exit 1 +fi +if compgen -G "$CONFIG_FILE_FOR_TEST.tmp.*" >/dev/null; then + echo "An empty initial download left a temporary configuration file" >&2 + exit 1 +fi + +if output=$(MOCK_IMAGE_MISSING=true run_node -c /java-tron/custom.conf &1); then + echo "A missing image unexpectedly started a container without a TTY" >&2 + echo "$output" >&2 + exit 1 +fi +if [[ "$output" != *"compatible local image not found: tronprotocol/java-tron:local"* ]] \ + || [[ "$output" != *"bash docker.sh --build"* ]]; then + echo "A missing image did not produce a non-interactive error:" >&2 + echo "$output" >&2 + exit 1 +fi +if [[ "$output" == *"[y/n]"* ]]; then + echo "A missing image prompted for a pull without a TTY" >&2 + echo "$output" >&2 + exit 1 +fi + +for option in -v -p -e --env -c --net --update-config --memory --jvm-opts --data-dir --image --container-name; do + expect_run_failure "requires a value" "$option" +done +expect_run_failure "expected main or private" --net test +expect_run_failure "expected main or private" --net unsupported +expect_run_failure "must be true or false" --update-config sometimes +expect_run_failure "is not a valid parameter" --unknown +for jvm_env_name in JAVA_OPTS FULL_NODE_OPTS JAVA_TOOL_OPTIONS _JAVA_OPTIONS JDK_JAVA_OPTIONS; do + expect_run_failure "use --jvm-opts to set JVM options" -e "$jvm_env_name=-Xmx8g" + expect_run_failure "use --jvm-opts to set JVM options" --env "$jvm_env_name=-Xmx8g" +done + +run_node -c /java-tron/custom.conf -- --unknown-fullnode-option >/dev/null +assert_trailing_arguments \ + "tronprotocol/java-tron:local" \ + "-c" \ + "/java-tron/custom.conf" \ + "--unknown-fullnode-option" + +run_node --image example/java-tron:ci -c /java-tron/custom.conf >/dev/null +assert_argument "example/java-tron:ci" +assert_no_argument "tronprotocol/java-tron:local" + +run_node --container-name java-tron-smoke-ci -c /java-tron/custom.conf >/dev/null +assert_argument "--name" +assert_argument "java-tron-smoke-ci" +assert_no_argument "tronprotocol-java-tron" +expect_run_failure "invalid container name" --container-name "-bad" -c /java-tron/custom.conf + +JAVA_TRON_IMAGE=example/java-tron:from-env run_node -c /java-tron/custom.conf >/dev/null +assert_argument "example/java-tron:from-env" + +run_node --image tronprotocol/java-tron:local -c /java-tron/custom.conf >/dev/null +assert_argument "tronprotocol/java-tron:local" +assert_no_argument "tronprotocol/java-tron:latest" + +MOCK_IMAGE_MISSING=true expect_run_failure "image not found: missing/java-tron:tag" \ + --image missing/java-tron:tag -c /java-tron/custom.conf + +set +e +duplicate_output=$(MOCK_CONTAINER_EXISTS=true run_node -c /java-tron/custom.conf 2>&1) +duplicate_status=$? +set -e +if [ "$duplicate_status" -ne 1 ] \ + || [[ "$duplicate_output" != *"already exists"* ]] \ + || [[ "$duplicate_output" != *"Use --start"* ]]; then + echo "An existing container did not produce an actionable error" >&2 + echo "$duplicate_output" >&2 + exit 1 +fi +if [ -s "$DOCKER_LOG" ]; then + echo "docker run was called even though the container already exists" >&2 + exit 1 +fi + +set +e +MOCK_RUN_STATUS=47 run_node -c /java-tron/custom.conf >/dev/null 2>&1 +run_status=$? +set -e +if [ "$run_status" -ne 47 ]; then + echo "Expected docker run failure status 47, got $run_status" >&2 + exit 1 +fi + +if output=$( + MOCK_PERMISSION_STATUS=48 run_node \ + --data-dir "$TEST_TMP/permission-failure" \ + -c /java-tron/custom.conf 2>&1 +); then + echo "A runtime-directory ownership initialization failure unexpectedly succeeded" >&2 + exit 1 +fi +if [[ "$output" != *"failed to initialize runtime-directory ownership"* ]]; then + echo "A runtime-directory ownership failure did not produce an actionable error:" >&2 + echo "$output" >&2 + exit 1 +fi + +echo "docker.sh build and run tests passed" diff --git a/docker/tests/docker-workflow-test.sh b/docker/tests/docker-workflow-test.sh new file mode 100644 index 00000000000..76396ac64fc --- /dev/null +++ b/docker/tests/docker-workflow-test.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# GitHub Actions expressions are intentionally matched as literal text. +# shellcheck disable=SC2016 +set -euo pipefail + +test_dir=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +repository_root=$(cd -- "$test_dir/../.." >/dev/null 2>&1 && pwd) +workflow="$repository_root/.github/workflows/docker.yml" +config_path="framework/src/main/resources/config.conf" + +if [ "$(grep -Fxc -- " - '$config_path'" "$workflow" || true)" -ne 1 ]; then + echo "Docker CI push paths do not include $config_path exactly once." >&2 + exit 1 +fi + +if ! grep -Eq -- "^[[:space:]]+.*${config_path//./\\.}.*\\)$" "$workflow"; then + echo "Docker CI's pull-request selector does not rebuild images for $config_path." >&2 + exit 1 +fi + +assert_job_needs_changes_only() { + local job="$1" + + if ! awk -v job="$job" ' + $0 == " " job ":" { in_job = 1; next } + in_job && /^ [^[:space:]]/ { exit } + in_job && $0 == " needs: changes" { found = 1 } + END { exit !found } + ' "$workflow"; then + echo "Docker CI job $job must depend only on change analysis." >&2 + exit 1 + fi +} + +assert_job_needs_changes_only build-amd64 +assert_job_needs_changes_only build-arm64 +assert_job_needs_changes_only remote-build-amd64 +assert_job_needs_changes_only remote-build-arm64 + +assert_selector_case_sets() { + local case_label="$1" + local assignment="$2" + + if ! awk -v label="$case_label)" -v assignment="$assignment" ' + { + normalized = $0 + sub(/^[[:space:]]+/, "", normalized) + } + normalized == label { + in_case = 1 + found_case = 1 + next + } + in_case && /^[[:space:]]+;;$/ { + in_case = 0 + exit + } + in_case && normalized == assignment { + found_assignment = 1 + } + END { exit !(found_case && found_assignment) } + ' "$workflow"; then + echo "Docker CI selector case $case_label must set $assignment." >&2 + exit 1 + fi +} + +assert_job_contains() { + local job="$1" + local expected="$2" + + if ! awk -v job="$job" -v expected="$expected" ' + $0 == " " job ":" { in_job = 1; next } + in_job && /^ [^[:space:]]/ { exit } + in_job && index($0, expected) { found = 1 } + END { exit !found } + ' "$workflow"; then + echo "Docker CI job $job does not contain: $expected" >&2 + exit 1 + fi +} + +assert_selector_case_sets schedule 'source_mode=remote' +assert_selector_case_sets schedule 'remote_source_ref=master' +assert_selector_case_sets workflow_dispatch 'remote_source_ref="$REF_NAME"' +assert_selector_case_sets pull_request 'remote_source_ref=master' +assert_selector_case_sets push 'remote_source_ref=master' +assert_selector_case_sets docker/Dockerfile 'remote_amd64=true' +assert_selector_case_sets docker/arm64/Dockerfile 'remote_arm64=true' +assert_selector_case_sets 'docker/.dockerignore|.github/workflows/docker.yml' 'remote_amd64=true' +assert_selector_case_sets 'docker/.dockerignore|.github/workflows/docker.yml' 'remote_arm64=true' + +if [ "$(grep -Fxc -- ' remote_amd64=true' "$workflow" || true)" -ne 2 ] \ + || [ "$(grep -Fxc -- ' remote_arm64=true' "$workflow" || true)" -ne 2 ]; then + echo "Only Dockerfile and shared remote-context changes should request additional remote builds." >&2 + exit 1 +fi + +if [ "$(grep -Fxc -- ' remote_source_ref: ${{ steps.select.outputs.remote_source_ref }}' "$workflow" || true)" -ne 1 ]; then + echo "Docker CI must expose the selected remote source ref exactly once." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' remote_amd64: ${{ steps.select.outputs.remote_amd64 }}' "$workflow" || true)" -ne 1 ] \ + || [ "$(grep -Fxc -- ' remote_arm64: ${{ steps.select.outputs.remote_arm64 }}' "$workflow" || true)" -ne 1 ]; then + echo "Docker CI must expose both additional remote-build selectors." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' echo "remote_amd64=$remote_amd64" >> "$GITHUB_OUTPUT"' "$workflow" || true)" -ne 1 ] \ + || [ "$(grep -Fxc -- ' echo "remote_arm64=$remote_arm64" >> "$GITHUB_OUTPUT"' "$workflow" || true)" -ne 1 ] \ + || [ "$(grep -Fxc -- ' echo "remote_source_ref=$remote_source_ref" >> "$GITHUB_OUTPUT"' "$workflow" || true)" -ne 1 ]; then + echo "Docker CI selector must publish all additional remote-build outputs." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' REF_NAME: ${{ github.ref_name }}' "$workflow" || true)" -ne 1 ]; then + echo "Docker CI must pass the selected dispatch ref into the selector exactly once." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' SOURCE_REF=${{ needs.changes.outputs.remote_source_ref }}' "$workflow" || true)" -ne 4 ]; then + echo "Every remote build must consume the selected remote source ref." >&2 + exit 1 +fi +if grep -Fq -- 'SOURCE_REF=${{ github.ref_name }}' "$workflow"; then + echo "Remote builds must not derive their source ref directly from the workflow trigger ref." >&2 + exit 1 +fi + +assert_job_contains remote-build-amd64 "if: needs.changes.outputs.remote_amd64 == 'true'" +assert_job_contains remote-build-amd64 'file: docker/Dockerfile' +assert_job_contains remote-build-amd64 'target: remote' +assert_job_contains remote-build-amd64 'no-cache-filters: remote-builder' +assert_job_contains remote-build-amd64 'run: docker run --rm --env JAVA_OPTS=-version "$IMAGE"' +assert_job_contains remote-build-amd64 'run: bash docker/tests/vmoptions-test.sh "$IMAGE"' +assert_job_contains remote-build-amd64 'run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE"' +assert_job_contains remote-build-arm64 "if: needs.changes.outputs.remote_arm64 == 'true'" +assert_job_contains remote-build-arm64 'file: docker/arm64/Dockerfile' +assert_job_contains remote-build-arm64 'target: remote' +assert_job_contains remote-build-arm64 'no-cache-filters: remote-builder' +assert_job_contains remote-build-arm64 'run: docker run --rm --env JAVA_OPTS=-version "$IMAGE"' +assert_job_contains remote-build-arm64 'run: bash docker/tests/vmoptions-test.sh "$IMAGE"' +assert_job_contains remote-build-arm64 'run: bash docker/tests/docker-sh-run-smoke.sh "$IMAGE"' +assert_job_contains gate 'REMOTE_AMD64_REQUIRED: ${{ needs.changes.outputs.remote_amd64 }}' +assert_job_contains gate 'REMOTE_AMD64_RESULT: ${{ needs.remote-build-amd64.result }}' +assert_job_contains gate 'REMOTE_ARM64_REQUIRED: ${{ needs.changes.outputs.remote_arm64 }}' +assert_job_contains gate 'REMOTE_ARM64_RESULT: ${{ needs.remote-build-arm64.result }}' +assert_job_contains gate 'require_success "$REMOTE_AMD64_REQUIRED" "$REMOTE_AMD64_RESULT"' +assert_job_contains gate 'require_success "$REMOTE_ARM64_REQUIRED" "$REMOTE_ARM64_RESULT"' +assert_job_contains gate 'if: always()' + +if ! grep -Fq -- "group: docker-\${{ github.workflow }}-\${{ github.event_name == 'schedule' && 'schedule' || github.event.pull_request.number || github.ref }}" "$workflow"; then + echo "Scheduled Docker CI must use a concurrency group independent of default-branch pushes." >&2 + exit 1 +fi +if ! grep -Fq -- 'needs: [changes, script-check, build-amd64, build-arm64, remote-build-amd64, remote-build-arm64]' "$workflow"; then + echo "Docker CI gate must wait for the additional remote builds." >&2 + exit 1 +fi + +if [ "$(grep -Fxc -- ' uses: docker/setup-buildx-action@v4' "$workflow" || true)" -ne 4 ]; then + echo "All local/full and additional remote architecture jobs must set up the official Docker Buildx action." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' uses: docker/build-push-action@v7' "$workflow" || true)" -ne 6 ]; then + echo "All local and remote builds must use build-push-action." >&2 + exit 1 +fi +if [ "$(grep -Fc -- 'cache-from: type=gha,scope=java-tron-' "$workflow" || true)" -ne 6 ] \ + || [ "$(grep -Fc -- 'cache-to: type=gha,mode=max,scope=java-tron-' "$workflow" || true)" -ne 6 ]; then + echo "Each local and remote architecture build must use the architecture/source-specific GHA cache scope." >&2 + exit 1 +fi +if [ "$(grep -Fxc -- ' no-cache-filters: remote-builder' "$workflow" || true)" -ne 4 ]; then + echo "Remote builds must bypass cache for the mutable source-builder stage." >&2 + exit 1 +fi + +echo "Docker CI selection, parallelism, and Buildx cache tests passed" diff --git a/docker/tests/dockerignore-test.sh b/docker/tests/dockerignore-test.sh new file mode 100644 index 00000000000..01cdaeba712 --- /dev/null +++ b/docker/tests/dockerignore-test.sh @@ -0,0 +1,88 @@ +#!/bin/bash +set -euo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +REPOSITORY_ROOT=$(cd -- "$TEST_DIR/../.." >/dev/null 2>&1 && pwd) +TEST_TMP=$(mktemp -d "$REPOSITORY_ROOT/.dockerignore-test.XXXXXX") +CONTEXT="$TEST_TMP/context" +ARCHIVE="$TEST_TMP/context.tar" +MANIFEST="$TEST_TMP/manifest" + +cleanup() { + rm -rf "$TEST_TMP" +} +trap cleanup EXIT + +mkdir -p "$CONTEXT/java-tron/bin" "$CONTEXT/java-tron/lib/nested" \ + "$CONTEXT/java-tron/Wallet" "$CONTEXT/java-tron/wallet" \ + "$CONTEXT/java-tron/output-directory" +cp "$REPOSITORY_ROOT/.dockerignore" "$CONTEXT/.dockerignore" + +printf 'launcher\n' > "$CONTEXT/java-tron/bin/FullNode" +printf 'windows-launcher\n' > "$CONTEXT/java-tron/bin/FullNode.bat" +printf 'vm-options\n' > "$CONTEXT/java-tron/bin/java-tron.vmoptions" +printf 'unexpected-launcher\n' > "$CONTEXT/java-tron/bin/helper" +printf 'jar\n' > "$CONTEXT/java-tron/lib/java-tron.jar" +printf 'nested-jar\n' > "$CONTEXT/java-tron/lib/nested/hidden.jar" +printf 'mainnet-config\n' > "$CONTEXT/java-tron/config.conf" +printf 'unexpected-runtime-file\n' > "$CONTEXT/java-tron/README.txt" + +printf 'private-key\n' > "$CONTEXT/java-tron/witness.key" +printf 'private-key\n' > "$CONTEXT/java-tron/witness.KEY" +printf 'private-key\n' > "$CONTEXT/java-tron/witness.key.bak" +printf 'private-key\n' > "$CONTEXT/java-tron/witness.pem" +printf 'keystore\n' > "$CONTEXT/java-tron/localwitnesskeystore.json" +printf 'private-config\n' > "$CONTEXT/java-tron/private_net_config.conf" +printf 'wallet\n' > "$CONTEXT/java-tron/Wallet/account.json" +printf 'wallet\n' > "$CONTEXT/java-tron/wallet/account.json" +printf 'node-id\n' > "$CONTEXT/java-tron/nodeId.properties" +printf 'database\n' > "$CONTEXT/java-tron/output-directory/block.data" +printf 'unrelated-root-file\n' > "$CONTEXT/source.txt" + +printf '%s\n' \ + 'FROM scratch' \ + 'COPY . /context' \ + > "$CONTEXT/Dockerfile" + +DOCKER_BUILDKIT=1 docker build \ + --file "$CONTEXT/Dockerfile" \ + --output "type=tar,dest=$ARCHIVE" \ + "$CONTEXT" >/dev/null + +LC_ALL=C tar -tf "$ARCHIVE" | sed 's#^\./##' | LC_ALL=C sort > "$MANIFEST" + +for expected in \ + context/java-tron/bin/FullNode \ + context/java-tron/bin/FullNode.bat \ + context/java-tron/bin/java-tron.vmoptions \ + context/java-tron/config.conf \ + context/java-tron/lib/java-tron.jar; do + if ! grep -Fqx -- "$expected" "$MANIFEST"; then + echo "Allowed Docker context file is missing: $expected" >&2 + sed 's/^/ /' "$MANIFEST" >&2 + exit 1 + fi +done + +for rejected in \ + context/java-tron/witness.key \ + context/java-tron/witness.KEY \ + context/java-tron/witness.key.bak \ + context/java-tron/witness.pem \ + context/java-tron/localwitnesskeystore.json \ + context/java-tron/private_net_config.conf \ + context/java-tron/Wallet/account.json \ + context/java-tron/wallet/account.json \ + context/java-tron/nodeId.properties \ + context/java-tron/output-directory/block.data \ + context/java-tron/bin/helper \ + context/java-tron/lib/nested/hidden.jar \ + context/java-tron/README.txt \ + context/source.txt; do + if grep -Fqx -- "$rejected" "$MANIFEST"; then + echo "Sensitive or unrelated file entered the Docker context: $rejected" >&2 + exit 1 + fi +done + +echo "Root Docker context filtering tests passed" diff --git a/docker/tests/unix-start-script-test.sh b/docker/tests/unix-start-script-test.sh new file mode 100755 index 00000000000..65cc80c44e9 --- /dev/null +++ b/docker/tests/unix-start-script-test.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Render gradle/unixStartScript.txt the way CreateStartScripts does and check +# that FullNode arguments containing spaces reach Java as a single argv entry. +set -euo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +REPOSITORY_ROOT=$(cd -- "$TEST_DIR/../.." >/dev/null 2>&1 && pwd) +TEMPLATE="$REPOSITORY_ROOT/gradle/unixStartScript.txt" +TEST_TMP=$(mktemp -d) +trap 'rm -rf "$TEST_TMP"' EXIT + +# The template escapes $ for Groovy; look for that exact CreateStartScripts form. +# shellcheck disable=SC2016 +if ! grep -Fq 'APP_ARGS=`save "\${array[@]}"`' "$TEMPLATE"; then + echo "unixStartScript.txt must quote array elements when calling save:" >&2 + grep -n 'APP_ARGS=' "$TEMPLATE" >&2 || true + exit 1 +fi + +python3 - "$TEMPLATE" "$TEST_TMP/FullNode" <<'PY' +import pathlib +import re +import sys + +template = pathlib.Path(sys.argv[1]).read_text() +template = template.replace("${applicationName}", "FullNode") +template = template.replace("${appHomeRelativePath}", "..") +template = template.replace("${defaultJvmOpts}", '""') +template = template.replace("${optsEnvironmentVar}", "FULLNODE_OPTS") +template = template.replace("${mainClassName}", "org.tron.program.FullNode") +template = re.sub( + r"<% if \( appNameSystemProperty \) \{ %>.*?<% \} %>", + "", + template, + flags=re.S, +) + +out = [] +i = 0 +while i < len(template): + if template[i] == "\\" and i + 1 < len(template): + nxt = template[i + 1] + if nxt in {"$", "\\"}: + out.append(nxt) + i += 2 + continue + out.append(template[i]) + i += 1 + +pathlib.Path(sys.argv[2]).write_text("".join(out)) +PY + +DIST="$TEST_TMP/java-tron" +JAVA_HOME="$TEST_TMP/java-home" +ARGV_LOG="$TEST_TMP/java-argv" +mkdir -p "$DIST/bin" "$DIST/lib" "$JAVA_HOME/bin" +mv "$TEST_TMP/FullNode" "$DIST/bin/FullNode" +# shellcheck disable=SC2016 +if ! grep -Fq 'APP_ARGS=`save "${array[@]}"`' "$DIST/bin/FullNode"; then + echo "Rendered FullNode script does not quote array elements:" >&2 + grep -n 'APP_ARGS=' "$DIST/bin/FullNode" >&2 || true + exit 1 +fi +chmod +x "$DIST/bin/FullNode" +printf '%s\n' "# fixture" > "$DIST/bin/java-tron.vmoptions" +touch "$DIST/lib/java-tron.jar" + +cat > "$JAVA_HOME/bin/java" <<'MOCK_JAVA' +#!/bin/bash +set -euo pipefail +: > "$JAVA_ARGV_LOG" +for argument in "$@"; do + printf '%s\0' "$argument" >> "$JAVA_ARGV_LOG" +done +MOCK_JAVA +chmod +x "$JAVA_HOME/bin/java" + +run_fullnode() { + : > "$ARGV_LOG" + JAVA_HOME="$JAVA_HOME" JAVA_ARGV_LOG="$ARGV_LOG" \ + "$DIST/bin/FullNode" "$@" +} + +assert_java_args_after_main() { + python3 - "$ARGV_LOG" "$@" <<'PY' +import pathlib +import sys + +payload = pathlib.Path(sys.argv[1]).read_bytes() +args = payload.split(b"\0") +if args and args[-1] == b"": + args = args[:-1] +decoded = [item.decode() for item in args] +try: + main_index = decoded.index("org.tron.program.FullNode") +except ValueError: + raise SystemExit("Java argv did not include the FullNode main class:\n" + "\n".join(decoded)) +actual = decoded[main_index + 1 :] +expected = sys.argv[2:] +if actual != expected: + raise SystemExit( + "Java argv after the main class did not match.\nExpected:\n " + + "\n ".join(expected) + + "\nActual:\n " + + "\n ".join(actual) + ) +PY +} + +run_fullnode \ + --p2p-disable true \ + --log-config "/java-tron/log configs/logback.xml" +assert_java_args_after_main \ + --p2p-disable true \ + --log-config "/java-tron/log configs/logback.xml" + +run_fullnode \ + -c /java-tron/config.conf \ + -jvm "{-Xms256m}" \ + --log-config "/java-tron/log configs/logback.xml" +assert_java_args_after_main \ + -c /java-tron/config.conf \ + --log-config "/java-tron/log configs/logback.xml" + +echo "unix start script argument quoting tests passed" diff --git a/docker/tests/verify-runtime-image.sh b/docker/tests/verify-runtime-image.sh new file mode 100755 index 00000000000..771f505c71e --- /dev/null +++ b/docker/tests/verify-runtime-image.sh @@ -0,0 +1,186 @@ +#!/bin/bash +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 IMAGE JAVA_VERSION_REGEX" >&2 + exit 1 +fi + +image=$1 +java_version_regex=$2 +test_dir=$(cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd) +repository_root=$(cd -- "$test_dir/../.." >/dev/null 2>&1 && pwd) + +test "$(docker image inspect --format '{{.Config.User}}' "$image")" = "10001:10001" + +docker run --rm --entrypoint sh "$image" -ec ' + set -eu + + test "$(id -u)" = 10001 + test "$(id -g)" = 10001 + test -x /java-tron/bin/FullNode + test -f /java-tron/bin/java-tron.vmoptions + test -f /java-tron/config.conf + test ! -L /java-tron/config.conf + test -r /java-tron/config.conf + test -d /java-tron/output-directory + test -d /java-tron/logs + + test "$(stat -c %u:%g /java-tron)" = "0:0" + test "$(stat -c %u:%g /java-tron/bin)" = "0:0" + test "$(stat -c %u:%g /java-tron/bin/FullNode)" = "0:0" + test "$(stat -c %u:%g /java-tron/bin/java-tron.vmoptions)" = "0:0" + test "$(stat -c %u:%g /java-tron/config.conf)" = "0:0" + test "$(stat -c %a /java-tron/config.conf)" = "644" + lib_jar=$(find /java-tron/lib -maxdepth 1 -type f -name "*.jar" -print -quit) + test -n "$lib_jar" + test "$(stat -c %u:%g "$lib_jar")" = "0:0" + test "$(stat -c %u:%g /java-tron/output-directory)" = "10001:10001" + test "$(stat -c %u:%g /java-tron/logs)" = "10001:10001" + test "$(stat -c %a /java-tron/output-directory)" = "700" + test "$(stat -c %a /java-tron/logs)" = "700" + test "$(sed -n "2p" /java-tron/bin/FullNode)" = "umask 077" + + test ! -w /java-tron + test ! -w /java-tron/bin + test ! -w /java-tron/bin/FullNode + test ! -w /java-tron/bin/java-tron.vmoptions + test ! -w /java-tron/config.conf + test ! -w "$lib_jar" + test -w /java-tron/output-directory + test -w /java-tron/logs + ! touch /java-tron/.write-test + + grep -Eq -- "-Xloggc:/java-tron/logs/gc.log|:file=/java-tron/logs/gc.log:" \ + /java-tron/bin/java-tron.vmoptions + test "$(grep -Fxc -- "-XX:+HeapDumpOnOutOfMemoryError" \ + /java-tron/bin/java-tron.vmoptions)" -eq 1 + ! grep -Fqx -- "-XX:-HeapDumpOnOutOfMemoryError" \ + /java-tron/bin/java-tron.vmoptions + grep -Fq -- "-XX:HeapDumpPath=/java-tron/logs" /java-tron/bin/java-tron.vmoptions + grep -Fq -- "-XX:ErrorFile=/java-tron/logs/hs_err_pid%p.log" \ + /java-tron/bin/java-tron.vmoptions + ! grep -Eq -- "-Xloggc:./gc.log|:file=gc.log:" /java-tron/bin/java-tron.vmoptions +' + +runtime_dir=$(mktemp -d "$repository_root/.verify-runtime-image.XXXXXX") + +remove_runtime_owned_directory() { + local directory="$1" + + [ -e "$directory" ] || return 0 + if rm -rf -- "$directory" 2>/dev/null && [ ! -e "$directory" ]; then + return 0 + fi + if [ ! -d "$directory" ]; then + echo "Runtime-test path is not a directory: $directory" >&2 + return 1 + fi + + # Delete files in the same user namespace and as the same container UID that + # created them. This avoids applying a host UID as though it were a container + # UID, which produces the wrong owner under rootless/userns-remap daemons. + if ! docker run --rm \ + --user 10001:10001 \ + --network none \ + --read-only \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --entrypoint find \ + --mount "type=bind,src=$directory,dst=/cleanup" \ + "$image" /cleanup -mindepth 1 -depth -delete; then + echo "Failed to empty container-owned test directory: $directory" >&2 + return 1 + fi + if ! rmdir -- "$directory"; then + echo "Failed to remove empty test directory: $directory" >&2 + return 1 + fi +} + +cleanup() { + local test_status=$? + local cleanup_status=0 + + trap - EXIT + set +e + + if [ -d "$runtime_dir" ]; then + if ! remove_runtime_owned_directory "$runtime_dir/output-directory"; then + cleanup_status=1 + fi + if ! remove_runtime_owned_directory "$runtime_dir/logs"; then + cleanup_status=1 + fi + if ! rm -rf -- "$runtime_dir"; then + echo "Failed to remove runtime-test directory: $runtime_dir" >&2 + cleanup_status=1 + fi + fi + + if [ "$test_status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then + test_status=$cleanup_status + fi + exit "$test_status" +} +trap cleanup EXIT + +mock_java_home="$runtime_dir/mock-java-home" +mkdir -p "$runtime_dir/output-directory" "$runtime_dir/logs" \ + "$mock_java_home/bin" +chmod 0700 "$runtime_dir/output-directory" "$runtime_dir/logs" +printf '%s\n' \ + '#!/bin/sh' \ + 'set -eu' \ + 'touch /java-tron/logs/.umask-file' \ + 'mkdir /java-tron/output-directory/.umask-directory' \ + > "$mock_java_home/bin/java" +chmod 0755 "$mock_java_home/bin/java" +docker run --rm \ + --user 0:0 \ + --network none \ + --read-only \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --cap-add CHOWN \ + --cap-add DAC_READ_SEARCH \ + --entrypoint chown \ + -v "$runtime_dir/output-directory:/java-tron/output-directory" \ + -v "$runtime_dir/logs:/java-tron/logs" \ + "$image" 10001:10001 /java-tron/output-directory /java-tron/logs +docker run --rm \ + --network none \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --env JAVA_HOME=/mock-java-home \ + -v "$mock_java_home:/mock-java-home:ro" \ + -v "$runtime_dir/output-directory:/java-tron/output-directory" \ + -v "$runtime_dir/logs:/java-tron/logs" \ + "$image" +docker run --rm \ + --user 10001:10001 \ + --network none \ + --read-only \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --entrypoint sh \ + -v "$runtime_dir/output-directory:/java-tron/output-directory:ro" \ + -v "$runtime_dir/logs:/java-tron/logs:ro" \ + "$image" -ec ' + test "$(stat -c %a /java-tron/logs/.umask-file)" = 600 + test "$(stat -c %a /java-tron/output-directory/.umask-directory)" = 700 + ' +docker run --rm --security-opt no-new-privileges --entrypoint sh \ + -v "$runtime_dir/output-directory:/java-tron/output-directory" \ + -v "$runtime_dir/logs:/java-tron/logs" \ + "$image" -ec ' + grep -Eq "^NoNewPrivs:[[:space:]]+1$" /proc/self/status + touch /java-tron/output-directory/.write-test + touch /java-tron/logs/.write-test + ' +test -f "$runtime_dir/output-directory/.write-test" +test -f "$runtime_dir/logs/.write-test" + +version=$(docker run --rm --entrypoint java "$image" -version 2>&1) +echo "$version" +grep -Eq "$java_version_regex" <<< "$version" diff --git a/docker/tests/vmoptions-test.sh b/docker/tests/vmoptions-test.sh new file mode 100755 index 00000000000..ffce45e4003 --- /dev/null +++ b/docker/tests/vmoptions-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 IMAGE" >&2 + exit 1 +fi + +image="$1" +fixture_dir=$(mktemp -d) +trap 'rm -rf "$fixture_dir"' EXIT +vm_options_file="$fixture_dir/java-tron.vmoptions" + +# Cover comments, blank lines, CRLF endings, and a quoted value containing +# spaces. Append the final option without a trailing newline. Mount the +# fixture over the image vmoptions so the test does not need a writable +# application directory. +printf "%s\r\n" \ + "# This comment must not be passed to the JVM." \ + "" \ + "-Djava.tron.vmoptions.spaced=\"value with spaces\"" \ + > "$vm_options_file" +printf "%s" \ + "-Djava.tron.vmoptions.final=\"last line without newline\"" \ + >> "$vm_options_file" + +if ! output=$(docker run --rm \ + --entrypoint bash \ + -v "$vm_options_file:/java-tron/bin/java-tron.vmoptions:ro" \ + "$image" \ + -c 'JAVA_OPTS="-XshowSettings:properties -version" exec /java-tron/bin/FullNode' \ + 2>&1); then + echo "$output" >&2 + echo "FullNode failed while parsing the JVM options fixture." >&2 + exit 1 +fi + +assert_output() { + local expected="$1" + + if ! grep -Fq -- "$expected" <<< "$output"; then + echo "Missing expected JVM property: $expected" >&2 + echo "$output" >&2 + exit 1 + fi +} + +assert_output "java.tron.vmoptions.spaced = value with spaces" +assert_output "java.tron.vmoptions.final = last line without newline" + +echo "JVM options parsing test passed for $image" diff --git a/docs/configuration.md b/docs/configuration.md index d021326a15e..449bbca8367 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,14 +151,14 @@ storage.properties = [ ### Block Production (Super Representatives) ```hocon -# Plain private key (use localwitnesskeystore for production) -localwitness = [ - "your-private-key-hex" +# Recommended for production: encrypted keystore file +localwitnesskeystore = [ + "localwitnesskeystore.json" ] -# Recommended: keystore file -# localwitnesskeystore = [ -# "/path/to/localwitnesskeystore.json" +# Plaintext compatibility option for isolated test environments only +# localwitness = [ +# "your-private-key-hex" # ] # Required when the witness account has delegated block-signing to a separate key diff --git a/gradle/java-tron.vmoptions b/gradle/java-tron.vmoptions index cf34689ebdd..f42bebc63b6 100644 --- a/gradle/java-tron.vmoptions +++ b/gradle/java-tron.vmoptions @@ -1,3 +1,6 @@ +# Runtime profile: x86_64 / amd64 with JDK 8 only. +# Keep these architecture-specific GC options when changing heap settings. +# Do not copy GC options from the ARM64 / JDK 17 profile. -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:./gc.log diff --git a/gradle/jdk17/java-tron.vmoptions b/gradle/jdk17/java-tron.vmoptions index 180ff1aa7c7..87a66cd52b1 100644 --- a/gradle/jdk17/java-tron.vmoptions +++ b/gradle/jdk17/java-tron.vmoptions @@ -1,3 +1,6 @@ +# Runtime profile: ARM64 / aarch64 with JDK 17 only. +# Keep these architecture-specific GC options when changing heap settings. +# Do not copy GC options from the x86_64 / JDK 8 profile. -XX:+UseZGC -Xlog:gc,gc+heap:file=gc.log:time,tags,level:filecount=10,filesize=100M -XX:ReservedCodeCacheSize=256m diff --git a/gradle/unixStartScript.txt b/gradle/unixStartScript.txt index 585998cb8c9..05e1cb1ada7 100644 --- a/gradle/unixStartScript.txt +++ b/gradle/unixStartScript.txt @@ -50,13 +50,6 @@ APP_BASE_NAME=`basename "\$0"` # JAVA_OPTS='"-Xmx\$MEM" "-Xms\$MEM"' #fi -# Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script. -DEFAULT_JVM_OPTS=${defaultJvmOpts} -for line in \$(cat \$APP_HOME/bin/java-tron.vmoptions) -do - DEFAULT_JVM_OPTS="\$DEFAULT_JVM_OPTS \$line" -done - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -71,6 +64,25 @@ die () { exit 1 } +# Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script. +DEFAULT_JVM_OPTS=${defaultJvmOpts} +VM_OPTIONS_FILE="\$APP_HOME/bin/java-tron.vmoptions" + +if [[ ! -r "\$VM_OPTIONS_FILE" ]]; then + die "ERROR: JVM options file is missing or unreadable: \$VM_OPTIONS_FILE" +fi + +while IFS= read -r line || [[ -n "\$line" ]]; do + # Support JVM options files with Windows line endings. + line="\${line%\$'\\r'}" + + # Ignore blank lines and comments. + trimmed="\${line#"\${line%%[![:space:]]*}"}" + [[ -z "\$trimmed" || "\${trimmed:0:1}" == "#" ]] && continue + + DEFAULT_JVM_OPTS="\$DEFAULT_JVM_OPTS \$line" +done < "\$VM_OPTIONS_FILE" + # OS specific support (must be 'true' or 'false'). cygwin=false msys=false @@ -215,7 +227,7 @@ save () { for i do printf %s\\\\n "\$i" | sed "s/'/'\\\\\\\\''/g;1s/^/'/;\\\$s/\\\$/' \\\\\\\\/" ; done echo " " } -APP_ARGS=`save \${array[*]}` +APP_ARGS=`save "\${array[@]}"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- \$DEFAULT_JVM_OPTS \$JAVA_OPTS \$${optsEnvironmentVar} <% if ( appNameSystemProperty ) { %>"\"-D${appNameSystemProperty}=\$APP_BASE_NAME\"" <% } %>-classpath "\"\$CLASSPATH\"" ${mainClassName} "\$APP_ARGS" diff --git a/quickstart.md b/quickstart.md index b3eeb7b7713..01e067cb510 100644 --- a/quickstart.md +++ b/quickstart.md @@ -1,223 +1,92 @@ -# How to quick start +# java-tron Quick Start -## Introduction +Choose the workflow that matches your goal. Production node operation, local smart-contract testing, and private-network deployment have different security and resource requirements. -This guide provides two ways for TRON quickstart: -- Set up a FullNode using the official tools: providing a wealth of configurable parameters to startup a FullNode -- Set up a complete private network for Tron development using a third-party tool: [docker-tron-quickstart](https://github.com/TRON-US/docker-tron-quickstart) +| Goal | Recommended entry point | +| --- | --- | +| Build or run java-tron directly | [java-tron README](README.md) | +| Run a single FullNode with the Bash helper | [java-tron Docker shell guide](docker/docker.md) | +| Build a development image from the current checkout | [java-tron Docker shell guide](docker/docker.md#build-an-image) | +| Run a FullNode with Docker Compose | [tron-docker single-node guide](https://github.com/tronprotocol/tron-docker/tree/main/single_node) | +| Build and test release images for amd64 and arm64 | [tron-docker image guide](https://github.com/tronprotocol/tron-docker/tree/main/tools/docker) | +| Create a multi-node private network | [tron-docker private-network guide](https://github.com/tronprotocol/tron-docker/tree/main/private_net) | +| Test smart contracts locally | [TronBox Runtime Environment](https://hub.docker.com/r/tronbox/tre) | -## Dependencies +## Prerequisites -### Docker +Install Docker and the Docker Compose plugin from the official documentation: -Please download and install the latest Docker from Docker official website: -* Docker Installation for [Mac](https://docs.docker.com/docker-for-mac/install/) -* Docker Installation for [Windows](https://docs.docker.com/docker-for-windows/install/) +- [Docker Engine on Linux](https://docs.docker.com/engine/install/) +- [Docker Desktop on macOS](https://docs.docker.com/desktop/setup/install/mac-install/) -## Quickstart based on official tools +A Mainnet FullNode requires at least 8 CPU cores and 16 GB of memory. Stable and production deployments require additional memory, SSD capacity, and network bandwidth. See the [Mainnet hardware requirements](README.md#hardware-requirements-for-mainnet) before deploying a node. -### Build the docker image from source +## Run a FullNode with Docker Compose -#### Clone the java-tron repo +java-tron provides two independently maintained Docker workflows: -Clone the java-tron repo from github and enter the directory `java-tron`: -``` -git clone https://github.com/tronprotocol/java-tron.git -cd java-tron -``` +- Use [`docker/docker.sh`](docker/docker.md) for lightweight image build, pull, and single-node container lifecycle operations. +- Use [`tron-docker`](https://github.com/tronprotocol/tron-docker) for Docker Compose deployments, private networks, and dedicated image build and test tooling. -#### Build the docker image +The workflows have separate commands, configuration, and defaults and are not interchangeable. The example below uses the `tron-docker` single-node Compose workflow. -Use the command below to navigate to the docker directory and start the build: -``` -cd docker -docker build -t tronprotocol/java-tron . -``` +> **CPU architecture:** Use a current checkout of [`tron-docker`](https://github.com/tronprotocol/tron-docker/tree/main/single_node) before starting the Compose example. Older copies of `docker-compose-quick-start.yml` included the JDK 8-only `-XX:+UseConcMarkSweepGC` option; the ARM64/aarch64 image uses JDK 17 and rejects that option. If it is present in your local Compose file, update the checkout or remove the option before starting the node. The current upstream quick-start file no longer includes this legacy collector option. -#### Using the official Docker images +> **Storage and synchronization:** The default Compose file synchronizes a full Mainnet database from genesis. It does not configure a Lite FullNode or preload a data snapshot. Allocate approximately 3.5–4 TB of high-performance SSD storage for this mode. The approximately 200 GB storage tier applies only when using Lite FullNode data. To avoid synchronizing from genesis, configure a compatible [FullNode or Lite FullNode data snapshot](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#data-snapshot) for the selected network and java-tron version before starting. +> +> The default Compose file also does not mount the database on the host, so synchronized data remains in the container's writable layer and can be lost when the container is removed or recreated. Configure a persistent volume for `output-directory` before using this example for a long-running node. -Download the official docker image from the Dockerhub with below command if you'd like to use the official images: -``` -docker pull tronprotocol/java-tron +```shell +git clone https://github.com/tronprotocol/tron-docker.git +cd tron-docker/single_node +docker compose -f docker-compose-quick-start.yml up -d ``` -### Run the container +Check the synchronization log: -You can run the command below to start the java-tron: -``` -docker run -it -d -p 8090:8090 -p 18888:18888 -p 50051:50051 --restart always tronprotocol/java-tron +```shell +docker exec tron-node tail -f ./logs/tron.log ``` -The `-p` flag defines the ports that the container needs to be mapped on the host machine. By default the container will start and join in the mainnet -using the built-in configuration file, you can specify other configuration file by mounting a directory and using the flag `-c`. -This image also supports customizing some startup parameters,here is an example for running a FullNode as an SR in production env: -``` -docker run -it -d -p 8080:8080 -p 8090:8090 -p 18888:18888 -p 50051:50051 \ - -v /Users/quan/tron/docker/conf:/java-tron/conf \ - -v /Users/quan/tron/docker/datadir:/java-tron/data \ - tronprotocol/java-tron \ - -jvm "{-Xmx10g -Xms10g}" \ - -c /java-tron/conf/config-localtest.conf \ - -d /java-tron/data \ - -w +Check the HTTP API: + +```shell +curl --request POST http://127.0.0.1:8090/wallet/getnowblock ``` -Note: The directory `/Users/tron/docker/conf` must contain the file `config-localtest.conf`. The jvm parameters must be enclosed in double quotes and braces. -## Quickstart for using docker-tron-quickstart +The quick-start Compose file is intended for evaluation. Before operating a long-running or production node: -The image exposes a Full Node and Event Server. Through TRON Quickstart, users can deploy DApps, smart contracts, and interact with the TronWeb library. +- Pin a released image tag instead of relying on `latest`. +- Persist the configuration, logs, and `output-directory` on the host. +- Set the container and JVM memory limits for the selected deployment tier. +- Publish both TCP and UDP for the P2P port. +- Restrict HTTP and gRPC access with host bindings, firewall rules, or a trusted proxy. +- Use a compatible configuration file and data snapshot for the selected network and java-tron version. -> Note: `docker-tron-quickstart` is a community-maintained tool. Check its repository for the latest status: [Quickstart](https://github.com/TRON-US/docker-tron-quickstart) +For production deployment, snapshot synchronization, JVM tuning, and upgrade procedures, follow the [java-tron deployment guide](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/). -### Node.JS Console - Node.JS is used to interact with the Full and Solidity Nodes via Tron-Web. - [Node.JS](https://nodejs.org/en/) Console Download - -### Clone TRON Quickstart -```shell -git clone https://github.com/TRON-US/docker-tron-quickstart.git -``` +## Build java-tron from source -### Pull the image using docker: -```shell -docker pull trontools/quickstart -``` +Source builds require `unzip` and the JDK matching the CPU architecture: JDK 8 on x86_64/amd64 and JDK 17 on ARM64/aarch64. Follow [Building the Source Code](README.md#building-the-source-code) to build the current checkout. -## Setup TRON Quickstart -### TRON Quickstart Run -Run the "docker run" command to launch TRON Quickstart. TRON Quickstart exposes port 9090 for Full Node and Event Server. -```shell -docker run -it \ - -p 9090:9090 \ - --rm \ - --name tron \ - trontools/quickstart -``` -Notice: the option --rm automatically removes the container after it exits. This is very important because the container cannot be restarted, it MUST be run from scratch to correctly configure the environment. - -### Testing - -If everything goes well, your terminal console output will look like following : -
- -Run Console Output - - - [PM2] Spawning PM2 daemon with pm2_home=/root/.pm2 - [PM2] PM2 Successfully daemonized - [PM2][WARN] Applications eventron not running, starting... - [PM2] App [eventron] launched (1 instances) - ┌──────────┬────┬─────────┬──────┬─────┬────────┬─────────┬────────┬─────┬───────────┬──────┬──────────┐ - │ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │ - ├──────────┼────┼─────────┼──────┼─────┼────────┼─────────┼────────┼─────┼───────────┼──────┼──────────┤ - │ eventron │ 0 │ N/A │ fork │ 60 │ online │ 0 │ 0s │ 0% │ 25.4 MB │ root │ disabled │ - └──────────┴────┴─────────┴──────┴─────┴────────┴─────────┴────────┴─────┴───────────┴──────┴──────────┘ - Use `pm2 show ` to get more details about an app - Start the http proxy for dApps... - [HPM] Proxy created: / -> http://127.0.0.1:18191 - [HPM] Proxy created: / -> http://127.0.0.1:18190 - [HPM] Proxy created: / -> http://127.0.0.1:8060 - - Tron Quickstart listening on http://127.0.0.1:9090 - - - - ADMIN /admin/accounts-generation - Sleeping for 1 second...Waiting when nodes are ready to generate 10 accounts... - (1) Waiting for sync... - Slept. - ... - Loading the accounts and waiting for the node to mine the transactions... - (1) Waiting for receipts... - Sending 10000 TRX to TSjfWSWcKCrJ1DbgMZSCbSqNK8DsEfqM9p - Sending 10000 TRX to THpWnj3dBQ5FrqW1KMVXXYSbHPtcBKeUJY - Sending 10000 TRX to TWFTHaKdeHWi3oPoaBokyZFfA7q1iiiAAb - Sending 10000 TRX to TFDGQo6f6dm9ikoV4Rc9NyTxMD5NNiSFJD - Sending 10000 TRX to TDZZNigWitFp5aE6j2j8YcycF7DVjtogBu - Sending 10000 TRX to TT8NRMcwdS9P3X9pvPC8JWi3x2zjwxZuhs - Sending 10000 TRX to TBBJw6Bk7w2NSZeqmzfUPnsn6CwDJAXTv8 - Sending 10000 TRX to TVcgSLpT97mvoiyv5ChyhQ6hWbjYLWdCVB - Sending 10000 TRX to TYjQd4xrLZQGYMdLJqsTCuXVGapPqUp9ZX - Sending 10000 TRX to THCw6hPZpFcLCWDcsZg3W77rXZ9rJQPncD - Sleeping for 3 seconds... Slept. - (2) Waiting for receipts... - Sleeping for 3 seconds... Slept. - (3) Waiting for receipts... - Sleeping for 3 seconds... Slept. - (4) Waiting for receipts... - Sleeping for 3 seconds... Slept. - (5) Waiting for receipts... - Sleeping for 3 seconds... Slept. - (6) Waiting for receipts... - Sleeping for 3 seconds... Slept. - (7) Waiting for receipts... - Done. - - Available Accounts - ================== - - (0) TSjfWSWcKCrJ1DbgMZSCbSqNK8DsEfqM9p (10000 TRX) - (1) THpWnj3dBQ5FrqW1KMVXXYSbHPtcBKeUJY (10000 TRX) - (2) TWFTHaKdeHWi3oPoaBokyZFfA7q1iiiAAb (10000 TRX) - (3) TFDGQo6f6dm9ikoV4Rc9NyTxMD5NNiSFJD (10000 TRX) - (4) TDZZNigWitFp5aE6j2j8YcycF7DVjtogBu (10000 TRX) - (5) TT8NRMcwdS9P3X9pvPC8JWi3x2zjwxZuhs (10000 TRX) - (6) TBBJw6Bk7w2NSZeqmzfUPnsn6CwDJAXTv8 (10000 TRX) - (7) TVcgSLpT97mvoiyv5ChyhQ6hWbjYLWdCVB (10000 TRX) - (8) TYjQd4xrLZQGYMdLJqsTCuXVGapPqUp9ZX (10000 TRX) - (9) THCw6hPZpFcLCWDcsZg3W77rXZ9rJQPncD (10000 TRX) - -
- - -### web browser ### -1. open your web browser -2. enter : http://127.0.0.1:9090/ -3. there will be a response JSON data: +To build a development image from the current working tree, run `bash docker/docker.sh --build --source local` from the repository root. This builds the distribution on the host and sends only a temporary distribution-only context to Docker. The legacy `--build` command without source options continues to build the remote `master` branch. See the [Docker shell guide](docker/docker.md#build-an-image) for source-selection options. -``` - {"Welcome to":"TronGrid v2.2.8"} -``` +For release-oriented amd64 and arm64 image build and test tooling, use the Gradle Docker tooling in `tron-docker`. -## Docker Commands -Here are some useful docker commands, which will help you manage the TRON Quickstart Docker container on your machine. +## Run a Super Representative node -**To list all active containers on your machine, run:** -```shell -docker container ps -``` -**Output:** -```shell -docker container ps +Do not use a shortened Quick Start command for a production Super Representative node. An SR requires additional hardware, JVM tuning, key protection, monitoring, backup, and upgrade planning. -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -513078dc7816 tron "./quickstart v2.0.0" About an hour ago Up About an hour 0.0.0.0:9090->9090/tcp, 0.0.0.0:18190->18190/tcp tron -``` -**To kill an active container, run:** -```shell -docker container kill 513078dc7816 // use your container ID -``` +Follow the [Starting a Block Production Node](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#starting-a-block-production-node) guide. Use an encrypted keystore and the production deployment's secret-management mechanism. Do not pass `--private-key` or `--password`: command arguments may be visible in process listings and, with Docker, are retained in container metadata. -### How to check the logs of the FullNode ### -``` - docker exec -it tron tail -f /tron/FullNode/logs/tron.log +## Create a private development network + +For a multi-node private TRON network, use the [official private-network guide](https://tronprotocol.github.io/documentation-en/using_javatron/private_network/) or the [tron-docker private-network example](https://github.com/tronprotocol/tron-docker/tree/main/private_net). + +For local smart-contract development and automated tests, run the TronBox Runtime Environment: + +```shell +docker run --rm --name tron -it -p 127.0.0.1:9090:9090 tronbox/tre:dev ``` -
- -Output: something like following - - ``` - number=204 - parentId=00000000000000cb0985978b3c780e4219dc51e4329beecabe7b71f99d269985 - witness address=41928c9af0651632157ef27a2cf17ca72c575a4d21 - generated by myself=true - generate time=2019-12-09 18:33:33.0 - txs are empty - ] - 18:33:33.008 INFO [Thread-5] [DB](Manager.java:1095) pushBlock block number:204, cost/txs:1/0 - 18:33:33.008 INFO [Thread-5] [witness](WitnessService.java:283) Produce block successfully, blockNumber:204, abSlot[525305471], blockId:00000000000000ccc37f1f5c2ceb574d14c490e3d0b86909855646f9384ba666, transactionSize:0, blockTime:2019-12-09T18:33:33.000Z, parentBlockId:00000000000000cb0985978b3c780e4219dc51e4329beecabe7b71f99d269985 - 18:33:33.008 INFO [Thread-5] [net](AdvService.java:156) Ready to broadcast block Num:204,ID:00000000000000ccc37f1f5c2ceb574d14c490e3d0b86909855646f9384ba666 - ........ etc - ``` -
+TRE includes funded test accounts and privileged development APIs. Use it only for local development or isolated CI, and never expose it to an untrusted network or use it with production keys or funds.