From af22b3bbb79715131d74a9f3e5f24647cf54839f Mon Sep 17 00:00:00 2001 From: Michael <12646562+mbround18@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:22:50 -0700 Subject: [PATCH] fix/fix config and docker (#15) * Fixed config.py and dockerifle * Fixed configurations --- .dockerignore | 11 +- Dockerfile | 223 ++++++++++++++++--------------- Makefile | 4 + README.md | 1 + docker-compose.yml | 3 +- enshrouded.iml | 2 +- scripts/config.py | 153 ++++++++++++++------- scripts/entrypoint.sh | 5 +- scripts/templates/config.json.j2 | 38 ++++-- scripts/utils.sh | 7 +- 10 files changed, 278 insertions(+), 169 deletions(-) create mode 100644 Makefile diff --git a/.dockerignore b/.dockerignore index 42b7e94..b22e40d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,10 @@ -tmp/ -docs/ + + .idea/ +.github +README.md +Makefile +docker-compose* +enshrouded.iml +/tmp +/docs diff --git a/Dockerfile b/Dockerfile index 8f3112e..283a061 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,109 +1,114 @@ -# --------------- # -# -- Steam CMD -- # -# --------------- # -FROM steamcmd/steamcmd:ubuntu - -ARG WINEARCH=win64 -ARG WINE_MONO_VERSION=4.9.4 - -ENV TZ=America/Los_Angeles -ENV PYTHONUNBUFFERED=1 -ENV DISPLAY=:0 -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y -qq \ - build-essential \ - htop net-tools nano gcc g++ gdb \ - netcat curl wget zip unzip \ - cron sudo gosu dos2unix jq \ - tzdata python3 python3-pip \ -# lib32z1 lib32gcc-s1 lib32stdc++6 \ - && rm -rf /var/lib/apt/lists/* \ - && gosu nobody true \ - && dos2unix - -RUN addgroup --system steam \ - && adduser --system \ - --home /home/steam \ - --shell /bin/bash \ - steam \ - && usermod -aG steam steam \ - && chmod ugo+rw /tmp/dumps - -# Install wget -RUN apt-get update -RUN apt-get install -y wget - -# Add 32-bit architecture -RUN dpkg --add-architecture i386 -RUN apt-get update - -# Install Wine -RUN apt-get install -y software-properties-common gnupg2 -RUN wget -nc https://dl.winehq.org/wine-builds/winehq.key -RUN apt-key add winehq.key -RUN apt-add-repository 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' -RUN apt-get install -y --install-recommends winehq-stable winbind -ENV WINEDEBUG=fixme-all - - -# Install Winetricks -RUN apt-get install -y cabextract -ADD --chmod=755 https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks /usr/local/bin/winetricks - -# Install Xvfb -RUN apt-get install -y xvfb - -# Container informaiton -ARG GITHUB_SHA="not-set" -ARG GITHUB_REF="not-set" -ARG GITHUB_REPOSITORY="not-set" - -ENV PUID=1000 -ENV PGID=1000 - -RUN usermod -u ${PUID} steam \ - && groupmod -g ${PGID} steam \ - && echo "steam ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers - -USER steam - -WORKDIR /home/steam - -ENV HOME=/home/steam -ENV USER=steam -ENV LD_LIBRARY_PATH="/home/steam/.steam/sdk32:${LD_LIBRARY_PATH}" -ENV LD_LIBRARY_PATH="/home/steam/.steam/sdk64:${LD_LIBRARY_PATH}" -ENV PATH="/home/steam/.local/bin:${PATH}" - - -# Setup a Wine prefix -ENV WINEPREFIX=/home/steam/.wine -ENV WINEARCH=${WINEARCH} -RUN winecfg - -# Install Mono -ADD https://dl.winehq.org/wine/wine-mono/${WINE_MONO_VERSION}/wine-mono-${WINE_MONO_VERSION}.msi /mono/wine-mono-${WINE_MONO_VERSION}.msi -RUN wineboot -u && sudo msiexec /i /mono/wine-mono-${WINE_MONO_VERSION}.msi \ - && sudo rm -rf /mono/wine-mono-${WINE_MONO_VERSION}.msi - -COPY --chown=${PUID}:${PGID} ./Pipfile ./Pipfile.lock /home/steam/scripts/ - -RUN pip3 install pipenv \ - && cd /home/steam/scripts \ - && pipenv install --system --deploy --ignore-pipfile \ - && pip3 uninstall -y pipenv \ - && sudo chown -R steam:steam /home/steam - -COPY --chown=${PUID}:${PGID} ./scripts /home/steam/scripts - -EXPOSE 15636 15637 - -RUN echo "source /home/steam/scripts/utils.sh" >> /home/steam/.bashrc - -#HEALTHCHECK --interval=1m --timeout=3s \ -# CMD pidof valheim_server.x86_64 || exit 1 - -ENTRYPOINT ["/bin/bash","/home/steam/scripts/entrypoint.sh"] +# Stage 1: Base setup +FROM ubuntu:24.04 AS base +ARG DEBIAN_FRONTEND=noninteractive +ENV USER=root HOME=/root + +# Set up steam +RUN /bin/bash -o pipefail -c ' \ + echo steam steam/question select "I AGREE" | debconf-set-selections && \ + echo steam steam/license note "" | debconf-set-selections && \ + dpkg --add-architecture i386 && \ + apt-get update -y && \ + apt-get install -y --no-install-recommends ca-certificates locales steamcmd && \ + rm -rf /var/lib/apt/lists/* && \ + locale-gen en_US.UTF-8 && \ + ln -s /usr/games/steamcmd /usr/bin/steamcmd && \ + steamcmd +quit && \ + mkdir -p $HOME/.steam && \ + ln -s $HOME/.local/share/Steam/steamcmd/linux32 $HOME/.steam/sdk32 && \ + ln -s $HOME/.local/share/Steam/steamcmd/linux64 $HOME/.steam/sdk64 && \ + ln -s $HOME/.steam/sdk32/steamclient.so $HOME/.steam/sdk32/steamservice.so && \ + ln -s $HOME/.steam/sdk64/steamclient.so $HOME/.steam/sdk64/steamservice.so' + +ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en + +# Stage 2: Wine setup +FROM base AS wine +ARG WINEARCH=win64 +ARG WINE_MONO_VERSION=4.9.4 +ENV TZ=America/Los_Angeles +ENV PYTHONUNBUFFERED=1 DISPLAY=:0 PUID=1000 PGID=1000 + +# Install dependencies +RUN /bin/bash -o pipefail -c ' \ + ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ + echo $TZ > /etc/timezone && \ + if [ "$(getent passwd $PUID | cut -d: -f1)" != "steam" ]; then userdel $(getent passwd $PUID | cut -d: -f1); fi && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y -qq build-essential htop net-tools nano gcc g++ gdb netcat-traditional curl wget zip unzip cron sudo gosu dos2unix jq tzdata && \ + rm -rf /var/lib/apt/lists/* && \ + gosu nobody true && \ + dos2unix && \ + addgroup --system steam && \ + adduser --system --home /home/steam --shell /bin/bash steam && \ + usermod -aG steam steam && \ + chmod ugo+rw /tmp/dumps' + +# Wine installation +ADD https://dl.winehq.org/wine-builds/winehq.key /tmp/winehq.key +RUN /bin/bash -o pipefail -c ' \ + dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install -y software-properties-common gnupg2 && \ + apt-key add /tmp/winehq.key && \ + apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main" && \ + apt-get install -y --install-recommends winehq-stable winbind cabextract && \ + rm -rf /var/lib/apt/lists/*' + +ENV WINEDEBUG=fixme-all + +# Add winetricks +ADD --chmod=755 https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks /usr/local/bin/winetricks + +# Stage 3: Python setup +FROM wine AS python +ARG WINEARCH=win64 +ARG WINE_MONO_VERSION=4.9.4 + +# Install Python, pip, and pyinstaller in a virtual environment +RUN /bin/bash -o pipefail -c ' \ + apt-get update && \ + apt-get install -y python3 python3-venv && \ + python3 -m venv /opt/venv && \ + . /opt/venv/bin/activate && \ + pip install --upgrade pip && \ + pip install pyinstaller jinja2 && \ + deactivate && \ + rm -rf /var/lib/apt/lists/*' + +WORKDIR /app + +COPY . /app +RUN . /opt/venv/bin/activate && pyinstaller --onefile scripts/config.py + +# Stage 4: Final stage +FROM python AS final +ARG GITHUB_SHA=not-set +ARG GITHUB_REF=not-set +ARG GITHUB_REPOSITORY=not-set +ENV ENSHROUDED_CONFIG_DIR=/usr/local/share/enshrouded-config +ENV CONFIG_TEMPLATE_PATH=/home/steam/scripts/templates/config.json.j2 + +COPY --from=python --chmod=steam /app/dist/ "${ENSHROUDED_CONFIG_DIR}" + + +# Copy entrypoint script with correct permissions +COPY --chmod=0755 --chown=steam:steam scripts/ /home/steam/scripts/ +COPY --chmod=0755 --chown=steam:steam ./scripts/templates/config.json.j2 $CONFIG_TEMPLATE_PATH + +RUN /bin/bash -o pipefail -c ' \ + usermod -u ${PUID} steam && \ + groupmod -g ${PGID} steam && \ + echo "steam ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ + mkdir -p "${ENSHROUDED_CONFIG_DIR}"' + +# Switch to steam user +USER steam +WORKDIR /home/steam +ENV HOME=/home/steam USER=steam +ENV LD_LIBRARY_PATH=/home/steam/.steam/sdk32:/home/steam/.steam/sdk64:/home/steam/.steam/sdk32 +ENV PATH=/home/steam/.local/bin:/usr/local/share/enshrouded-config:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +# Set entrypoint +ENTRYPOINT ["/home/steam/scripts/entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc89cc5 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: dev + +dev: + @docker compose up --build --abort-on-container-exit \ No newline at end of file diff --git a/README.md b/README.md index 67fb430..0e7e3ca 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ services: environment: SERVER_NAME: "My Enshrouded Server" # Optional, Name of the server # PASSWORD: "" # Optional, Password for the server + # ADMIN_PASSWORD: "adminpassword" # Optional, Admin password for the server # SAVE_DIRECTORY: ./savegame # Optional, Save directory for the game # LOG_DIRECTORY: ./logs # Optional, Log directory for the server # SERVER_IP: 0.0.0.0 # Optional, IP address for the server diff --git a/docker-compose.yml b/docker-compose.yml index 80d9c7b..0a52d2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,8 @@ services: - linux/amd64 environment: SERVER_NAME: "My Enshrouded Server" # Optional, Name of the server -# PASSWORD: "" # Optional, Password for the server + PASSWORD: "securepassword" # Optional, Password for the server +# ADMIN_PASSWORD: "adminpassword" # Optional, Admin password for the server # SAVE_DIRECTORY: ./savegame # Optional, Save directory for the game # LOG_DIRECTORY: ./logs # Optional, Log directory for the server # SERVER_IP: 0.0.0.0 # Optional, IP address for the server diff --git a/enshrouded.iml b/enshrouded.iml index 91f6230..72f77db 100644 --- a/enshrouded.iml +++ b/enshrouded.iml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/scripts/config.py b/scripts/config.py index e2a8c3a..cdaafb8 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -1,82 +1,145 @@ import os +import logging +import json from argparse import ArgumentParser from jinja2 import Template +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + def get_args(): - # Initialize the parser - parser = ArgumentParser() - # Add the parameters positional/optional + parser = ArgumentParser(description="Generate and merge configuration files.") parser.add_argument("-o", "--output", help="Output destination of the config.ini") - parser.add_argument( - "--server-name", help="Name of the server", default=os.getenv("SERVER_NAME", "Enshrouded Server") + "--server-name", + help="Name of the server", + default=os.getenv("SERVER_NAME", "Enshrouded Server"), ) - parser.add_argument( "--password", help="Password for the server", default=os.getenv("PASSWORD", "") ) - parser.add_argument( - "--save-directory", help="Save directory for the game", default=os.getenv("SAVE_DIRECTORY", "./savegame") + "--admin-password", + help="Admin password for the server", + default=os.getenv("ADMIN_PASSWORD", ""), ) parser.add_argument( - "--log-directory", help="Log directory for the server", default=os.getenv("LOG_DIRECTORY", "./logs") + "--save-directory", + help="Save directory for the game", + default=os.getenv("SAVE_DIRECTORY", "./savegame"), ) - parser.add_argument( - "--server-ip", help="IP address for the server", default=os.getenv("SERVER_IP", "0.0.0.0") + "--log-directory", + help="Log directory for the server", + default=os.getenv("LOG_DIRECTORY", "./logs"), ) - parser.add_argument( - "--game-port", type=int, help="Game port for the server", default=os.getenv("GAME_PORT", 15636) + "--server-ip", + help="IP address for the server", + default=os.getenv("SERVER_IP", "0.0.0.0"), ) - parser.add_argument( - "--query-port", type=int, help="Query port for the server", default=os.getenv("QUERY_PORT", 15637) + "--game-port", + type=int, + help="Game port for the server", + default=os.getenv("GAME_PORT", 15636), ) - parser.add_argument( - "--slot-count", type=int, help="Number of slots for the server", default=os.getenv("SLOT_COUNT", 16) + "--query-port", + type=int, + help="Query port for the server", + default=os.getenv("QUERY_PORT", 15637), + ) + parser.add_argument( + "--slot-count", + type=int, + help="Number of slots for the server", + default=os.getenv("SLOT_COUNT", 16), ) - return parser.parse_args() + args = parser.parse_args() + logging.info( + f"Arguments received (password omitted): {[(k, v) for k, v in vars(args).items() if k != 'password']}" + ) + return args + + +def load_template(template_path): + logging.info(f"Loading template from {template_path}") + with open(template_path, "r") as file: + return Template(file.read()) + + +def render_template(template, args_dict): + logging.info("Rendering template with provided arguments") + return template.render(**args_dict) + + +def merge_configs(base, update): + logging.info("Merging existing and new configurations") + if not isinstance(update, dict): + logging.critical(f"Update is not a dictionary: {update}") + return base + + for key, value in update.items(): + if key == "userGroups" and isinstance(value, list): + if key not in base: + base[key] = value + else: + # Create a map of existing userGroups by name + existing_groups = {group["name"]: group for group in base[key]} + for item in value: + if item["name"] in existing_groups: + existing_group = existing_groups[item["name"]] + existing_group_index = base[key].index(existing_group) + base[key][existing_group_index] = merge_configs( + existing_group, item + ) + else: + base[key].append(item) + elif isinstance(value, dict): + if key not in base: + base[key] = value + else: + base[key] = merge_configs(base.get(key, {}), value) + else: + base[key] = value + return base def main(): - # Parse arguments args = get_args() - - # Create a dictionary of all arguments - args_dict = { - key: value - for key, value in vars(args).items() - if key != "output" and value is not None - } - - # Load and render the template with arguments - # get a path of script + args_dict = {key: value for key, value in vars(args).items() if value is not None} script_path = os.path.dirname(os.path.realpath(__file__)) - # join a script path to templates/config.ini.j2 - template_path = os.path.join(script_path, "templates/config.json.j2") - - template = Template(open(template_path).read()) - rendered = template.render(**args_dict) - - # Output to either stdout or a file + template_path = os.environ.get( + "CONFIG_TEMPLATE_PATH", os.path.join(script_path, "templates/config.ini.j2") + ) + template = load_template(template_path) + rendered_config = render_template(template, args_dict) + rendered_config = json.loads(rendered_config) if args.output: - # get a dir path of output and create folders if not exists - output_dir = os.path.dirname(args.output) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - with open(args.output, "w") as file: - file.write(rendered) + if os.path.exists(args.output): + logging.info(f"Existing configuration found at {args.output}") + if os.stat(args.output).st_size == 0: + existing_content = {} + else: + with open(args.output, "r") as file: + existing_content = json.load(file) + merged_config = merge_configs(existing_content, rendered_config) + with open(args.output, "w") as f: + json.dump(merged_config, f, indent=2) + else: + logging.info( + f"No existing configuration found. Creating new one at {args.output}" + ) + with open(args.output, "w") as f: + json.dump(rendered_config, f, indent=2) else: - print(rendered) + print(rendered_config) if __name__ == "__main__": main() - -# use jinja2 to render the templates/config.ini.j2 file diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 4621f5f..47c1de5 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -2,7 +2,6 @@ if [ ! -f ~/enshrouded ]; then mkdir -p ~/enshrouded && cd ~/enshrouded || exit 1 - fi # Copy Root Steam Files to Steam User @@ -19,4 +18,6 @@ mkdir -p $HOME/.steam \ source /home/steam/scripts/utils.sh -enshrouded_launch \ No newline at end of file +enshrouded_configure + +enshrouded_launch diff --git a/scripts/templates/config.json.j2 b/scripts/templates/config.json.j2 index 83e61e6..fcf1d77 100644 --- a/scripts/templates/config.json.j2 +++ b/scripts/templates/config.json.j2 @@ -1,10 +1,32 @@ { - "name": "{{ server_name | default('My Enshrouded Server') }}", - "password": "{{ password }}", - "saveDirectory": "{{ save_directory | default('./savegame') }}", - "logDirectory": "{{ log_directory | default('./logs') }}", - "ip": "{{ server_ip | default('0.0.0.0') }}", - "gamePort": {{ game_port | default(15636) }}, - "queryPort": {{ query_port | default(15637) }}, - "slotCount": {{ slot_count | default(16) }} + "name": "{{ server_name | default('My Enshrouded Server') }}", + "password": "{{ password }}", + "saveDirectory": "{{ save_directory | default('./savegame') }}", + "logDirectory": "{{ log_directory | default('./logs') }}", + "ip": "{{ server_ip | default('0.0.0.0') }}", + "gamePort": {{ game_port | default(15636) }}, + "queryPort": {{ query_port | default(15637) }}, + "slotCount": {{ slot_count | default(16) }}, + "userGroups": [ + { + "name": "Default", + "password": "{{ password }}", + "canKickBan": false, + "canAccessInventories": true, + "canEditBase": true, + "canExtendBase": true, + "reservedSlots": 0 + {% if admin_password %} + }, + { + "name": "Admin", + "password": "{{ admin_password }}", + "canKickBan": true, + "canAccessInventories": true, + "canEditBase": true, + "canExtendBase": true, + "reservedSlots": 1 + {% endif %} + } + ] } \ No newline at end of file diff --git a/scripts/utils.sh b/scripts/utils.sh index 446b2a0..9144f9c 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -13,7 +13,12 @@ function enshrouded_update() { function enshrouded_configure() { echo "configuring enshrouded" - python3 /home/steam/scripts/config.py --output /home/steam/enshrouded/enshrouded_server.json + if [ ! -d "${ENSHROUDED_CONFIG_DIR}" ]; then + echo -e "\nERROR: ${ENSHROUDED_CONFIG_DIR} does not exist. " + exit 1 + fi + + "${ENSHROUDED_CONFIG_DIR}/config" --output /home/steam/enshrouded/enshrouded_server.json } function enshrouded_launch() {