Initial commit: EDGE Ignition gateway docker-compose build
Custom Dockerfile-based build for an Ignition Edge gateway with pre-baked base gwbk, module registration, and admin password provisioning, backed by MariaDB.
This commit is contained in:
124
CHANGES.md
Normal file
124
CHANGES.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# EDGE docker-compose build/runtime fixes — 2026-07-22
|
||||
|
||||
## Starting problem
|
||||
`docker compose -f EDGE/docker-compose.yml up -d --build` failed. The custom
|
||||
`gw-build/Dockerfile` (patterned after
|
||||
https://github.com/thirdgen88/ignition-examples/tree/main/iiot) referenced four
|
||||
helper scripts that did not exist anywhere in `gw-build/` or on disk:
|
||||
|
||||
- `retrieve-modules.sh`
|
||||
- `register-module.sh`
|
||||
- `register-password.sh`
|
||||
- `docker-entrypoint-shim.sh`
|
||||
|
||||
Build cache masked this initially — earlier `RUN` layers that *used* these
|
||||
scripts showed `CACHED` from a prior successful build, while the `COPY` steps
|
||||
that needed the actual files on disk failed with "not found".
|
||||
|
||||
## What was checked
|
||||
- Searched the whole filesystem (`~/git`, home directory) for the four
|
||||
filenames — not found anywhere, including in the sibling `BAT/` repo (which
|
||||
uses the stock Ignition image directly, no custom Dockerfile).
|
||||
- Checked for any previously-built `edge-ignition` image, container, or volume
|
||||
that might have the files baked in — none existed; this build has never
|
||||
succeeded on this machine.
|
||||
- User confirmed the reference source: the `iiot` example in
|
||||
`thirdgen88/ignition-examples`, which contains all four scripts plus a
|
||||
matching `Dockerfile`.
|
||||
|
||||
## Changes made
|
||||
|
||||
### 1. Copied the four missing scripts from the reference repo
|
||||
Fetched from `thirdgen88/ignition-examples` (`iiot/gw-build/`) into
|
||||
`EDGE/gw-build/`, marked executable (0755):
|
||||
- `retrieve-modules.sh`
|
||||
- `register-module.sh`
|
||||
- `register-password.sh`
|
||||
- `docker-entrypoint-shim.sh` — this one already existed locally, identical to
|
||||
upstream, so no change was needed there.
|
||||
|
||||
Note: a stray `register-modules.sh` (plural) was found already sitting in
|
||||
`gw-build/` — an exact duplicate of `retrieve-modules.sh`'s content under the
|
||||
wrong filename, unreferenced by the Dockerfile. Left in place, not deleted
|
||||
(flagged to user).
|
||||
|
||||
### 2. Dropped `mqttdistributor`/`mqttengine` from the build (user decision)
|
||||
The reference `Dockerfile` defines `ARG` pairs
|
||||
(`SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_URL`/`_SHA256`,
|
||||
`SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_URL`/`_SHA256`) that our local
|
||||
Dockerfile was missing, even though `docker-compose.yml` requested
|
||||
`SUPPLEMENTAL_MODULES: "mqttdistributor mqttengine"`. User said they don't
|
||||
want those modules, so instead of adding the missing ARGs, the module list in
|
||||
`docker-compose.yml` was changed to:
|
||||
```yaml
|
||||
SUPPLEMENTAL_MODULES: ""
|
||||
```
|
||||
|
||||
### 3. Fixed `retrieve-modules.sh` invocation for the empty-module case
|
||||
`retrieve-modules.sh`'s `main()` is written to silently no-op when
|
||||
`SUPPLEMENTAL_MODULES` is empty, but its own `getopts` argument-validation
|
||||
block rejects `-m ""` before `main()` ever runs (`exit 1` with a usage
|
||||
message). This is a bug in the vendored script itself, inherited from the
|
||||
reference repo — not something introduced here.
|
||||
|
||||
Rather than edit the vendored script (to keep it matching upstream), the
|
||||
Dockerfile's `RUN` line was changed to skip calling the script at all when
|
||||
there are no modules to fetch:
|
||||
```dockerfile
|
||||
# before
|
||||
RUN ./retrieve-modules.sh \
|
||||
-m "${SUPPLEMENTAL_MODULES:-}"
|
||||
|
||||
# after
|
||||
RUN if [ -n "${SUPPLEMENTAL_MODULES:-}" ]; then ./retrieve-modules.sh -m "${SUPPLEMENTAL_MODULES}"; fi
|
||||
```
|
||||
|
||||
### 4. Fixed `BASE_GWBK_NAME` mismatch
|
||||
`docker-compose.yml` passed `BASE_GWBK_NAME: gateway.gwbk` as a build arg, but
|
||||
the actual gateway backup file in `gw-build/` is named `base.gwbk` (matching
|
||||
the Dockerfile's own default `ARG BASE_GWBK_NAME="base.gwbk"`). This mismatch
|
||||
didn't hard-fail the build immediately — it caused `unzip` inside the
|
||||
module/password-registration `RUN` step to silently fail-and-fallback
|
||||
("cannot find or open gateway.gwbk" / "skipping password registration"),
|
||||
masking the real problem until the final `COPY --from=prep .../${BASE_GWBK_NAME}`
|
||||
step errored with "not found".
|
||||
|
||||
Fixed by correcting the compose arg to match the real filename:
|
||||
```yaml
|
||||
BASE_GWBK_NAME: base.gwbk
|
||||
```
|
||||
This also means admin password registration now actually runs during the
|
||||
build (previously it was silently skipped due to the filename mismatch).
|
||||
|
||||
### 5. Fixed a runtime crash loop after the image built successfully
|
||||
Once the image built, the `EDGE` container crash-looped with:
|
||||
```
|
||||
ERROR: Gateway Public HTTP/HTTPS/Address must be specified together:
|
||||
- HTTPS Port not specified or is invalid
|
||||
```
|
||||
The compose `command:` for the `ignition` service set `-a
|
||||
gateway.localtest.me` (public address) and `-h 8088` (HTTP port) but no `-s`
|
||||
(HTTPS port). Ignition's entrypoint requires all three (address, HTTP port,
|
||||
HTTPS port) to be set together or none at all — this is just gateway
|
||||
public-address metadata used for URL generation, not a request to actually
|
||||
expose HTTPS. Fixed by adding the conventional Ignition default HTTPS port:
|
||||
```yaml
|
||||
command: >
|
||||
-n FW-DST-SEN
|
||||
-m 512
|
||||
-a gateway.localtest.me
|
||||
-h 8088
|
||||
-s 8043
|
||||
```
|
||||
No compose port mapping was added for 8043 — it isn't published, only used
|
||||
for the internal public-address config.
|
||||
|
||||
## Result
|
||||
`docker compose -f EDGE/docker-compose.yml up -d --build` now completes
|
||||
successfully. Both `EDGE` (Ignition gateway) and `EDGE-db` (MariaDB)
|
||||
containers reach `healthy` status. Gateway is reachable at
|
||||
`http://gateway.localtest.me:8088` (or `http://localhost:8088`).
|
||||
|
||||
## Open item for follow-up
|
||||
- Confirm whether the stray `gw-build/register-modules.sh` (plural, unused
|
||||
duplicate) should be deleted.
|
||||
102
docker-compose.yml
Normal file
102
docker-compose.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
x-default-logging:
|
||||
&default-logging
|
||||
logging:
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '5'
|
||||
driver: json-file
|
||||
|
||||
x-ignition-opts:
|
||||
&ignition-opts
|
||||
<<: *default-logging
|
||||
env_file: gw-init/gateway.env
|
||||
secrets:
|
||||
- gateway-admin-password
|
||||
services:
|
||||
ignition:
|
||||
<<: *ignition-opts
|
||||
build:
|
||||
context: gw-build
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
IGNITION_VERSION: ${IGNITION_VERSION:-latest}
|
||||
SUPPLEMENTAL_MODULES: ""
|
||||
BASE_GWBK_NAME: base.gwbk
|
||||
GATEWAY_ADMIN_USERNAME: admin
|
||||
secrets:
|
||||
# NOTE: changing a build secret will not bust the cache, run the build with `--no-cache` to force a rebuild
|
||||
- gateway-admin-password
|
||||
pull_policy: build
|
||||
container_name: EDGE
|
||||
ports:
|
||||
- "8088:8088"
|
||||
|
||||
environment:
|
||||
ACCEPT_IGNITION_EULA: "Y"
|
||||
GATEWAY_ADMIN_USERNAME: admin
|
||||
GATEWAY_ADMIN_PASSWORD: password
|
||||
IGNITION_EDITION: edge
|
||||
DISABLE_QUICKSTART: "true"
|
||||
# Run the gateway as the HOST dev uid/gid (primebench pattern): started
|
||||
# as root (user 0:0), the official entrypoint chowns the bind mounts to
|
||||
# IGNITION_UID/GID and drops privileges — repo-mapped gateway files stay
|
||||
# owned by you, so host-side edits need no chown dance (WSL2 included).
|
||||
IGNITION_UID: "${IGNITION_UID:-1000}"
|
||||
IGNITION_GID: "${IGNITION_GID:-1000}"
|
||||
TZ: America/Chicago
|
||||
user: "0:0"
|
||||
# Runtime args: gateway name + 500 mb max JVM heap (8.3 uses the -m flag, not an env var)
|
||||
command: >
|
||||
-n FW-DST-SEN
|
||||
-m 512
|
||||
-a gateway.localtest.me
|
||||
-h 8088
|
||||
-s 8043
|
||||
volumes:
|
||||
|
||||
- db-data-EDGE:/usr/local/bin/ignition/data
|
||||
- ./gw-backup/gateway:/backup
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- EDGE
|
||||
|
||||
db:
|
||||
image: mariadb:11.8
|
||||
container_name: EDGE-db
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: password
|
||||
MARIADB_DATABASE: ignition
|
||||
MARIADB_USER: ignition
|
||||
MARIADB_PASSWORD: ignition
|
||||
ports:
|
||||
# localhost only, for inspecting with a SQL client
|
||||
- "127.0.0.1:3306:3306"
|
||||
volumes:
|
||||
- db-data-EDGE:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
start_period: 15s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- EDGE
|
||||
|
||||
secrets:
|
||||
gateway-admin-password:
|
||||
file: secrets/gateway-admin-password
|
||||
db-ignition-password:
|
||||
file: secrets/db-ignition-password
|
||||
db-root-password:
|
||||
file: secrets/db-root-password
|
||||
|
||||
networks:
|
||||
EDGE:
|
||||
|
||||
volumes:
|
||||
db-data-EDGE:
|
||||
74
gw-build/Dockerfile
Normal file
74
gw-build/Dockerfile
Normal file
@@ -0,0 +1,74 @@
|
||||
# Draw from environment (see .env file) for Ignition version/tag to source from
|
||||
ARG IGNITION_VERSION=${IGNITION_VERSION}
|
||||
FROM inductiveautomation/ignition:${IGNITION_VERSION} as prep
|
||||
|
||||
# Switch to root user for base image updates
|
||||
USER root
|
||||
|
||||
# Install some prerequisite packages
|
||||
RUN apt-get update && apt-get install -y wget jq zip unzip sqlite3
|
||||
|
||||
ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/AWS-Injector-signed.modl"
|
||||
ARG SUPPLEMENTAL_AWSINJECTOR_DOWNLOAD_SHA256="5aa6ab27829e6dfa67026358553a3e5e5d1b8ec689e5ffaae3cdaddbc9a3c252"
|
||||
ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/Azure-Injector-signed.modl"
|
||||
ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_SHA256="90cee320d98fa9c1d8c0f3786586c8cd8dd256803e08ed3fcfc772acccdf3dbd"
|
||||
ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/Google-Cloud-Injector-signed.modl"
|
||||
ARG SUPPLEMENTAL_GCPINJECTOR_DOWNLOAD_SHA256="72078b25fb3c6853d85a76c212fb63a8c1277707666b492f67fb91ea333775b8"
|
||||
ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.24/MQTT-Transmission-signed.modl"
|
||||
ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_SHA256="cb5d620513110c23b618989b56c5c2a67c45c36944a25d127586e8e303f0273a"
|
||||
ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Transmission-signed.modl"
|
||||
ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_SHA256="notused"
|
||||
ARG SUPPLEMENTAL_MODULES
|
||||
|
||||
# Set working directory for this prep image and ensure that exits from sub-shells bubble up and report an error
|
||||
WORKDIR /root
|
||||
SHELL [ "/usr/bin/env", "-S", "bash", "-euo", "pipefail", "-O", "inherit_errexit", "-c" ]
|
||||
|
||||
# Retrieve all targeted modules and verify their integrity
|
||||
COPY --chmod=0755 retrieve-modules.sh .
|
||||
RUN if [ -n "${SUPPLEMENTAL_MODULES:-}" ]; then ./retrieve-modules.sh -m "${SUPPLEMENTAL_MODULES}"; fi
|
||||
|
||||
# Set CERTIFICATES/EULAS acceptance in gateway backup config db
|
||||
COPY *.gwbk ./
|
||||
COPY --chmod=0755 register-module.sh register-password.sh ./
|
||||
ARG GATEWAY_ADMIN_USERNAME="admin"
|
||||
ARG BASE_GWBK_NAME="base.gwbk"
|
||||
RUN --mount=type=secret,id=gateway-admin-password \
|
||||
unzip -q "${BASE_GWBK_NAME}" db_backup_sqlite.idb && \
|
||||
shopt -s nullglob; \
|
||||
for module in *.modl; do \
|
||||
./register-module.sh \
|
||||
-f "${module}" \
|
||||
-d db_backup_sqlite.idb; \
|
||||
done; \
|
||||
shopt -u nullglob && \
|
||||
./register-password.sh \
|
||||
-u "${GATEWAY_ADMIN_USERNAME}" \
|
||||
-f /run/secrets/gateway-admin-password \
|
||||
-d db_backup_sqlite.idb && \
|
||||
zip -q -f "${BASE_GWBK_NAME}" db_backup_sqlite.idb || \
|
||||
if [[ ${ZIP_EXIT_CODE:=$?} == 12 ]]; then \
|
||||
echo "No changes to internal database needed during module registration."; \
|
||||
else \
|
||||
echo "Unknown error (${ZIP_EXIT_CODE}) encountered during re-packaging of config db, exiting." && \
|
||||
exit ${ZIP_EXIT_CODE}; \
|
||||
fi
|
||||
|
||||
# Final Image
|
||||
FROM inductiveautomation/ignition:${IGNITION_VERSION} as final
|
||||
ARG BASE_GWBK_NAME="base.gwbk"
|
||||
ARG IGNITION_EDITION="edge"
|
||||
|
||||
# Embed modules and base gwbk from prep image as well as entrypoint shim
|
||||
COPY --from=prep --chown=ignition:ignition /root/*.modl ${IGNITION_INSTALL_LOCATION}/user-lib/modules/
|
||||
COPY --from=prep --chown=ignition:ignition /root/${BASE_GWBK_NAME} ${IGNITION_INSTALL_LOCATION}/base.gwbk
|
||||
COPY --chmod=0755 --chown=root:root docker-entrypoint-shim.sh /usr/local/bin/
|
||||
|
||||
# Return to ignition user
|
||||
USER ignition
|
||||
|
||||
# Set Ignition Edition default based on build argument
|
||||
ENV IGNITION_EDITION="${IGNITION_EDITION}"
|
||||
|
||||
# Target the entrypoint shim for any custom logic prior to gateway launch
|
||||
ENTRYPOINT [ "docker-entrypoint-shim.sh" ]
|
||||
BIN
gw-build/base.gwbk
Normal file
BIN
gw-build/base.gwbk
Normal file
Binary file not shown.
5
gw-build/docker-entrypoint-shim.sh
Normal file
5
gw-build/docker-entrypoint-shim.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Kick off the built-in entrypoint, with an in-built restore (-r <gwbk path>) directive
|
||||
exec docker-entrypoint.sh -r base.gwbk "$@"
|
||||
125
gw-build/register-module.sh
Executable file
125
gw-build/register-module.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
###############################################################################
|
||||
# Performs auto-acceptance of EULA and import of certificates for third-party modules
|
||||
###############################################################################
|
||||
function main() {
|
||||
if [ ! -f "${MODULE_LOCATION}" ]; then
|
||||
echo ""
|
||||
return 0 # Silently exit if there is no /modules path
|
||||
elif [ ! -f "${DB_LOCATION}" ]; then
|
||||
echo "WARNING: ${DB_FILE} not found, skipping module registration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
register_module
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Register the module with the target Config DB
|
||||
###############################################################################
|
||||
function register_module() {
|
||||
local SQLITE3=( sqlite3 "${DB_LOCATION}" )
|
||||
|
||||
# Tie into db
|
||||
local keytool module_sourcepath
|
||||
module_basename=$(basename "${MODULE_LOCATION}")
|
||||
module_sourcepath=${MODULE_LOCATION}
|
||||
keytool=$(which keytool)
|
||||
|
||||
echo "Processing Module: ${module_basename}"
|
||||
|
||||
# Populate CERTIFICATES table
|
||||
local cert_info subject_name thumbprint next_certificates_id thumbprint_already_exists
|
||||
cert_info=$( unzip -qq -c "${module_sourcepath}" certificates.p7b | $keytool -printcert -v | head -n 9 )
|
||||
thumbprint=$( echo "${cert_info}" | grep -A 2 "Certificate fingerprints" | grep SHA1 | cut -d : -f 2- | sed -e 's/\://g' | awk '{$1=$1;print tolower($0)}' )
|
||||
subject_name=$( echo "${cert_info}" | grep -m 1 -Po '^Owner: CN=\K(.+?)(?=, (OU|O|L|ST|C)=)' | sed -e 's/"//g' )
|
||||
echo " Thumbprint: ${thumbprint}"
|
||||
echo " Subject Name: ${subject_name}"
|
||||
next_certificates_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(CERTIFICATES_ID)+1,1) FROM CERTIFICATES" )
|
||||
thumbprint_already_exists=$( "${SQLITE3[@]}" "SELECT 1 FROM CERTIFICATES WHERE lower(hex(THUMBPRINT)) = '${thumbprint}'" )
|
||||
if [ "${thumbprint_already_exists}" != "1" ]; then
|
||||
echo " Accepting Certificate as CERTIFICATES_ID=${next_certificates_id}"
|
||||
"${SQLITE3[@]}" "INSERT INTO CERTIFICATES (CERTIFICATES_ID, THUMBPRINT, SUBJECTNAME) VALUES (${next_certificates_id}, x'${thumbprint}', '${subject_name}'); UPDATE SEQUENCES SET val=${next_certificates_id} WHERE name='CERTIFICATES_SEQ'"
|
||||
else
|
||||
echo " Thumbprint already found in CERTIFICATES table, skipping INSERT"
|
||||
fi
|
||||
|
||||
# Populate EULAS table
|
||||
local next_eulas_id license_crc32 module_id
|
||||
local -i module_id_check
|
||||
next_eulas_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(EULAS_ID)+1,1) FROM EULAS" )
|
||||
license_filename=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=<license>).*(?=</license)' )
|
||||
license_crc32=$( unzip -qq -c "${module_sourcepath}" "${license_filename}" | gzip -c | tail -c8 | od -t u4 -N 4 -A n | cut -c 2- )
|
||||
module_id=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=<id>).*(?=</id)' )
|
||||
module_id_check=$( "${SQLITE3[@]}" "SELECT CASE WHEN CRC=${license_crc32} THEN -1 ELSE 1 END FROM EULAS WHERE MODULEID='${module_id}'" )
|
||||
if (( module_id_check == 1 )); then
|
||||
echo " Removing previous EULAS entries for MODULEID='${module_id}'"
|
||||
"${SQLITE3[@]}" "DELETE FROM EULAS WHERE MODULEID='${module_id}'"
|
||||
fi
|
||||
if (( module_id_check >= 0 )); then
|
||||
echo " Accepting License on your behalf as EULAS_ID=${next_eulas_id}"
|
||||
"${SQLITE3[@]}" "INSERT INTO EULAS (EULAS_ID, MODULEID, CRC) VALUES (${next_eulas_id}, '${module_id}', ${license_crc32}); UPDATE SEQUENCES SET val=${next_eulas_id} WHERE name='EULAS_SEQ'"
|
||||
else
|
||||
echo " License EULA already found in EULAS table, skipping INSERT"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Outputs to stderr
|
||||
###############################################################################
|
||||
function debug() {
|
||||
# shellcheck disable=SC2236
|
||||
if [ ! -z ${verbose+x} ]; then
|
||||
>&2 echo " DEBUG: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Print usage information
|
||||
###############################################################################
|
||||
function usage() {
|
||||
>&2 echo "Usage: $0 -f <path/to/module> -d <path/to/db>"
|
||||
}
|
||||
|
||||
# Argument Processing
|
||||
while getopts ":hvf:d:" opt; do
|
||||
case "$opt" in
|
||||
v)
|
||||
verbose=1
|
||||
;;
|
||||
f)
|
||||
MODULE_LOCATION="${OPTARG}"
|
||||
;;
|
||||
d)
|
||||
DB_LOCATION="${OPTARG}"
|
||||
DB_FILE=$(basename "${DB_LOCATION}")
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG}" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG} requires an argument" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# shift positional args based on number consumed by getopts
|
||||
shift $((OPTIND-1))
|
||||
|
||||
if [ -z "${MODULE_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main
|
||||
88
gw-build/register-modules.sh
Normal file
88
gw-build/register-modules.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
###############################################################################
|
||||
# Retrieves third-party modules and verifies their checksums
|
||||
###############################################################################
|
||||
function main() {
|
||||
if [ -z "${SUPPLEMENTAL_MODULES}" ]; then
|
||||
return 0 # Silently exit if there are no supplemental modules to target
|
||||
fi
|
||||
|
||||
retrieve_modules
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Download the modules
|
||||
###############################################################################
|
||||
function retrieve_modules() {
|
||||
IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}"
|
||||
for module_install_key in "${module_install_key_arr[@]}"; do
|
||||
download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL"
|
||||
download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256"
|
||||
if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then
|
||||
download_basename=$(basename "${!download_url_env}")
|
||||
wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \
|
||||
[[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c -
|
||||
else
|
||||
echo "Error finding specified module ${module_install_key} in build args, aborting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Outputs to stderr
|
||||
###############################################################################
|
||||
function debug() {
|
||||
# shellcheck disable=SC2236
|
||||
if [ ! -z ${verbose+x} ]; then
|
||||
>&2 echo " DEBUG: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Print usage information
|
||||
###############################################################################
|
||||
function usage() {
|
||||
>&2 echo "Usage: $0 -m \"space-separated modules list\""
|
||||
>&2 echo " -m: space-separated list of module identifiers to download"
|
||||
}
|
||||
|
||||
# Argument Processing
|
||||
while getopts ":hvm:" opt; do
|
||||
case "$opt" in
|
||||
v)
|
||||
verbose=1
|
||||
;;
|
||||
m)
|
||||
SUPPLEMENTAL_MODULES="${OPTARG}"
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG}" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG} requires an argument" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# shift positional args based on number consumed by getopts
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# exit on missing required args
|
||||
if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main
|
||||
148
gw-build/register-password.sh
Executable file
148
gw-build/register-password.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
# Global variables
|
||||
declare -u AUTH_SALT
|
||||
|
||||
###############################################################################
|
||||
# Update an Ignition SQLite Configuration DB with a baseline username/password
|
||||
# ----------------------------------------------------------------------------
|
||||
# ref: https://gist.github.com/thirdgen88/c4257bd4c47b6cc7194d1f5e7cbd6444
|
||||
###############################################################################
|
||||
function main() {
|
||||
if [ ! -f "${SECRET_LOCATION}" ]; then
|
||||
echo ""
|
||||
return 0 # Silently exit if there is no secret at target path
|
||||
elif [ ! -f "${DB_LOCATION}" ]; then
|
||||
echo "WARNING: ${DB_FILE} not found, skipping password registration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
register_password
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Updates the target Config DB with the target username and salted pw hash
|
||||
###############################################################################
|
||||
function register_password() {
|
||||
local SQLITE3=( sqlite3 "${DB_LOCATION}" ) password_hash password_input
|
||||
|
||||
echo "Registering Admin Password with Configuration DB"
|
||||
|
||||
# Generate Salted PW Hash
|
||||
password_input="$(< "${SECRET_LOCATION}")"
|
||||
if [[ "${password_input}" =~ ^\[[0-9A-F]{8,}][0-9a-f]{64}$ ]]; then
|
||||
debug "Password is already hashed"
|
||||
password_hash="${password_input}"
|
||||
else
|
||||
password_hash=$(generate_salted_hash "$(<"${SECRET_LOCATION}")")
|
||||
fi
|
||||
|
||||
# Update INTERNALUSERTABLE
|
||||
echo " Setting default admin user to USERNAME='${GATEWAY_ADMIN_USERNAME}' and PASSWORD='${password_hash}'"
|
||||
"${SQLITE3[@]}" "UPDATE INTERNALUSERTABLE SET USERNAME='${GATEWAY_ADMIN_USERNAME}', PASSWORD='${password_hash}' WHERE PROFILEID=1 AND USERID=1"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Processes password input and translates to salted hash
|
||||
###############################################################################
|
||||
function generate_salted_hash() {
|
||||
local auth_pwhash auth_pwsalthash auth_password password_input
|
||||
password_input="${1}"
|
||||
|
||||
debug "auth_salt is ${AUTH_SALT}"
|
||||
auth_pwhash=$(printf %s "${password_input}" | sha256sum - | cut -c -64)
|
||||
debug "auth_pwhash is ${auth_pwhash}"
|
||||
auth_pwsalthash=$(printf %s "${password_input}${AUTH_SALT}" | sha256sum - | cut -c -64)
|
||||
debug "auth_pwsalthash is ${auth_pwsalthash}"
|
||||
auth_password="[${AUTH_SALT}]${auth_pwsalthash}"
|
||||
|
||||
echo "${auth_password}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Outputs to stderr
|
||||
###############################################################################
|
||||
function debug() {
|
||||
# shellcheck disable=SC2236
|
||||
if [ ! -z ${verbose+x} ]; then
|
||||
>&2 echo " DEBUG: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Print usage information
|
||||
###############################################################################
|
||||
function usage() {
|
||||
>&2 echo "Usage: $0 -u <string> -f <path/to/file> -d <path/to/db> [...]"
|
||||
>&2 echo " -u <string> Gateway Admin Username"
|
||||
>&2 echo " -f <path/to/file> Path to secret file containing password or salted hash"
|
||||
>&2 echo " -d <path/to/db> Path to Ignition Configuration DB"
|
||||
>&2 echo " -s <salt method> Salt method, either 'timestamp' or 'random' (default)"
|
||||
}
|
||||
|
||||
# Argument Processing
|
||||
while getopts ":hvu:f:d:s:" opt; do
|
||||
case "$opt" in
|
||||
v)
|
||||
verbose=1
|
||||
;;
|
||||
u)
|
||||
GATEWAY_ADMIN_USERNAME="${OPTARG}"
|
||||
;;
|
||||
f)
|
||||
SECRET_LOCATION="${OPTARG}"
|
||||
;;
|
||||
d)
|
||||
DB_LOCATION="${OPTARG}"
|
||||
DB_FILE=$(basename "${DB_LOCATION}")
|
||||
;;
|
||||
s)
|
||||
# Compute AUTH_SALT based on timestamp or random
|
||||
case "${OPTARG}" in
|
||||
timestamp)
|
||||
AUTH_SALT=$(date +%s | sha256sum | head -c 8)
|
||||
;;
|
||||
random)
|
||||
# no-op, default will be set below
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
echo "Invalid salt method: ${OPTARG}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG}" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG} requires an argument" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# shift positional args based on number consumed by getopts
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# Check for required defaults
|
||||
if [ -z "${GATEWAY_ADMIN_USERNAME:-}" ] || [ -z "${SECRET_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# set defaults for unset optional args
|
||||
if [[ -z ${AUTH_SALT+x} ]]; then
|
||||
AUTH_SALT=$(od -An -v -t x1 -N 4 /dev/random | tr -d ' ')
|
||||
fi
|
||||
|
||||
main
|
||||
88
gw-build/retrieve-modules.sh
Executable file
88
gw-build/retrieve-modules.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
###############################################################################
|
||||
# Retrieves third-party modules and verifies their checksums
|
||||
###############################################################################
|
||||
function main() {
|
||||
if [ -z "${SUPPLEMENTAL_MODULES}" ]; then
|
||||
return 0 # Silently exit if there are no supplemental modules to target
|
||||
fi
|
||||
|
||||
retrieve_modules
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Download the modules
|
||||
###############################################################################
|
||||
function retrieve_modules() {
|
||||
IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}"
|
||||
for module_install_key in "${module_install_key_arr[@]}"; do
|
||||
download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL"
|
||||
download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256"
|
||||
if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then
|
||||
download_basename=$(basename "${!download_url_env}")
|
||||
wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \
|
||||
[[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c -
|
||||
else
|
||||
echo "Error finding specified module ${module_install_key} in build args, aborting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Outputs to stderr
|
||||
###############################################################################
|
||||
function debug() {
|
||||
# shellcheck disable=SC2236
|
||||
if [ ! -z ${verbose+x} ]; then
|
||||
>&2 echo " DEBUG: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Print usage information
|
||||
###############################################################################
|
||||
function usage() {
|
||||
>&2 echo "Usage: $0 -m \"space-separated modules list\""
|
||||
>&2 echo " -m: space-separated list of module identifiers to download"
|
||||
}
|
||||
|
||||
# Argument Processing
|
||||
while getopts ":hvm:" opt; do
|
||||
case "$opt" in
|
||||
v)
|
||||
verbose=1
|
||||
;;
|
||||
m)
|
||||
SUPPLEMENTAL_MODULES="${OPTARG}"
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG}" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
usage
|
||||
echo "Invalid option: -${OPTARG} requires an argument" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# shift positional args based on number consumed by getopts
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# exit on missing required args
|
||||
if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main
|
||||
5
gw-init/gateway.env
Normal file
5
gw-init/gateway.env
Normal file
@@ -0,0 +1,5 @@
|
||||
ACCEPT_IGNITION_EULA=Y
|
||||
# GATEWAY_ADMIN_USERNAME=admin
|
||||
# GATEWAY_ADMIN_PASSWORD_FILE=/run/secrets/gateway-admin-password
|
||||
TZ=America/Chicago
|
||||
GATEWAY_MODULES_ENABLED=all
|
||||
BIN
modules/MQTT-Transmission-signed (1).modl
Normal file
BIN
modules/MQTT-Transmission-signed (1).modl
Normal file
Binary file not shown.
1
secrets/db-ignition-password
Normal file
1
secrets/db-ignition-password
Normal file
@@ -0,0 +1 @@
|
||||
password
|
||||
1
secrets/db-root-password
Normal file
1
secrets/db-root-password
Normal file
@@ -0,0 +1 @@
|
||||
password
|
||||
1
secrets/gateway-admin-password
Normal file
1
secrets/gateway-admin-password
Normal file
@@ -0,0 +1 @@
|
||||
password
|
||||
Reference in New Issue
Block a user