feat(docker): add guided install and saved one-command updates
This commit is contained in:
1 parent
8dc187483c
commit
27447ce029
11 files changed
+393
-5
No files matched your search
@@ -36,3 +36,7 @@ db_backup_*.sql
|
|||||||
*.log
|
*.log
|
||||||
.codex
|
.codex
|
||||||
.agents
|
.agents
|
||||||
|
|
||||||
|
.docker-install
|
||||||
|
.docker-install.tmp.*
|
||||||
|
.env.install.*
|
||||||
@@ -45,3 +45,8 @@ coverage/
|
|||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
blob-report/
|
blob-report/
|
||||||
|
|
||||||
|
# Local Docker installation metadata
|
||||||
|
.docker-install
|
||||||
|
.docker-install.tmp.*
|
||||||
|
.env.install.*
|
||||||
@@ -217,6 +217,50 @@ local `/swf/c_images/album1584` fallback. Public URL resolution uses runtime `AP
|
|||||||
After changing `.env`, recreate the container; an image rebuild is not required.
|
After changing `.env`, recreate the container; an image rebuild is not required.
|
||||||
`NEXT_PUBLIC_CMS_RELEASE` deliberately remains compiled into the image.
|
`NEXT_PUBLIC_CMS_RELEASE` deliberately remains compiled into the image.
|
||||||
|
|
||||||
|
### Guided installation from this repository
|
||||||
|
|
||||||
|
On a Linux host with Docker Engine, the Compose plugin, Git and `flock` available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitlab.epicnabbo.nl/remco/EpicNext-Cms.git
|
||||||
|
cd EpicNext-Cms
|
||||||
|
bash cms install
|
||||||
|
```
|
||||||
|
|
||||||
|
The wizard asks for your public URL, delivery method, hotel name, existing database
|
||||||
|
connection, Redis and avatar imager. It generates an authentication secret and
|
||||||
|
creates `.env` with private permissions. An existing `.env` is preserved; review
|
||||||
|
its `APP_URL`, `AUTH_URL`, RCON and optional original Laravel `APP_KEY` when moving
|
||||||
|
an existing hotel. This installs the CMS, not the Habbo database, emulator, Redis
|
||||||
|
or reverse proxy. Point HTTPS at port 3002 before running the final public check.
|
||||||
|
The wizard may request sudo to prepare persistent directories for UID/GID 33;
|
||||||
|
it does not recursively change ownership of existing assets.
|
||||||
|
|
||||||
|
Choose **prebuilt** to download the images specified by this repository in
|
||||||
|
`docker-image.txt`, without entering a registry namespace or commit tag. Choose
|
||||||
|
**source** when building your own fork on the installation host. Private packages
|
||||||
|
still require `docker login` on that host. Maintainers must update `docker-image.txt`
|
||||||
|
if the publication registry or namespace changes.
|
||||||
|
|
||||||
|
After installation, update with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash cms update
|
||||||
|
```
|
||||||
|
|
||||||
|
The updater pulls the configured Git upstream, loads `.docker-install`, uses the
|
||||||
|
matching application/migration images and verifies the running release locally
|
||||||
|
and at the saved public URL. If publication for the new commit is still running,
|
||||||
|
it stops before replacing the current container; run the same command after CI
|
||||||
|
succeeds. Existing application rollback remains available on a failed cutover;
|
||||||
|
database migrations are not reversed.
|
||||||
|
|
||||||
|
`bash cms install --configure-only` saves configuration without preparing runtime
|
||||||
|
directories or starting containers. Neither `.env` nor `.docker-install` is
|
||||||
|
committed or sent in Docker build contexts. Existing users of
|
||||||
|
`bash scripts/docker-update.sh` retain the previous behavior when no wizard
|
||||||
|
profile exists; explicit `CMS_IMAGE_REPOSITORY`/`CMS_PUBLIC_URL` overrides still work.
|
||||||
|
|
||||||
To publish from Gitea:
|
To publish from Gitea:
|
||||||
|
|
||||||
Gitea packages belong to an account or organization, independently of repository
|
Gitea packages belong to an account or organization, independently of repository
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
case "${1:-help}" in
|
||||||
|
install) shift; exec bash "$DIR/scripts/docker-install.sh" "$@" ;;
|
||||||
|
update) shift; [[ $# = 0 ]] || { echo "Usage: bash cms update" >&2; exit 1; }; exec bash "$DIR/scripts/docker-update.sh" ;;
|
||||||
|
help|--help|-h) printf '%s\n' 'bash cms install Configure and install on a Linux Docker host' 'bash cms update Update using the saved installation settings' ;;
|
||||||
|
*) echo "Unknown command. Use: bash cms install | update" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
gitlab.epicnabbo.nl/simo/epicnext-cms
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Read installation metadata as data, never execute it as a shell script.
|
||||||
|
load_docker_config() {
|
||||||
|
local root="$1" key value mode="" public_url="" repository=""
|
||||||
|
[[ -f "$root/.docker-install" ]] || return 0
|
||||||
|
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||||
|
value="${value%$'\r'}"
|
||||||
|
case "$key" in
|
||||||
|
''|'#'*) continue ;;
|
||||||
|
MODE) [[ "$value" = prebuilt || "$value" = source ]] || return 1; mode="$value" ;;
|
||||||
|
PUBLIC_URL) [[ "$value" =~ ^https?://[^[:space:]]+$ && "$value" != *'@'* && "$value" != *'?'* && "$value" != *'#'* ]] || return 1; public_url="$value" ;;
|
||||||
|
*) echo "Unknown installation setting: $key" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
done < "$root/.docker-install"
|
||||||
|
[[ -n "$mode" && -n "$public_url" ]] || return 1
|
||||||
|
if [[ "$mode" = prebuilt && ! ${CMS_IMAGE_REPOSITORY+x} ]]; then
|
||||||
|
IFS= read -r repository < "$root/docker-image.txt"
|
||||||
|
repository="${repository%$'\r'}"
|
||||||
|
[[ "$repository" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || return 1
|
||||||
|
export CMS_IMAGE_REPOSITORY="$repository"
|
||||||
|
fi
|
||||||
|
CMS_INSTALL_MODE="$mode"
|
||||||
|
export CMS_PUBLIC_URL="${CMS_PUBLIC_URL-$public_url}"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -69,6 +69,9 @@ if [ "$script_before" != "$(git hash-object scripts/docker-update.sh)" ]; then
|
|||||||
exec 9>&-
|
exec 9>&-
|
||||||
exec bash "$DIR/scripts/docker-update.sh"
|
exec bash "$DIR/scripts/docker-update.sh"
|
||||||
fi
|
fi
|
||||||
|
# Load after pulling so image location changes follow the repository.
|
||||||
|
source "$DIR/scripts/docker-config.sh"
|
||||||
|
load_docker_config "$DIR" || die "Invalid .docker-install: expected MODE=prebuilt or source and PUBLIC_URL=http(s)://your-host."
|
||||||
export CMS_RELEASE="$(git rev-parse HEAD)"
|
export CMS_RELEASE="$(git rev-parse HEAD)"
|
||||||
[[ "$CMS_RELEASE" =~ ^[0-9a-f]{40}$ ]] || die "Invalid Git commit."
|
[[ "$CMS_RELEASE" =~ ^[0-9a-f]{40}$ ]] || die "Invalid Git commit."
|
||||||
[[ -f .env ]] || die "Create .env before installing or updating."
|
[[ -f .env ]] || die "Create .env before installing or updating."
|
||||||
@@ -82,16 +85,16 @@ if [[ -n "$previous_container" ]]; then
|
|||||||
rollback_tag="rollback-$CMS_RELEASE-$$"
|
rollback_tag="rollback-$CMS_RELEASE-$$"
|
||||||
docker image tag "$previous_image" "epicnext-cms:$rollback_tag"
|
docker image tag "$previous_image" "epicnext-cms:$rollback_tag"
|
||||||
fi
|
fi
|
||||||
log "Building release $CMS_RELEASE from $DIR"
|
log "Preparing release $CMS_RELEASE from $DIR"
|
||||||
# The migrations stage contains matching source and locked dependencies.
|
# The migrations stage contains matching source and locked dependencies.
|
||||||
# No Node/package manager installation on the host is required.
|
# No Node/package manager installation on the host is required.
|
||||||
migration_image="epicnext-cms-migrations:$CMS_RELEASE"
|
migration_image="epicnext-cms-migrations:$CMS_RELEASE"
|
||||||
if [[ -n "${CMS_IMAGE_REPOSITORY:-}" ]]; then
|
if [[ -n "${CMS_IMAGE_REPOSITORY:-}" ]]; then
|
||||||
[[ "$CMS_IMAGE_REPOSITORY" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || die "Invalid CMS_IMAGE_REPOSITORY; use registry/owner/image without a tag."
|
[[ "$CMS_IMAGE_REPOSITORY" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || die "Invalid CMS_IMAGE_REPOSITORY; use registry/owner/image without a tag."
|
||||||
log "Pulling prebuilt application and matching migrations for $CMS_RELEASE"
|
log "Pulling prebuilt application and matching migrations for $CMS_RELEASE"
|
||||||
docker pull "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" >>"$LOG_FILE" 2>&1
|
docker pull "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" >>"$LOG_FILE" 2>&1 || die "Application image unavailable. Publication for this commit may still be running; retry bash cms update after CI succeeds. For private packages, log in to the registry first."
|
||||||
remote_migration_image="$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations"
|
remote_migration_image="$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations"
|
||||||
docker pull "$remote_migration_image" >>"$LOG_FILE" 2>&1
|
docker pull "$remote_migration_image" >>"$LOG_FILE" 2>&1 || die "Matching migrations image unavailable. Current CMS is unchanged; retry after publication succeeds."
|
||||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" "epicnext-cms:$CMS_RELEASE"
|
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" "epicnext-cms:$CMS_RELEASE"
|
||||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations" "$migration_image"
|
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations" "$migration_image"
|
||||||
else
|
else
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,14 @@ function simulate(scenario: string) {
|
|||||||
try {
|
try {
|
||||||
mkdirSync(join(dir, "scripts"));
|
mkdirSync(join(dir, "scripts"));
|
||||||
mkdirSync(join(dir, "logs"));
|
mkdirSync(join(dir, "logs"));
|
||||||
|
copyFileSync(
|
||||||
|
resolve(root, "scripts/docker-config.sh"),
|
||||||
|
join(dir, "scripts/docker-config.sh"),
|
||||||
|
);
|
||||||
|
copyFileSync(
|
||||||
|
resolve(root, "docker-image.txt"),
|
||||||
|
join(dir, "docker-image.txt"),
|
||||||
|
);
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(dir, "logs/docker-release-history.log"),
|
join(dir, "logs/docker-release-history.log"),
|
||||||
`${"b".repeat(40)}\n${"c".repeat(40)}\n`,
|
`${"b".repeat(40)}\n${"c".repeat(40)}\n`,
|
||||||
@@ -39,6 +47,11 @@ function simulate(scenario: string) {
|
|||||||
join(dir, "scripts/docker-update.sh"),
|
join(dir, "scripts/docker-update.sh"),
|
||||||
);
|
);
|
||||||
writeFileSync(join(dir, ".env"), "HOTEL_NAME=Test\n");
|
writeFileSync(join(dir, ".env"), "HOTEL_NAME=Test\n");
|
||||||
|
if (scenario.startsWith("saved-"))
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, ".docker-install"),
|
||||||
|
`MODE=${scenario === "saved-source" ? "source" : "prebuilt"}\nPUBLIC_URL=https://saved.test\n`,
|
||||||
|
);
|
||||||
const result = spawnSync(bash, [join(dir, "scripts/docker-update.sh")], {
|
const result = spawnSync(bash, [join(dir, "scripts/docker-update.sh")], {
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
@@ -49,8 +62,12 @@ function simulate(scenario: string) {
|
|||||||
TEST_DIR: dir.replaceAll("\\", "/"),
|
TEST_DIR: dir.replaceAll("\\", "/"),
|
||||||
TEST_SHA: sha,
|
TEST_SHA: sha,
|
||||||
SCENARIO: scenario,
|
SCENARIO: scenario,
|
||||||
CMS_PUBLIC_URL: "https://example.test",
|
CMS_PUBLIC_URL: scenario.startsWith("saved-")
|
||||||
CMS_IMAGE_REPOSITORY: scenario.startsWith("registry")
|
? undefined
|
||||||
|
: "https://example.test",
|
||||||
|
CMS_IMAGE_REPOSITORY: scenario.startsWith("saved-")
|
||||||
|
? undefined
|
||||||
|
: scenario.startsWith("registry")
|
||||||
? "registry.test/team/cms"
|
? "registry.test/team/cms"
|
||||||
: "",
|
: "",
|
||||||
},
|
},
|
||||||
@@ -169,3 +186,19 @@ describe("HTTP release verification", () => {
|
|||||||
expect(result.status, result.stderr).toBe(expected);
|
expect(result.status, result.stderr).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates using the saved profile and repository-provided image without arguments", () => {
|
||||||
|
const r = simulate("saved-prebuilt");
|
||||||
|
expect(r.status, r.output).toBe(0);
|
||||||
|
expect(r.calls).toContain(
|
||||||
|
`docker pull gitlab.epicnabbo.nl/simo/epicnext-cms:${sha}`,
|
||||||
|
);
|
||||||
|
expect(r.calls).toContain("https://saved.test/api/health");
|
||||||
|
expect(r.calls).not.toContain("docker compose build");
|
||||||
|
});
|
||||||
|
it("keeps source builds available for a saved source installation", () => {
|
||||||
|
const r = simulate("saved-source");
|
||||||
|
expect(r.status, r.output).toBe(0);
|
||||||
|
expect(r.calls).toContain("docker compose build");
|
||||||
|
expect(r.calls).not.toContain("docker pull");
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# No Docker, permissions or external services are modified by wizard tests.
|
||||||
|
docker() { return 0; }
|
||||||
|
uname() { echo Linux; }
|
||||||
|
flock() { :; }
|
||||||
|
export -f docker uname flock
|
||||||
|
install() { :; }
|
||||||
|
chown() { :; }
|
||||||
|
stat() { echo 33:33; }
|
||||||
|
sudo() { "$@"; }
|
||||||
|
export -f install chown stat sudo
|
||||||
Reference in new issue
Block a user