From 7d4eaa11e7139879205c4a46ed7b452a6258f5bf Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Fri, 18 Oct 2024 00:13:19 -0500 Subject: [PATCH 01/12] Run docker image as non-root user --- Docker/appsettings.json | 3 +++ Docker/liberate.sh | 20 +------------------- Dockerfile | 17 +++++++++++------ 3 files changed, 15 insertions(+), 25 deletions(-) create mode 100644 Docker/appsettings.json diff --git a/Docker/appsettings.json b/Docker/appsettings.json new file mode 100644 index 00000000..e581ac93 --- /dev/null +++ b/Docker/appsettings.json @@ -0,0 +1,3 @@ +{ + "LibationFiles": "/config" +} diff --git a/Docker/liberate.sh b/Docker/liberate.sh index 83249b87..036d629d 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -19,18 +19,6 @@ fi echo "" -# Check if the config directory is passed in, and there is no link to it then create the link. -if [ -d "/config" ] && [ ! -d "/root/Libation" ]; then - echo "Linking config directory to the Libation config directory" - ln -s /config/ /root/Libation -fi - -# If no config error and exit -if [ ! -d "/config" ]; then - echo "ERROR: No /config directory. You must pass in a volume containing your config mapped to /config" - exit 1 -fi - # If user passes in db from a /db/ folder and a db does not already exist / is not already linked FILE=/db/LibationContext.db if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then @@ -38,12 +26,6 @@ if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then ln -s $FILE /config/LibationContext.db fi -# Confirm we have a db in the config direcotry. -if [ ! -f "/config/LibationContext.db" ]; then - echo "ERROR: No Libation database detected, exiting." - exit 1 -fi - # ################################ # Loop and liberate # ################################ @@ -65,4 +47,4 @@ do sleep "${SLEEP_TIME}" done -echo "Exiting" \ No newline at end of file +echo "Exiting" diff --git a/Dockerfile b/Dockerfile index ef274842..c976b3c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile -FROM mcr.microsoft.com/dotnet/sdk:8.0 as build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env COPY Source /Source RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml @@ -7,16 +7,21 @@ COPY Docker/liberate.sh /Source/bin/Publish/Linux-chardonnay FROM mcr.microsoft.com/dotnet/runtime:8.0 +ARG USER_UID=1001 -ENV SLEEP_TIME "30m" -# Sets the character set that will be used for folder and filenames when liberating -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 +ENV SLEEP_TIME=30m -RUN mkdir /db /config /data +# Set the character set that will be used for folder and filenames when liberating +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +RUN apt-get update && apt-get -y upgrade && \ + mkdir /db /config /data COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation +COPY Docker/appsettings.json /libation/ +USER ${USER_UID} CMD ["./libation/liberate.sh"] From 9bc53e45cdc2f2aab45bf59b63b26ec8af3b4dbb Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Fri, 18 Oct 2024 00:13:42 -0500 Subject: [PATCH 02/12] large overhaul of docker run script --- Docker/appsettings.json | 2 +- Docker/liberate.sh | 144 ++++++++++++++++++++++++++++++---------- Dockerfile | 21 +++--- 3 files changed, 122 insertions(+), 45 deletions(-) diff --git a/Docker/appsettings.json b/Docker/appsettings.json index e581ac93..1a5525b3 100644 --- a/Docker/appsettings.json +++ b/Docker/appsettings.json @@ -1,3 +1,3 @@ { - "LibationFiles": "/config" + "LibationFiles": "/config-internal" } diff --git a/Docker/liberate.sh b/Docker/liberate.sh index 036d629d..9991b1eb 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -1,50 +1,124 @@ #!/bin/bash -# Rewire echo to print date time -echo() { - if [[ -n $1 ]]; then - printf "$(date '+%F %T'): %s\n" "$1" - fi +error() { + log "ERROR" "$1" } -# ################################ -# Setup -# ################################ -echo "Starting" -if [[ -z "${SLEEP_TIME}" ]]; then - echo "No sleep time passed in. Will run once and exit." -else - echo "Sleep time is set to ${SLEEP_TIME}" -fi +warn() { + log "WARNING" "$1" +} -echo "" +info() { + log "info" "$1" +} -# If user passes in db from a /db/ folder and a db does not already exist / is not already linked -FILE=/db/LibationContext.db -if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then - echo "Linking passed in Libation database from /db/ to the Libation config directory" - ln -s $FILE /config/LibationContext.db -fi +debug() { + log "debug" "$1" +} -# ################################ -# Loop and liberate -# ################################ -while true -do - echo "" - echo "Scanning accounts" - /libation/LibationCli scan - echo "Liberating books" - /libation/LibationCli liberate - echo "" +log() { + LEVEL=$1 + MESSAGE=$2 + printf "$(date '+%F %T') %s: %s\n" "${LEVEL}" "${MESSAGE}" +} + +init_config_file() { + FILE=$1 + FULLPATH=${LIBATION_CONFIG_DIR}/${FILE} + if [ -f ${FULLPATH} ]; then + info "loading ${FILE}" + cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/ + return 0 + else + warn "${FULLPATH} not found, using defaults" + echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE} + return 1 + fi +} + +update_settings() { + FILE=$1 + KEY=$2 + VALUE=$3 + info "setting ${KEY} to ${VALUE}" + echo $(jq --arg k "${KEY}" --arg v "${VALUE}" '.[$k] = $v' ${LIBATION_CONFIG_INTERNAL}/${FILE}) > ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp + mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE} +} + +run() { + info "scanning accounts" + /libation/LibationCli scan + info "liberating books" + /libation/LibationCli liberate +} + +main() { + # TERM isn't set by default in docker images + if [[ -z ${TERM} || ${TERM} = "dumb" ]]; then + TERM=xterm-256color + fi + + info ${TERM} + + info "initializing libation" + init_config_file AccountsSettings.json + init_config_file Settings.json + + info "loading settings" + update_settings Settings.json Books /data + update_settings Settings.json InProgress /tmp + + # If user provides a separate database use that + info "loading database" + FILE=LibationContext.db + if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then + info "database found in ${LIBATION_DB_DIR}" + ln -s /${LIBATION_DB_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + # If user provides database in config use that + elif [ -f "${LIBATION_CONFIG_DIR}/${FILE}" ]; then + info "database found in ${LIBATION_CONFIG_DIR}" + ln -s /${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + # We didn't get a database + else + warn "no database found, creating one in ${LIBATION_CONFIG_DIR}" + if ! touch ${LIBATION_CONFIG_DIR}/${FILE}; then + error "unable to create database, check permissions on host" + exit 1 + fi + ln -s ${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + fi + + # Try to warn if books dir wasn't mounted in + if [ -z "$(mount | grep ${LIBATION_BOOKS_DIR})" ] + then + warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved" + fi + + # Let the user know what the run type will be + if [[ -z "${SLEEP_TIME}" ]]; then + SLEEP_TIME=-1 + fi + + if [ "${SLEEP_TIME}" = -1 ]; then + info "running once" + else + info "running every ${SLEEP_TIME}" + fi + + # loop + while true + do + run # Liberate only once if SLEEP_TIME was set to -1 if [ "${SLEEP_TIME}" = -1 ]; then break fi - echo "Sleeping for ${SLEEP_TIME}" sleep "${SLEEP_TIME}" -done + done -echo "Exiting" + info "exiting" +} + +main diff --git a/Dockerfile b/Dockerfile index c976b3c2..36eddace 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,25 +3,28 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env COPY Source /Source RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml -COPY Docker/liberate.sh /Source/bin/Publish/Linux-chardonnay - FROM mcr.microsoft.com/dotnet/runtime:8.0 ARG USER_UID=1001 - - -ENV SLEEP_TIME=30m +ARG USER_GID=1001 # Set the character set that will be used for folder and filenames when liberating ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 +ENV SLEEP_TIME=-1 +ENV LIBATION_CONFIG_INTERNAL=/config-internal +ENV LIBATION_CONFIG_DIR=/config +ENV LIBATION_DB_DIR=/db +ENV LIBATION_BOOKS_DIR=/data + RUN apt-get update && apt-get -y upgrade && \ - mkdir /db /config /data + apt-get install -y jq && \ + mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR} COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation -COPY Docker/appsettings.json /libation/ +COPY Docker/* /libation -USER ${USER_UID} +USER ${USER_UID}:${USER_GID} -CMD ["./libation/liberate.sh"] +CMD ["/libation/liberate.sh"] From ade693bebb42e29ee7a8ce42bd0a9c213f7097a3 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Sat, 19 Oct 2024 01:54:37 -0500 Subject: [PATCH 03/12] Update docker readme --- Documentation/Docker.md | 55 +++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/Documentation/Docker.md b/Documentation/Docker.md index 9d75b6dd..afda7e09 100644 --- a/Documentation/Docker.md +++ b/Documentation/Docker.md @@ -3,38 +3,30 @@ ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us) ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. - +> [!WARNING] +> ## Breaking Changes +> * The docker image now runs as user 1001 and group 1001. Make sure that the permissions on your volumes allow user 1001 to read and write to them. +> * `SLEEP_TIME` is now set to `-1` by default. This means the image will run once and exit. If you were relying on the previous default, you'll need to explicitly set the `SLEEP_TIME` environment variable to `30m` to replicate the previous behavior. +> * The docker image now ignores the values in `Settings.json` for `Books` and `InProgress`. You can now change the folder that books are saved to by using the `LIBATION_BOOKS_DIR` environment variable. # Disclaimer The docker image is provided as-is. We hope it can be useful to you but it is not officially supported. -### Setup -In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image. - -In Settings.json, make the following changes: -* Change `Books` to `/data` -* Change `InProgress` to `/tmp` * - -*You may have to paste the following at the end of your your Settings.json file if `InProgess` is not present: - -``` - "InProgress": "/tmp" -``` -![image](https://github.com/patienttruth/Libation/assets/105557996/cf65a108-cadf-4284-9000-e7672c99c59b) - +### Configuration +Configuration in Libation is handled by two files, `AccountsSettings.json` and `Settings.json`. These files can usually be found in the Libation folder in your user's home directory. The easiest way to configure these is to run the desktop version of Libation and then copy them into a folder, such as `/opt/libation/config`, that you'll volume mount into the image. `Settings.json` is technically optional, and, if not provided, Libation will run using the default settings. Additionally, the `Books` and `InProgress` settings in `Settings.json` will be ignored and the image will instead substitute it's own values. ### Running -Once the configuration files are copied and edited, the docker image can be run with the following command. +Once the configuration files are copied, the docker image can be run with the following command. ``` sudo docker run -d \ -v /opt/libation/config:/config \ -v /opt/libation/books:/data \ --name libation \ --restart=always \ - rmcrackan/libation + rmcrackan/libation:latest ``` -By default the container will scan for new books every 30 minutes and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. Additionally, if you pass in `-1` it will scan and download books once and then exit. +By default the container will scan for new books once and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. For example, if you pass in `10m` it will keep running, scan for new books, and download them every 10 minutes. ``` sudo docker run -d \ @@ -43,6 +35,31 @@ sudo docker run -d \ -e SLEEP_TIME='10m' \ --name libation \ --restart=always \ - rmcrackan/libation + rmcrackan/libation:latest ``` +### Environment Variables +| Env Var | Default | Description | +| -------- | ------- | ----------- | +| SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. | +| LIBATION_BOOKS_DIR | /data | Folder where books will be saved | +| LIBATION_CONFIG_DIR | /config | Folder to read configuration from. | +| LIBATION_DB_DIR | /db | Optional folder to load database from. If not present, will load database from `LIBATION_CONFIG_DIR`. | + +### User +This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors + +If you want to change the user the image runs as, you can specify `-u :`. For example, to run it as user `2000` and group `3000`, you could do the following: + +``` +sudo docker run -d \ + -u 2000:3000 \ + -v /opt/libation/config:/config \ + -v /opt/libation/books:/data \ + --name libation \ + --restart=always \ + rmcrackan/libation:latest +``` + +### Advanced Database Options +The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. The image will attempt to load the database first from `LIBATION_DB_DIR`, then from `LIBATION_CONFIG_DIR`, and finally it will try to create a new database in `LIBATION_CONFIG_DIR`. \ No newline at end of file From ac8c090c4c63fd5f1643b92a6a91a91aa7c283ac Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Mon, 21 Oct 2024 14:02:52 -0500 Subject: [PATCH 04/12] Rework run script to support db mount better --- Docker/liberate.sh | 59 +++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/Docker/liberate.sh b/Docker/liberate.sh index 9991b1eb..40ef534a 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -13,7 +13,9 @@ info() { } debug() { - log "debug" "$1" + if [ "${LOG_LEVEL}" = "debug" ]; then + log "debug" "$1" + fi } log() { @@ -45,6 +47,26 @@ update_settings() { mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE} } +is_mounted() { + DIR=$1 + return $(mount | grep ${DIR}) +} + +create_db() { + FILE=$1 + if [ -f "${FILE}" ]; then + warn "prexisting database found when creating" + return 0 + else + warn "database not found, creating one at ${FILE}" + if ! touch "${FILE}"; then + error "unable to create database, check permissions on host" + exit 1 + fi + return 1 + fi +} + run() { info "scanning accounts" /libation/LibationCli scan @@ -68,28 +90,31 @@ main() { update_settings Settings.json Books /data update_settings Settings.json InProgress /tmp - # If user provides a separate database use that info "loading database" FILE=LibationContext.db - if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then - info "database found in ${LIBATION_DB_DIR}" - ln -s /${LIBATION_DB_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} - # If user provides database in config use that - elif [ -f "${LIBATION_CONFIG_DIR}/${FILE}" ]; then - info "database found in ${LIBATION_CONFIG_DIR}" - ln -s /${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} - # We didn't get a database - else - warn "no database found, creating one in ${LIBATION_CONFIG_DIR}" - if ! touch ${LIBATION_CONFIG_DIR}/${FILE}; then - error "unable to create database, check permissions on host" - exit 1 + # If user provides a separate database mount, use that + if [ is_mounted ${LIBATION_DB_DIR} ]; + then + debug using database directory `${LIBATION_DB_DIR}` + if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then + info "database found in ${LIBATION_DB_DIR}" + else + create_db ${LIBATION_DB_DIR}/${FILE} fi - ln -s ${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + ln -s /${LIBATION_DB_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + # Otherwise, use the config directory + else + debug using config directory `${LIBATION_CONFIG_DIR}` + if [ -f "${LIBATION_CONFIG_DIR}/${FILE}" ]; then + info "database found in ${LIBATION_CONFIG_DIR}" + else + create_db ${LIBATION_CONFIG_DIR}/${FILE} + fi + ln -s /${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} fi # Try to warn if books dir wasn't mounted in - if [ -z "$(mount | grep ${LIBATION_BOOKS_DIR})" ] + if [ ! is_mounted ${LIBATION_BOOKS_DIR} ]; then warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved" fi From 074fe79ded1c57f8cad22b0db0d1c74067e70434 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Mon, 21 Oct 2024 16:08:19 -0500 Subject: [PATCH 05/12] Update docker workflow to try building on validate --- .github/workflows/docker.yml | 51 ++++++++++++++++++++++++++++------ .github/workflows/release.yml | 1 + .github/workflows/validate.yml | 8 ++++++ Dockerfile | 8 +++++- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7ea0fc0b..22d6d0a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,22 +10,39 @@ on: type: string description: 'Version number' required: true + release: + type: boolean + description: 'Is this a release build?' + required: true secrets: docker_username: required: true docker_token: required: true -env: - DOCKER_IMAGE: ${{ secrets.docker_username }}/libation - jobs: - docker: + configure: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 + build_and_push: + runs-on: ubuntu-latest + needs: configure + strategy: + # Prevent a failure in one image from stopping the other builds + fail-fast: false + matrix: + os: + - ubuntu-latest + arch: + - amd64 + - arm64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -38,9 +55,25 @@ jobs: username: ${{ secrets.docker_username }} password: ${{ secrets.docker_token }} - - name: Build and push - uses: docker/build-push-action@v5 + - name: Generate docker image tags + id: metadata + uses: docker/metadata-action@v5 with: - push: true - build-args: 'FOLDER_NAME=Linux-chardonnay' - tags: ${{ env.DOCKER_IMAGE }}:latest,${{ env.DOCKER_IMAGE }}:${{ inputs.version }} + flavor: | + latest=true + images: | + name=${{ secrets.docker_username }}/libation + tags: | + type=raw,value=${{ inputs.version }},enable=${{ inputs.release }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + platforms: ${{ matrix.arch }} + push: ${{ steps.metadata.outputs.tags != ''}} + cache-from: type=gha + cache-to: type=gha,mode=max + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + build-args: | + TARGET_ARCH=${{ matrix.arch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd1cc731..be07bd8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: uses: ./.github/workflows/docker.yml with: version: ${{ needs.prerelease.outputs.version }} + release: true secrets: docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0ae4c712..d8b302ec 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -12,3 +12,11 @@ on: jobs: build: uses: ./.github/workflows/build.yml + docker: + uses: ./.github/workflows/docker.yml + with: + version: ${GITHUB_SHA} + release: false + secrets: + docker_username: ${{ secrets.DOCKERHUB_USERNAME }} + docker_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 36eddace..36386ec1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,14 @@ # Dockerfile FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env +ARG TARGET_ARCH=linux-x64 COPY Source /Source -RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml +RUN dotnet publish \ + /Source/LibationCli/LibationCli.csproj \ + --runtime linux-${TARGET_ARCH} \ + --configuration Release \ + --output /Source/bin/Publish/Linux-chardonnay \ + -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml FROM mcr.microsoft.com/dotnet/runtime:8.0 ARG USER_UID=1001 From 2bdcc221f53ac127ca378bd0826170ad2b2c5bab Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Tue, 22 Oct 2024 00:03:06 -0500 Subject: [PATCH 06/12] Specify platform(?) --- .github/workflows/docker.yml | 2 +- Dockerfile | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 22d6d0a4..86dc2e59 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -76,4 +76,4 @@ jobs: tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} build-args: | - TARGET_ARCH=${{ matrix.arch }} + TARGETARCH=${{ matrix.arch }} diff --git a/Dockerfile b/Dockerfile index 8333acc6..bf33ef79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ # Dockerfile -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env -ARG TARGET_ARCH=amd64 +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETARCH=amd64 COPY Source /Source RUN dotnet publish \ /Source/LibationCli/LibationCli.csproj \ - --runtime linux-${TARGET_ARCH} \ + --arch ${TARGETARCH} \ --configuration Release \ --output /Source/bin/Publish/Linux-chardonnay \ -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml @@ -28,7 +28,7 @@ RUN apt-get update && apt-get -y upgrade && \ apt-get install -y jq && \ mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR} -COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation +COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation COPY Docker/* /libation USER ${USER_UID}:${USER_GID} From 011efe3676c0ef20ea6b1545da18050e60531cca Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Tue, 22 Oct 2024 00:36:29 -0500 Subject: [PATCH 07/12] remove unused configure step --- .github/workflows/docker.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 86dc2e59..d5ebf516 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,15 +21,8 @@ on: required: true jobs: - configure: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - build_and_push: runs-on: ubuntu-latest - needs: configure strategy: # Prevent a failure in one image from stopping the other builds fail-fast: false From 9825e2b5521ce226076d68f42e1e81e3e48bfe1e Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Tue, 22 Oct 2024 03:45:44 -0500 Subject: [PATCH 08/12] Build both platforms in one action --- .github/workflows/docker.yml | 15 +++------------ Docker/liberate.sh | 23 ++++++++++++++--------- Dockerfile | 4 ++-- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d5ebf516..dd41c9f1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,15 +23,7 @@ on: jobs: build_and_push: runs-on: ubuntu-latest - strategy: - # Prevent a failure in one image from stopping the other builds - fail-fast: false - matrix: - os: - - ubuntu-latest - arch: - - amd64 - - arm64 + steps: - name: Checkout uses: actions/checkout@v4 @@ -43,6 +35,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub + if: ${{ inputs.release }} uses: docker/login-action@v3 with: username: ${{ secrets.docker_username }} @@ -62,11 +55,9 @@ jobs: - name: Build and push image uses: docker/build-push-action@v6 with: - platforms: ${{ matrix.arch }} + platforms: linux/amd64,linux/arm64 push: ${{ steps.metadata.outputs.tags != ''}} cache-from: type=gha cache-to: type=gha,mode=max tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - build-args: | - TARGETARCH=${{ matrix.arch }} diff --git a/Docker/liberate.sh b/Docker/liberate.sh index 40ef534a..ba71bad9 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -49,17 +49,22 @@ update_settings() { is_mounted() { DIR=$1 - return $(mount | grep ${DIR}) + if mount | grep -q "${DIR}"; + then + return 0 + else + return 1 + fi } create_db() { - FILE=$1 - if [ -f "${FILE}" ]; then + DBFILE=$1 + if [ -f "${DBFILE}" ]; then warn "prexisting database found when creating" return 0 else - warn "database not found, creating one at ${FILE}" - if ! touch "${FILE}"; then + warn "database not found, creating one at ${DBFILE}" + if ! touch "${DBFILE}"; then error "unable to create database, check permissions on host" exit 1 fi @@ -93,9 +98,9 @@ main() { info "loading database" FILE=LibationContext.db # If user provides a separate database mount, use that - if [ is_mounted ${LIBATION_DB_DIR} ]; + if is_mounted ${LIBATION_DB_DIR}; then - debug using database directory `${LIBATION_DB_DIR}` + debug "using database directory ${LIBATION_DB_DIR}" if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then info "database found in ${LIBATION_DB_DIR}" else @@ -104,7 +109,7 @@ main() { ln -s /${LIBATION_DB_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} # Otherwise, use the config directory else - debug using config directory `${LIBATION_CONFIG_DIR}` + debug "using config directory ${LIBATION_CONFIG_DIR}" if [ -f "${LIBATION_CONFIG_DIR}/${FILE}" ]; then info "database found in ${LIBATION_CONFIG_DIR}" else @@ -114,7 +119,7 @@ main() { fi # Try to warn if books dir wasn't mounted in - if [ ! is_mounted ${LIBATION_BOOKS_DIR} ]; + if ! is_mounted ${LIBATION_BOOKS_DIR}; then warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved" fi diff --git a/Dockerfile b/Dockerfile index bf33ef79..88909c4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Dockerfile -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG TARGETARCH=amd64 +FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETARCH COPY Source /Source RUN dotnet publish \ From 9ed6c1fd0d39f5480f0b804f1378c8807d86ef42 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Tue, 22 Oct 2024 09:54:15 -0500 Subject: [PATCH 09/12] cleanup --- Docker/liberate.sh | 13 +++---------- Documentation/Docker.md | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/Docker/liberate.sh b/Docker/liberate.sh index ba71bad9..decf16c4 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -49,7 +49,7 @@ update_settings() { is_mounted() { DIR=$1 - if mount | grep -q "${DIR}"; + if grep -qs "${DIR} " /proc/mounts; then return 0 else @@ -80,13 +80,6 @@ run() { } main() { - # TERM isn't set by default in docker images - if [[ -z ${TERM} || ${TERM} = "dumb" ]]; then - TERM=xterm-256color - fi - - info ${TERM} - info "initializing libation" init_config_file AccountsSettings.json init_config_file Settings.json @@ -98,7 +91,7 @@ main() { info "loading database" FILE=LibationContext.db # If user provides a separate database mount, use that - if is_mounted ${LIBATION_DB_DIR}; + if is_mounted "${LIBATION_DB_DIR}"; then debug "using database directory ${LIBATION_DB_DIR}" if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then @@ -119,7 +112,7 @@ main() { fi # Try to warn if books dir wasn't mounted in - if ! is_mounted ${LIBATION_BOOKS_DIR}; + if ! is_mounted "${LIBATION_BOOKS_DIR}"; then warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved" fi diff --git a/Documentation/Docker.md b/Documentation/Docker.md index afda7e09..2b82b00d 100644 --- a/Documentation/Docker.md +++ b/Documentation/Docker.md @@ -44,7 +44,7 @@ sudo docker run -d \ | SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. | | LIBATION_BOOKS_DIR | /data | Folder where books will be saved | | LIBATION_CONFIG_DIR | /config | Folder to read configuration from. | -| LIBATION_DB_DIR | /db | Optional folder to load database from. If not present, will load database from `LIBATION_CONFIG_DIR`. | +| LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. | ### User This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors @@ -62,4 +62,4 @@ sudo docker run -d \ ``` ### Advanced Database Options -The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. The image will attempt to load the database first from `LIBATION_DB_DIR`, then from `LIBATION_CONFIG_DIR`, and finally it will try to create a new database in `LIBATION_CONFIG_DIR`. \ No newline at end of file +The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there. \ No newline at end of file From e0dd9b845a6705a93377b844081bc972e15fba50 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Thu, 14 Nov 2024 00:38:04 -0600 Subject: [PATCH 10/12] rework database handling --- Docker/liberate.sh | 65 ++++++++++++++++++++++++++++++++-------------- Dockerfile | 3 +++ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Docker/liberate.sh b/Docker/liberate.sh index decf16c4..73cd8966 100755 --- a/Docker/liberate.sh +++ b/Docker/liberate.sh @@ -32,7 +32,7 @@ init_config_file() { cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/ return 0 else - warn "${FULLPATH} not found, using defaults" + warn "${FULLPATH} not found, creating empty file" echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE} return 1 fi @@ -63,7 +63,6 @@ create_db() { warn "prexisting database found when creating" return 0 else - warn "database not found, creating one at ${DBFILE}" if ! touch "${DBFILE}"; then error "unable to create database, check permissions on host" exit 1 @@ -72,6 +71,46 @@ create_db() { fi } +setup_db() { + DBPATH=$1 + dbpattern="*.db" + + debug "using database directory ${DBPATH}" + + # Figure out the right databse file + if [[ -z "${LIBATION_DB_FILE}" ]]; + then + dbCount=$(find "${DBPATH}" -type f -name "${dbpattern}" | wc -l) + if [ "${dbCount}" -gt 1 ]; + then + error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use" + exit 1 + elif [ "${dbCount}" -eq 1 ]; + then + files=( ${DBPATH}/${dbpattern} ) + FILE=${files[0]} + else + FILE="${DBPATH}/LibationContext.db" + fi + else + FILE="${DBPATH}/${LIBATION_DB_FILE}" + fi + + debug "planning to use database ${FILE}" + + if [ -f "${FILE}" ]; then + info "database found at ${FILE}" + elif [ ${LIBATION_CREATE_DB} = "true" ]; + then + warn "database not found, creating one at ${FILE}" + create_db ${FILE} + else + error "database not found and creation is disabled" + exit 1 + fi + ln -s "${FILE}" "${LIBATION_CONFIG_INTERNAL}/LibationContext.db" +} + run() { info "scanning accounts" /libation/LibationCli scan @@ -89,27 +128,15 @@ main() { update_settings Settings.json InProgress /tmp info "loading database" - FILE=LibationContext.db # If user provides a separate database mount, use that if is_mounted "${LIBATION_DB_DIR}"; then - debug "using database directory ${LIBATION_DB_DIR}" - if [ -f "${LIBATION_DB_DIR}/${FILE}" ]; then - info "database found in ${LIBATION_DB_DIR}" - else - create_db ${LIBATION_DB_DIR}/${FILE} - fi - ln -s /${LIBATION_DB_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + DB_LOCATION=${LIBATION_DB_DIR} # Otherwise, use the config directory else - debug "using config directory ${LIBATION_CONFIG_DIR}" - if [ -f "${LIBATION_CONFIG_DIR}/${FILE}" ]; then - info "database found in ${LIBATION_CONFIG_DIR}" - else - create_db ${LIBATION_CONFIG_DIR}/${FILE} - fi - ln -s /${LIBATION_CONFIG_DIR}/${FILE} ${LIBATION_CONFIG_INTERNAL}/${FILE} + DB_LOCATION=${LIBATION_CONFIG_DIR} fi + setup_db ${DB_LOCATION} # Try to warn if books dir wasn't mounted in if ! is_mounted "${LIBATION_BOOKS_DIR}"; @@ -122,7 +149,7 @@ main() { SLEEP_TIME=-1 fi - if [ "${SLEEP_TIME}" = -1 ]; then + if [ "${SLEEP_TIME}" -eq -1 ]; then info "running once" else info "running every ${SLEEP_TIME}" @@ -134,7 +161,7 @@ main() { run # Liberate only once if SLEEP_TIME was set to -1 - if [ "${SLEEP_TIME}" = -1 ]; then + if [ "${SLEEP_TIME}" -eq -1 ]; then break fi diff --git a/Dockerfile b/Dockerfile index 88909c4e..7e9eb9be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,11 @@ ENV SLEEP_TIME=-1 ENV LIBATION_CONFIG_INTERNAL=/config-internal ENV LIBATION_CONFIG_DIR=/config ENV LIBATION_DB_DIR=/db +ENV LIBATION_DB_FILE= +ENV LIBATION_CREATE_DB=true ENV LIBATION_BOOKS_DIR=/data + RUN apt-get update && apt-get -y upgrade && \ apt-get install -y jq && \ mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR} From 97b792868f460d4d7315b2b88a66a00f0cd2c46f Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Thu, 14 Nov 2024 10:54:35 -0600 Subject: [PATCH 11/12] Update documentation with new envvars --- Documentation/Docker.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/Docker.md b/Documentation/Docker.md index 2b82b00d..09ca1cdb 100644 --- a/Documentation/Docker.md +++ b/Documentation/Docker.md @@ -45,6 +45,8 @@ sudo docker run -d \ | LIBATION_BOOKS_DIR | /data | Folder where books will be saved | | LIBATION_CONFIG_DIR | /config | Folder to read configuration from. | | LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. | +| LIBATION_DB_FILE | | Name of database file to load. By default it will look for all `.db` files and load one if there is only one present. | +| LIBATION_CREATE_DB | true | Whether or not the image should create a database file if none are found. | ### User This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors From cd7040cdc7ff90de6f39407b3e40ecb50efd2f81 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Thu, 14 Nov 2024 11:15:57 -0600 Subject: [PATCH 12/12] pretty up the workflows --- .github/workflows/build-linux.yml | 30 ++++++++++++++--------------- .github/workflows/build-windows.yml | 12 ++++++------ .github/workflows/build.yml | 13 ++++++------- .github/workflows/docker.yml | 4 ++-- .github/workflows/release.yml | 12 ++++++------ .github/workflows/validate.yml | 2 +- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 38df80b7..d7895998 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -8,37 +8,37 @@ on: inputs: version_override: type: string - description: 'Version number override' + description: "Version number override" required: false run_unit_tests: type: boolean - description: 'Skip running unit tests' + description: "Skip running unit tests" required: false default: true runs_on: type: string - description: 'The GitHub hosted runner to use' + description: "The GitHub hosted runner to use" required: true OS: type: string description: > The operating system targeted by the build. - + There must be a corresponding Bundle_$OS.sh script file in ./Scripts required: true architecture: type: string - description: 'CPU architecture targeted by the build.' + description: "CPU architecture targeted by the build." required: true env: - DOTNET_CONFIGURATION: 'Release' - DOTNET_VERSION: '8.0.x' - RELEASE_NAME: 'chardonnay' + DOTNET_CONFIGURATION: "Release" + DOTNET_VERSION: "8.0.x" + RELEASE_NAME: "chardonnay" jobs: build: - name: '${{ inputs.OS }}-${{ inputs.architecture }}' + name: "${{ inputs.OS }}-${{ inputs.architecture }}" runs-on: ${{ inputs.runs_on }} steps: - uses: actions/checkout@v4 @@ -60,7 +60,7 @@ jobs: version="$(grep -Eio -m 1 '.*' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')" fi echo "version=${version}" >> "${GITHUB_OUTPUT}" - + - name: Unit test if: ${{ inputs.run_unit_tests }} working-directory: ./Source @@ -69,7 +69,7 @@ jobs: - name: Publish id: publish working-directory: ./Source - run: | + run: | if [[ "${{ inputs.OS }}" == "MacOS" ]] then display_os="macOS" @@ -78,13 +78,13 @@ jobs: display_os="Linux" RUNTIME_ID="linux-${{ inputs.architecture }}" fi - + OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}" - + echo "display_os=${display_os}" >> $GITHUB_OUTPUT echo "Runtime Identifier: $RUNTIME_ID" echo "Output Directory: $OUTPUT" - + dotnet publish \ LibationAvalonia/LibationAvalonia.csproj \ --runtime $RUNTIME_ID \ @@ -122,7 +122,7 @@ jobs: ${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}" artifact=$(ls ./bundle) echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}" - + - name: Publish bundle uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 7b4c1ddf..cff369b0 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -8,21 +8,21 @@ on: inputs: version_override: type: string - description: 'Version number override' + description: "Version number override" required: false run_unit_tests: type: boolean - description: 'Skip running unit tests' + description: "Skip running unit tests" required: false default: true env: - DOTNET_CONFIGURATION: 'Release' - DOTNET_VERSION: '8.0.x' + DOTNET_CONFIGURATION: "Release" + DOTNET_VERSION: "8.0.x" jobs: build: - name: '${{ matrix.os }}-${{ matrix.release_name }}' + name: "${{ matrix.os }}-${{ matrix.release_name }}" runs-on: windows-latest strategy: matrix: @@ -112,4 +112,4 @@ jobs: name: ${{ steps.zip.outputs.artifact }}.zip path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip if-no-files-found: error - retention-days: 7 + retention-days: 7 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce1186ac..a67098ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,22 +8,21 @@ on: inputs: version_override: type: string - description: 'Version number override' + description: "Version number override" required: false run_unit_tests: type: boolean - description: 'Skip running unit tests' + description: "Skip running unit tests" required: false - default: true + default: true jobs: - windows: uses: ./.github/workflows/build-windows.yml with: version_override: ${{ inputs.version_override }} run_unit_tests: ${{ inputs.run_unit_tests }} - + linux: strategy: matrix: @@ -36,7 +35,7 @@ jobs: OS: ${{ matrix.OS }} architecture: ${{ matrix.architecture }} run_unit_tests: ${{ inputs.run_unit_tests }} - + macos: strategy: matrix: @@ -47,4 +46,4 @@ jobs: runs_on: macos-latest OS: MacOS architecture: ${{ matrix.architecture }} - run_unit_tests: ${{ inputs.run_unit_tests }} + run_unit_tests: ${{ inputs.run_unit_tests }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dd41c9f1..d1a14479 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,11 +8,11 @@ on: inputs: version: type: string - description: 'Version number' + description: "Version number" required: true release: type: boolean - description: 'Is this a release build?' + description: "Is this a release build?" required: true secrets: docker_username: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be07bd8e..7c6b136a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ name: release on: push: tags: - - 'v*' + - "v*" jobs: prerelease: runs-on: ubuntu-latest @@ -15,7 +15,7 @@ jobs: - name: Get tag version id: get_version run: | - export TAG='${{ github.ref_name }}' + export TAG="${{ github.ref_name }}" echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}" docker: @@ -34,9 +34,9 @@ jobs: with: version_override: ${{ needs.prerelease.outputs.version }} run_unit_tests: false - + release: - needs: [prerelease,build] + needs: [prerelease, build] runs-on: ubuntu-latest steps: - name: Download artifacts @@ -56,7 +56,7 @@ jobs: - name: Upload release assets uses: dwenegar/upload-release-assets@v2 env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" with: - release_id: '${{ steps.release.outputs.id }}' + release_id: "${{ steps.release.outputs.id }}" assets_path: ./artifacts diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d8b302ec..27abc275 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,5 +1,5 @@ # validate.yml -# Validates that Libation will build on a pull request or push to master. +# Validates that Libation will build on a pull request or push to master. --- name: validate