Merge 15aab8bdaa into 91fc59a134
66
.github/workflows/easy-docker.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: Easy Docker Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "easy-docker.sh"
|
||||
- "scripts/easy-docker/**"
|
||||
- "tests/easy-docker/**"
|
||||
- ".github/workflows/easy-docker.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "easy-docker.sh"
|
||||
- "scripts/easy-docker/**"
|
||||
- "tests/easy-docker/**"
|
||||
- ".github/workflows/easy-docker.yml"
|
||||
|
||||
jobs:
|
||||
bats:
|
||||
name: Bats (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Ubuntu 24.04
|
||||
image: ubuntu:24.04
|
||||
install_cmd: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y bash ca-certificates curl gawk git gzip tar ncurses-bin
|
||||
update-alternatives --set awk /usr/bin/gawk
|
||||
- name: Debian 12
|
||||
image: debian:12
|
||||
install_cmd: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y bash ca-certificates curl gawk git gzip tar ncurses-bin
|
||||
update-alternatives --set awk /usr/bin/gawk
|
||||
- name: Fedora 43
|
||||
image: fedora:43
|
||||
install_cmd: |
|
||||
dnf install -y bash ca-certificates curl gawk git gzip tar ncurses
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
env:
|
||||
TERM: xterm-256color
|
||||
|
||||
steps:
|
||||
- name: Install distro dependencies
|
||||
run: ${{ matrix.install_cmd }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Bats
|
||||
run: |
|
||||
BATS_VERSION="v1.11.1"
|
||||
curl -fsSL "https://github.com/bats-core/bats-core/archive/refs/tags/${BATS_VERSION}.tar.gz" -o bats-core.tar.gz
|
||||
tar -xzf bats-core.tar.gz
|
||||
"./bats-core-${BATS_VERSION#v}/install.sh" /usr/local
|
||||
|
||||
- name: Run easy-docker Bats tests
|
||||
run: bats --recursive tests/easy-docker
|
||||
3
.gitignore
vendored
|
|
@ -32,3 +32,6 @@ node_modules
|
|||
# VitePress
|
||||
**/.vitepress/dist
|
||||
**/.vitepress/cache
|
||||
|
||||
# easy-docker local runtime data (contains secrets)
|
||||
.easy-docker/
|
||||
|
|
|
|||
18
README.md
|
|
@ -67,6 +67,24 @@ The full `frappe_docker` documentation is available in [`docs/`](docs/) and publ
|
|||
- **Development workflows:** [Development](docs/05-development/01-development.md)
|
||||
- **FAQ:** [Frequently Asked Questions](https://github.com/frappe/frappe_docker/wiki/Frequently-Asked-Questions)
|
||||
|
||||
### Easy Docker
|
||||
|
||||
`easy-docker` is the guided terminal workflow for creating and managing Frappe
|
||||
Docker stacks from one place. It helps with stack creation, app and branch
|
||||
selection, image builds, runtime actions, and supported site operations without
|
||||
requiring users to assemble the Compose and Bench commands manually.
|
||||
|
||||
If you want a guided setup path, start with the
|
||||
[Easy Docker docs](docs/10-easy-docker/index.md).
|
||||
|
||||
Run it from the repository root with:
|
||||
|
||||
```bash
|
||||
bash easy-docker.sh
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
|
|
|
|||
52
docs/01-getting-started/05-easy-docker.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
title: Easy Docker
|
||||
---
|
||||
|
||||
# Easy Docker
|
||||
|
||||
`easy-docker` is the interactive setup and management workflow for this repository.
|
||||
It guides common stack operations through a terminal UI so you do not have to assemble
|
||||
every Docker and Bench command manually.
|
||||
|
||||
For the detailed guide, use the dedicated docs area under `docs/10-easy-docker/`.
|
||||
This getting-started page stays short and focuses on the first steps.
|
||||
|
||||
Current status:
|
||||
|
||||
- `single-host` is the primary supported topology
|
||||
- `split-services` is available for separated stack setup and Compose runtime control
|
||||
- site actions currently remain part of the `single-host` workflow
|
||||
- stack, site, app, backup, restart, and update flows are being expanded iteratively
|
||||
|
||||
The script entrypoint is:
|
||||
|
||||
```bash
|
||||
bash ./easy-docker.sh
|
||||
```
|
||||
|
||||
Before the wizard opens, `easy-docker` validates its startup dependencies.
|
||||
Today that means:
|
||||
|
||||
- `gum`
|
||||
- `docker`
|
||||
- `docker compose`
|
||||
- Docker daemon availability
|
||||
- `jq`
|
||||
|
||||
If `gum` or `jq` is missing, `easy-docker` first tries package-manager
|
||||
installation and can then fall back to a pinned GitHub binary when the setup is
|
||||
interactive and fallback is not disabled. On Windows, use a real Bash
|
||||
environment such as WSL or Git Bash and keep the script path in Bash syntax.
|
||||
|
||||
Minimal first use:
|
||||
|
||||
1. Start `easy-docker.sh`
|
||||
2. Create a new stack
|
||||
3. Choose `single-host`
|
||||
4. Pick the apps and branches you want
|
||||
5. Build the custom image
|
||||
6. Start the stack
|
||||
7. Create the first site or manage an existing one from the stack menu
|
||||
|
||||
Use this page as the entry point. For the full workflow reference, jump to the
|
||||
dedicated `easy-docker` docs section in the root `docs` tree.
|
||||
|
|
@ -4,6 +4,7 @@ title: Troubleshoot
|
|||
|
||||
- [Fixing MariaDB issues after rebuilding the container](#fixing-mariadb-issues-after-rebuilding-the-container)
|
||||
- [docker-compose does not recognize variables from `.env` file](#docker-compose-does-not-recognize-variables-from-env-file)
|
||||
- [easy-docker dependency checks](#easy-docker-dependency-checks)
|
||||
- [Windows Based Installation](#windows-based-installation)
|
||||
- [Redo installation](#redo-installation)
|
||||
|
||||
|
|
@ -69,6 +70,33 @@ Note: For MariaDB 10.3 and older use `mysql.user` instead of `mysql.global_priv`
|
|||
|
||||
If you are using old version of `docker-compose` the .env file needs to be located in directory from where the docker-compose command is executed. There may also be difference in official `docker-compose` and the one packaged by distro. Use `--env-file=.env` if available to explicitly specify the path to file.
|
||||
|
||||
### easy-docker dependency checks
|
||||
|
||||
`easy-docker` now validates its startup dependencies before the TUI opens.
|
||||
|
||||
The check order is:
|
||||
|
||||
1. CLI options
|
||||
2. `gum`
|
||||
3. `docker`
|
||||
4. `jq`
|
||||
|
||||
If `gum` or `jq` is missing, the wizard first tries package-manager
|
||||
installation. If that does not work and the session is interactive, it can then
|
||||
offer a pinned GitHub binary fallback unless `--no-installation-fallback` is
|
||||
set.
|
||||
|
||||
If `jq` is still missing after those steps, startup stops with install guidance
|
||||
instead of continuing into the menus.
|
||||
|
||||
On Windows, pay attention to which Bash runtime you are actually using:
|
||||
|
||||
- `bash` from PowerShell usually means WSL
|
||||
- Git Bash is a separate runtime
|
||||
- use Bash path syntax such as `bash ./easy-docker.sh`
|
||||
|
||||
Windows-native Bash setups can use either `jq` or `jq.exe` on `PATH`.
|
||||
|
||||
### Windows Based Installation
|
||||
|
||||
- Set environment variable `COMPOSE_CONVERT_WINDOWS_PATHS` e.g. `set COMPOSE_CONVERT_WINDOWS_PATHS=1`
|
||||
|
|
|
|||
237
docs/10-easy-docker/01-overview.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
`easy-docker` is the guided terminal workflow for setting up and managing a
|
||||
Frappe Docker stack from one place.
|
||||
|
||||
Instead of collecting Docker Compose, image build, and Bench commands manually,
|
||||
you move through a small set of menus that guide the main lifecycle of a stack.
|
||||
|
||||
The interface is powered by `gum`, which is used to render the interactive
|
||||
terminal menus and prompts.
|
||||
|
||||
All stack data created by the wizard is written into the repository-local
|
||||
`.easy-docker` directory. That includes the generated stack environment files
|
||||
and the stack-specific metadata used by the workflow.
|
||||
|
||||
Internally, the stack JSON contract is now handled through `jq` instead of
|
||||
line-based `awk` parsing. This is meant to improve robustness against harmless
|
||||
JSON formatting differences without changing the generated layout of
|
||||
`metadata.json` or `apps.json`.
|
||||
|
||||
This means `easy-docker` is not a closed system. After the setup has been
|
||||
created, you can still inspect the generated files, keep working with them
|
||||
manually, and continue outside the wizard if that fits your workflow better.
|
||||
|
||||
## What It Helps With
|
||||
|
||||
`easy-docker` is built to make the usual stack workflow easier to follow:
|
||||
|
||||
- create a new stack
|
||||
- choose the stack setup path
|
||||
- select apps and branches
|
||||
- generate the stack environment
|
||||
- build the custom image
|
||||
- start, stop, restart, or delete the stack
|
||||
- create and manage a site
|
||||
- install or uninstall apps on the site
|
||||
- create backups and run site maintenance actions
|
||||
|
||||
## How It Feels to Use
|
||||
|
||||
The workflow is organized as a guided sequence.
|
||||
|
||||
You start with a stack, define how it should be configured, let the wizard write
|
||||
the stack files, then continue into the management area for image, runtime, and
|
||||
site actions.
|
||||
|
||||
This makes `easy-docker` useful both for first-time setup and for returning to
|
||||
an existing stack later when you need to update apps, rebuild an image, restart
|
||||
services, or work on the site itself.
|
||||
|
||||
If you have not worked with a guided Docker setup before, it helps to think of
|
||||
`easy-docker` as a step-by-step assistant. It does not ask you to memorize the
|
||||
Docker commands first. Instead, it asks a small number of questions, writes the
|
||||
stack configuration for you, and then gives you a menu for the most common next
|
||||
actions.
|
||||
|
||||

|
||||
|
||||
## What It Needs
|
||||
|
||||
To run `easy-docker`, the environment should have:
|
||||
|
||||
- a working `docker` CLI
|
||||
- Docker Compose v2 through `docker compose`
|
||||
- a running Docker daemon
|
||||
- `gum` for the interactive terminal UI
|
||||
- `jq` for stack JSON processing
|
||||
|
||||
The startup checks happen in this order:
|
||||
|
||||
1. CLI option parsing
|
||||
2. `gum`
|
||||
3. `docker`
|
||||
4. `jq`
|
||||
5. TUI startup
|
||||
|
||||
That means `--help` exits before dependency checks, while missing dependencies
|
||||
stop the workflow before the menus open.
|
||||
|
||||
When `gum` is already installed, the wizard uses it directly.
|
||||
|
||||
When `gum` is missing, `easy-docker` first tries to install it through the
|
||||
system package manager. If that is not available or does not succeed, the
|
||||
wizard can fall back to a pinned GitHub release and install `gum`
|
||||
automatically when possible.
|
||||
|
||||
This means the usual setup flow is:
|
||||
|
||||
- check whether `gum` is already available
|
||||
- try package-manager installation first
|
||||
- use the verified fallback path only if needed
|
||||
- continue into the wizard once the required tooling is ready
|
||||
|
||||
The Docker requirements are also checked on startup so the workflow stops early
|
||||
with guidance instead of failing later in the middle of stack setup.
|
||||
|
||||
`jq` now follows the same install strategy as `gum`: `easy-docker` first checks
|
||||
whether it is already available, then tries the system package manager, and can
|
||||
finally offer a pinned GitHub binary fallback in interactive sessions.
|
||||
Runtime resolution accepts either `jq` or `jq.exe`, which keeps Windows-native
|
||||
Bash setups compatible as long as one of them is on `PATH`.
|
||||
|
||||
## Main Areas
|
||||
|
||||
### Stack Creation
|
||||
|
||||
The stack creation flow collects the main decisions up front and stores them in
|
||||
the stack directory so the workflow can be resumed later.
|
||||
|
||||
This is where you define the stack identity, choose the setup path, and prepare
|
||||
the generated configuration for the next steps.
|
||||
|
||||
A typical stack creation run moves through these prompts:
|
||||
|
||||
1. Name the stack and choose the Frappe version profile.
|
||||
|
||||
The stack name is simply the label under which `easy-docker` will remember this
|
||||
setup. If you plan to run more than one setup later, choose a name that makes
|
||||
the purpose obvious.
|
||||
|
||||
The Frappe version profile is the base version the stack should start from. If
|
||||
you are unsure, pick the version you intend to use for the actual project or
|
||||
the version your apps are built for.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. Choose the deployment topology and the main infrastructure options.
|
||||
|
||||
In this phase, the wizard asks how the stack should be structured. For most
|
||||
users, this is the point where you choose the simplest practical setup and let
|
||||
the wizard generate the rest of the configuration around it.
|
||||
|
||||
The proxy and database choices decide how traffic reaches the stack and where
|
||||
the site data is stored. Even if you do not know every Docker detail yet, the
|
||||
important part is that these choices describe how your stack should behave once
|
||||
it is running.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
3. Define the image naming and versioning that should be used for the stack.
|
||||
|
||||
This step controls the image that will later be built for your stack. You can
|
||||
think of it as naming the packaged application that Docker should run.
|
||||
|
||||
The image name identifies the image, while the image version or tag helps you
|
||||
track which build you are currently using. That becomes especially useful when
|
||||
you rebuild the stack after changing app branches or updating the setup.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
4. Select the apps and branches that should be built into the stack image.
|
||||
|
||||
This is the point where you decide what should actually be included in the
|
||||
stack. The app selection defines the application set, and the branch selection
|
||||
defines which code line of each app should be used for the build.
|
||||
|
||||
For new users, the practical rule is simple: only include the apps you really
|
||||
need, and choose branches that match the Frappe version profile you selected
|
||||
earlier.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
After these decisions, `easy-docker` has enough information to write the stack
|
||||
files and prepare the next phase. At that point, the workflow moves from
|
||||
planning the stack to actually building and running it.
|
||||
|
||||
### Stack Management
|
||||
|
||||
Once a stack exists, `easy-docker` becomes the control point for the stack:
|
||||
|
||||
- app selection and branch updates
|
||||
- custom image build and rebuild
|
||||
- Compose lifecycle actions
|
||||
- site operations such as create, migrate, backup, and delete
|
||||
|
||||
That means the same workflow continues after setup instead of ending once the
|
||||
first stack files are written.
|
||||
|
||||
The first management steps usually focus on preparing the image and bringing the
|
||||
stack up in Docker Compose.
|
||||
|
||||
The build step creates the actual Docker image for the stack you just defined.
|
||||
Until that image exists, there is nothing concrete for Docker Compose to start.
|
||||
That is why the build action comes before the start action.
|
||||
|
||||

|
||||
|
||||
Once the image has been built successfully, you can start the stack. This tells
|
||||
Docker Compose to create the containers and launch the services that belong to
|
||||
your setup.
|
||||
|
||||

|
||||
|
||||
After startup, the status view helps you confirm that the stack is actually
|
||||
running. This is especially useful for beginners because it gives a visible
|
||||
checkpoint before moving on to site creation or later maintenance steps.
|
||||
|
||||

|
||||
|
||||
In this example, the one stopped container shown in the status output is the
|
||||
`configurator` container. That container is expected to finish and stop after
|
||||
its setup work has completed.
|
||||
|
||||
From there, the workflow usually continues into site-level actions such as
|
||||
creating the first site, installing apps on the site, running migrations, or
|
||||
creating backups. In other words: stack creation defines the environment, and
|
||||
stack management is where that environment becomes usable.
|
||||
|
||||
### Development Stacks
|
||||
|
||||
When you create a stack through the development path, newly created sites in
|
||||
that stack automatically enable `developer_mode`.
|
||||
|
||||
This keeps the development-specific behavior attached to the stack itself, so
|
||||
the workflow stays consistent when you return to manage it later.
|
||||
|
||||
## Entry Point
|
||||
|
||||
Run the wizard from the repository root:
|
||||
|
||||
```bash
|
||||
bash ./easy-docker.sh
|
||||
```
|
||||
37
docs/10-easy-docker/02-workflows.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
title: Workflows
|
||||
---
|
||||
|
||||
# Workflows
|
||||
|
||||
The wizard follows a simple order:
|
||||
|
||||
1. Create a stack.
|
||||
2. Choose `single-host` or `split-services`.
|
||||
3. Select the apps and branches for the stack.
|
||||
4. Generate the stack environment and render the Compose snapshot.
|
||||
5. Build the custom image.
|
||||
6. Start the stack.
|
||||
7. Continue into site actions when the selected workflow supports them.
|
||||
|
||||
Stack actions are grouped around image and Compose lifecycle:
|
||||
|
||||
- `Apps` manages the stack app selection
|
||||
- `Updates` handles app-branch changes and custom image tag updates
|
||||
- `Site` handles site creation, backup, install, uninstall, and deletion
|
||||
- `Start`, `Restart`, `Stop`, and `Delete` control the Compose lifecycle
|
||||
|
||||
Site app management is intentionally scoped to apps that are already part of the
|
||||
stack image. The wizard does not try to install arbitrary apps that are not part
|
||||
of the selected stack configuration.
|
||||
|
||||
Internally, the stack app contract is now handled through `jq` instead of
|
||||
line-based `awk` parsing. This is intended to keep app selection and branch
|
||||
update behavior the same while making the JSON processing more robust in the
|
||||
background. The generated `metadata.json` and `apps.json` files are still meant
|
||||
to look the same to users.
|
||||
|
||||
For the split-services path, see
|
||||
[Split Services](./05-split-services.md). That page explains the intended flow
|
||||
in simple terms and shows where the proxy, application, database, and Redis
|
||||
choices fit into the setup.
|
||||
32
docs/10-easy-docker/03-updates.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: Updates
|
||||
---
|
||||
|
||||
# Updates
|
||||
|
||||
App updates are handled as an image update workflow, not as a live in-container
|
||||
`git pull`.
|
||||
|
||||
The recommended sequence is:
|
||||
|
||||
1. Update the selected app branches.
|
||||
2. Set a new `CUSTOM_TAG`.
|
||||
3. Build the updated custom image.
|
||||
4. Restart the stack.
|
||||
5. Run `migrate` on the site if required by the app change.
|
||||
|
||||
The wizard keeps the current `frappe_branch` visible while you update apps so
|
||||
you can see the base version the stack is built against.
|
||||
|
||||
`CUSTOM_TAG` is stored in the stack `.env` file. The Compose stack reads that
|
||||
value on the next start or restart, so the tag change becomes effective once the
|
||||
image has been rebuilt and the stack is restarted.
|
||||
|
||||
The update flow still rebuilds the image from a regenerated `apps.json`, and
|
||||
`metadata.json` remains the source of truth for the stack state. The difference
|
||||
is internal: stack metadata and generated app state are now processed through
|
||||
`jq` instead of line-based `awk` parsing. No user-visible change in the file
|
||||
layout is intended.
|
||||
|
||||
For now, this update flow focuses on app branch changes. A separate Frappe base
|
||||
version update flow can be added later without changing the overall model.
|
||||
23
docs/10-easy-docker/04-generated-compose.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: Generated Compose
|
||||
---
|
||||
|
||||
# Generated Compose
|
||||
|
||||
`easy-docker` can render a `compose.generated.yaml` snapshot from the stack
|
||||
metadata and environment.
|
||||
|
||||
This file is useful when you want to inspect or reuse the resolved Compose
|
||||
configuration outside the wizard, but it is not the primary runtime input for
|
||||
stack start or stop.
|
||||
|
||||
What is important:
|
||||
|
||||
- the stack runtime reads the original Compose files from metadata
|
||||
- the runtime also reads the stack `.env`
|
||||
- `compose.generated.yaml` is a rendered snapshot, not the source of truth
|
||||
- it is refreshed after a successful custom image build
|
||||
|
||||
That means the generated file stays aligned with the current stack state when the
|
||||
image has actually been rebuilt, which is the point where manual reuse is most
|
||||
likely to matter.
|
||||
215
docs/10-easy-docker/05-split-services.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
---
|
||||
title: Split Services
|
||||
---
|
||||
|
||||
# Split Services
|
||||
|
||||
`split-services` is the guided setup path for users who want to keep the
|
||||
application part of the stack separate from the data part, with an optional
|
||||
proxy layer in front.
|
||||
|
||||
The goal of this page is to help a first-time user understand what each step
|
||||
means and what choice they are making in the wizard.
|
||||
|
||||
The current split-services flow focuses on:
|
||||
|
||||
- writing the split stack files
|
||||
- rendering the generated Compose snapshot
|
||||
- building the custom image
|
||||
- starting, stopping, restarting, and deleting the stack
|
||||
|
||||
This keeps the first version easier to understand while still making the stack
|
||||
layout explicit.
|
||||
|
||||
Think of the split-services setup as three simple parts:
|
||||
|
||||
- `Application Services` run the Frappe application itself
|
||||
- `Data Services` provide the database and Redis services
|
||||
- `Reverse Proxy` handles incoming web traffic when you want a proxy in front
|
||||
|
||||
This page uses the clearer names above so the setup is easier to understand the
|
||||
first time you see it.
|
||||
|
||||
## What This Setup Is For
|
||||
|
||||
Split services are useful when you do not want every service in one combined
|
||||
stack.
|
||||
|
||||
This is a good fit if you want to:
|
||||
|
||||
- keep the application layer separate from the data layer
|
||||
- run the database and Redis independently from the app stack
|
||||
- place a proxy in front only when you actually need it
|
||||
- keep the setup readable so you can manage parts of it later
|
||||
|
||||
If you are new to Docker, you can think of this as splitting one big stack into
|
||||
smaller parts that each have a clearer job.
|
||||
|
||||
## How The Wizard Feels
|
||||
|
||||
The wizard still behaves like the other `easy-docker` flows.
|
||||
|
||||
It asks one question at a time, explains what that choice affects, and then
|
||||
writes the stack files for you. You do not need to build the final Compose
|
||||
files by hand before you start.
|
||||
|
||||

|
||||
|
||||
## Step 1. Choose The Split Topology
|
||||
|
||||
When you select `Split services`, the wizard first explains that the stack is
|
||||
being divided into separate parts instead of using one combined setup.
|
||||
|
||||
At this point you are not changing any service settings yet. You are only
|
||||
choosing the layout of the stack.
|
||||
|
||||
This matters because later steps will follow the same idea. If the setup is
|
||||
split, the wizard will ask about the application side, the data side, and the
|
||||
proxy side separately.
|
||||
|
||||
## Step 2. Decide How The Data Services Should Work
|
||||
|
||||
The next decision is the data layer.
|
||||
|
||||
This is where you decide whether `easy-docker` manages the database and Redis
|
||||
for you, or whether the stack should connect to services that already exist
|
||||
elsewhere.
|
||||
|
||||
### Managed Data Services
|
||||
|
||||
Choose this if you want `easy-docker` to create the database and Redis
|
||||
containers as part of the generated split setup.
|
||||
|
||||
This is usually the easier choice when you are trying split-services for the
|
||||
first time.
|
||||
|
||||
It means:
|
||||
|
||||
- the wizard writes the data service configuration for you
|
||||
- the stack can start with its own managed database and Redis services
|
||||
- you still keep the data layer separate from the application layer
|
||||
|
||||
### External Data Services
|
||||
|
||||
Choose this if the database and Redis are already running somewhere else.
|
||||
|
||||
This is more advanced, but it can be useful when:
|
||||
|
||||
- your organization already provides shared database and Redis services
|
||||
- you want the application stack to connect to existing infrastructure
|
||||
- you are splitting responsibilities across different systems
|
||||
|
||||
If you choose this path, the wizard asks for the required connection values and
|
||||
stores them in the generated stack configuration.
|
||||
|
||||

|
||||
|
||||
## Step 3. Choose The Database Engine
|
||||
|
||||
After choosing how the data layer should be handled, the wizard asks which
|
||||
database engine should be used for the stack.
|
||||
|
||||
For most users, this is the simpler way to think about it:
|
||||
|
||||
- `MariaDB` is the default path and usually the easiest starting point
|
||||
- `PostgreSQL` is available if that is the database you already use or want to use
|
||||
|
||||
If the data layer is managed by `easy-docker`, this choice decides which
|
||||
database service will be created in the generated setup.
|
||||
|
||||
If the data layer is external, this choice still matters because it tells the
|
||||
stack which kind of database it is configured to connect to.
|
||||
|
||||

|
||||
|
||||
## Step 4. Decide How Redis Should Work
|
||||
|
||||
Redis is asked about separately so the setup stays explicit.
|
||||
|
||||
This is useful because some users want Redis managed together with the split
|
||||
stack, while others already have Redis running elsewhere.
|
||||
|
||||
The choices are simple:
|
||||
|
||||
- `Managed Redis Services` means the stack includes Redis services for you
|
||||
- `External Redis Services` means you provide the Redis endpoints yourself
|
||||
- `No Redis Services` is the advanced option when you do not want the wizard to
|
||||
configure Redis for this stack
|
||||
|
||||
The important beginner-friendly idea is that Redis belongs to the data side of
|
||||
the setup, even though it is asked in its own step.
|
||||
|
||||

|
||||
|
||||
## Step 5. Decide Whether A Proxy Should Be Included
|
||||
|
||||
The proxy layer is optional.
|
||||
|
||||
If you want the stack to answer HTTP or HTTPS traffic directly in front of the
|
||||
application services, include a proxy.
|
||||
|
||||
If you do not need that yet, you can skip it and keep the setup simpler.
|
||||
|
||||
For a first-time user, the important idea is:
|
||||
|
||||
- the proxy is the front door
|
||||
- the application services do the actual work
|
||||
- the data services keep the site data and queue state safe
|
||||
|
||||

|
||||
|
||||
## Step 6. Review The Setup Before It Is Written
|
||||
|
||||
Before the wizard writes the files, it shows a summary of the choices you just
|
||||
made.
|
||||
|
||||
This summary is the last chance to stop and check whether the stack is shaped
|
||||
the way you expected.
|
||||
|
||||
The summary makes the split very obvious:
|
||||
|
||||
- what runs as application services
|
||||
- what runs as data services
|
||||
- whether a proxy is included
|
||||
- whether the data services are managed by `easy-docker` or external
|
||||
|
||||

|
||||
|
||||
## What Happens After Setup
|
||||
|
||||
Once the split stack has been written, the generated files are stored in the
|
||||
same repository-local `.easy-docker` area as the other wizard data.
|
||||
|
||||
That means you can still inspect the generated files later and continue working
|
||||
with them manually if needed.
|
||||
|
||||
After the files are written, the wizard returns to the stack management view for
|
||||
that split-services stack. From there you can work with the stack runtime, app
|
||||
selection, and update actions that are currently supported for this topology.
|
||||
|
||||

|
||||
|
||||
For split-services, the most important practical point is that the application
|
||||
side, the data side, and the proxy side remain easy to understand
|
||||
individually. If you come back later to change one part, you should not have to
|
||||
guess where the other parts are defined.
|
||||
|
||||
## A Simple Mental Model
|
||||
|
||||
If this is the first time you use a split setup, this is the easiest way to
|
||||
think about it:
|
||||
|
||||
- `Application Services` are the part you interact with most often
|
||||
- `Data Services` keep the database and Redis available
|
||||
- `Reverse Proxy` is optional and sits in front of the application side
|
||||
|
||||
You do not need to understand every Docker detail before using the wizard.
|
||||
You only need to know which part you want to manage separately.
|
||||
|
||||
## Where To Go Next
|
||||
|
||||
After reading this page, the next useful pages are:
|
||||
|
||||
- [Overview](./01-overview.md)
|
||||
- [Workflows](./02-workflows.md)
|
||||
- [Generated Compose](./04-generated-compose.md)
|
||||
30
docs/10-easy-docker/index.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
title: Easy Docker
|
||||
---
|
||||
|
||||
# Easy Docker
|
||||
|
||||
`easy-docker` is the interactive setup and management workflow for this repository.
|
||||
It is designed to make common Frappe Docker tasks easier from the terminal while
|
||||
keeping the underlying Compose and Bench model visible.
|
||||
|
||||
This section documents the current behavior of the wizard:
|
||||
|
||||
- `single-host` is the supported production workflow today
|
||||
- `split-services` is available for separated stack setup and Compose runtime control
|
||||
- site actions currently remain part of the `single-host` workflow
|
||||
- stack, site, app, and update actions are handled through the wizard
|
||||
- the generated Compose output is available as a rendered snapshot
|
||||
|
||||
Before the wizard opens, `easy-docker` validates its startup dependencies. That
|
||||
includes `gum`, `docker`, `docker compose`, a running Docker daemon, and `jq`.
|
||||
`gum` and `jq` can both use package-manager installation and a pinned GitHub
|
||||
binary fallback. `docker` still must already be present.
|
||||
|
||||
Start here:
|
||||
|
||||
- [Overview](./01-overview.md)
|
||||
- [Workflows](./02-workflows.md)
|
||||
- [Updates](./03-updates.md)
|
||||
- [Generated Compose](./04-generated-compose.md)
|
||||
- [Split Services](./05-split-services.md)
|
||||
BIN
docs/images/easy-docker/entry/main-menu.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
docs/images/easy-docker/single-host/database-engine.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
docs/images/easy-docker/single-host/proxy-mode.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
docs/images/easy-docker/split-services/data-services-choice.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
docs/images/easy-docker/split-services/database-engine.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/images/easy-docker/split-services/manage-stack-actions.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/images/easy-docker/split-services/proxy-choice.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
docs/images/easy-docker/split-services/redis-services.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
docs/images/easy-docker/split-services/summary.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
docs/images/easy-docker/split-services/topology-menu.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
docs/images/easy-docker/stack-creation/apps/app-selection.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
docs/images/easy-docker/stack-creation/apps/app-version.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
docs/images/easy-docker/stack-creation/core/frappe-version.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/images/easy-docker/stack-creation/core/name.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
docs/images/easy-docker/stack-creation/image/image-name.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
docs/images/easy-docker/stack-creation/image/image-version.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
BIN
docs/images/easy-docker/stack-runtime/build-image.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/images/easy-docker/stack-runtime/running-stack.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
docs/images/easy-docker/stack-runtime/start-stack.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
6
easy-docker.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
exec "${ROOT_DIR}/scripts/easy-docker/main.sh" "$@"
|
||||
70
scripts/easy-docker/README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Easy-Frappe-Docker
|
||||
|
||||
Easy installation script for Frappe Docker for development and production
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
bash ./easy-docker.sh
|
||||
```
|
||||
|
||||
Run the entrypoint from a real Bash environment.
|
||||
|
||||
- On Linux, use your normal shell session.
|
||||
- On Windows, use WSL or Git Bash.
|
||||
- If you start `bash` from PowerShell, that usually means WSL, so keep the path
|
||||
in Bash form such as `bash ./easy-docker.sh`, not `bash .\easy-docker.sh`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `gum` is used for the TUI and is installed automatically when possible
|
||||
- `docker` CLI is required and checked on startup
|
||||
- `docker compose` (Compose v2 command) is required and checked on startup
|
||||
- `jq` is required for stack JSON handling and is checked on startup
|
||||
- Docker Desktop includes Compose v2 by default; on Linux Engine-only setups you may need the `docker-compose-plugin` package
|
||||
- Docker daemon must be running before the TUI starts
|
||||
- Required docker commands are validated (`docker ps/exec/inspect/cp` and `docker compose config/up/down/logs/exec/pull/ps`)
|
||||
- Startup validation order is: CLI options, `gum`, `docker`, then `jq`
|
||||
- If package manager installation for `gum` or `jq` fails, the script can use a pinned GitHub binary fallback
|
||||
- The `gum` fallback is pinned to `gum` `v0.17.0` and verifies SHA256 checksums from `scripts/easy-docker/config/gum-checksums.tsv`
|
||||
- The `jq` fallback is pinned to `jq` `1.8.1` and verifies SHA256 checksums from `scripts/easy-docker/config/jq-checksums.tsv`
|
||||
- `docker` still has no installation fallback path and must already be present
|
||||
- Runtime `jq` resolution accepts either `jq` or `jq.exe`, so Windows-native setups with only `jq.exe` on `PATH` are supported
|
||||
|
||||
## JSON Handling
|
||||
|
||||
- `metadata.json` remains the source of truth for stack state
|
||||
- `apps.json` is still generated from stack metadata and still used for the image build
|
||||
- `easy-docker` now reads and writes stack JSON through `jq` instead of line-based `awk` parsing
|
||||
- This is an internal robustness change only; the generated layout of `metadata.json` and `apps.json` is intended to stay the same for users
|
||||
|
||||
## Options
|
||||
|
||||
- `-h`, `--help`
|
||||
- Shows usage and exits without starting the TUI
|
||||
- `--no-installation-fallback`
|
||||
- Disables GitHub binary fallback prompts for `gum` and `jq`
|
||||
- If package manager installation fails, the script exits with manual installation guidance
|
||||
|
||||
## Apps Catalog
|
||||
|
||||
- App options in the wizard are read from:
|
||||
- `scripts/easy-docker/config/apps.tsv`
|
||||
- Format per line:
|
||||
- `id<TAB>label<TAB>repo<TAB>default_branch<TAB>branches_csv`
|
||||
- Example:
|
||||
- `erpnext<TAB>ERPNext<TAB>https://github.com/frappe/erpnext<TAB>version-15<TAB>version-15,version-16,develop`
|
||||
- The install selection in the wizard is limited to apps from this catalog.
|
||||
- For each selected app, the wizard shows the configured branch list from this catalog and prompts branch selection.
|
||||
|
||||
## Frappe Version Profiles
|
||||
|
||||
- During new stack creation (after stack name), the wizard asks for a Frappe branch profile from:
|
||||
- `scripts/easy-docker/config/frappe.tsv`
|
||||
- Format per line:
|
||||
- `id<TAB>label<TAB>frappe_branch`
|
||||
- Example:
|
||||
- `v16<TAB>Frappe v16 (version-16)<TAB>version-16`
|
||||
- The selected `frappe_branch` is saved in stack `metadata.json` and used as default branch suggestion for app branch selection.
|
||||
- In `metadata.json`, this value is stored top-level as:
|
||||
- `"frappe_branch": "version-16"` (example)
|
||||
7
scripts/easy-docker/config/apps.tsv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# id label repo default_branch branches_csv
|
||||
erpnext ERPNext https://github.com/frappe/erpnext develop develop,version-15,version-16
|
||||
crm CRM https://github.com/frappe/crm develop develop,main
|
||||
hrms HRMS https://github.com/frappe/hrms develop develop,version-16,version-15
|
||||
lms LMS https://github.com/frappe/lms develop develop,main
|
||||
helpdesk Helpdesk https://github.com/frappe/helpdesk develop develop,main
|
||||
drive Drive https://github.com/frappe/drive develop develop
|
||||
|
4
scripts/easy-docker/config/frappe.tsv
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# id label frappe_branch
|
||||
v16 Frappe v16 (version-16) version-16
|
||||
v15 Frappe v15 (version-15) version-15
|
||||
develop Frappe develop (develop) develop
|
||||
|
7
scripts/easy-docker/config/gum-checksums.tsv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# version asset_name sha256
|
||||
0.17.0 gum_0.17.0_Darwin_arm64.tar.gz e2a4b8596efa05821d8c58d0c1afbcd7ad1699ba69c689cc3ff23a4a99c8b237
|
||||
0.17.0 gum_0.17.0_Darwin_x86_64.tar.gz cd66576aeebe6cd19c771863c7e8d696e0e1d5387d1e7075666baa67c2052e53
|
||||
0.17.0 gum_0.17.0_Linux_arm64.tar.gz b0b9ed95cbf7c8b7073f17b9591811f5c001e33c7cfd066ca83ce8a07c576f9c
|
||||
0.17.0 gum_0.17.0_Linux_armv7.tar.gz 25711c2fbc6887cde79ed586972834121a04955968808dd688c688381ac50ab2
|
||||
0.17.0 gum_0.17.0_Linux_x86_64.tar.gz 69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb
|
||||
0.17.0 gum_0.17.0_Windows_x86_64.zip b2be80531c6babc8d4e0e6ca95773d58118a2e1582ae006aace08dbc55503072
|
||||
|
10
scripts/easy-docker/config/jq-checksums.tsv
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# version asset_name sha256
|
||||
1.8.1 jq-linux-amd64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
|
||||
1.8.1 jq-linux64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
|
||||
1.8.1 jq-linux-arm64 6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4
|
||||
1.8.1 jq-linux-armhf ac304e50cf7cd24933d83dc7d0e4f79892a71a92fb02336d4ecaffa8933760bd
|
||||
1.8.1 jq-macos-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
|
||||
1.8.1 jq-osx-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
|
||||
1.8.1 jq-macos-arm64 a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603
|
||||
1.8.1 jq-win64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334
|
||||
1.8.1 jq-windows-amd64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334
|
||||
|
38
scripts/easy-docker/lib/app/options.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
print_usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: bash easy-docker.sh [options]
|
||||
|
||||
Options:
|
||||
--no-installation-fallback Disable installation fallback prompt
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
parse_cli_options() {
|
||||
local result_var="${1}"
|
||||
local disable_installation_fallback_value=0
|
||||
shift
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--no-installation-fallback)
|
||||
disable_installation_fallback_value=1
|
||||
;;
|
||||
-h | --help)
|
||||
print_usage
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
print_usage
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
printf -v "${result_var}" "%s" "${disable_installation_fallback_value}"
|
||||
return 0
|
||||
}
|
||||
17
scripts/easy-docker/lib/app/run.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_app_modules() {
|
||||
local app_dir=""
|
||||
app_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common.sh
|
||||
source "${app_dir}/wizard/common.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env.sh
|
||||
source "${app_dir}/wizard/env.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/single_host.sh
|
||||
source "${app_dir}/wizard/single_host.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows.sh
|
||||
source "${app_dir}/wizard/flows.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_app_modules
|
||||
45
scripts/easy-docker/lib/app/screen.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
ALT_SCREEN_ACTIVE=0
|
||||
CURSOR_HIDDEN=0
|
||||
|
||||
stdout_is_terminal() {
|
||||
[ -t 1 ]
|
||||
}
|
||||
|
||||
run_tput_quietly() {
|
||||
tput "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
enter_alt_screen() {
|
||||
if ! stdout_is_terminal || ! command_exists tput; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if run_tput_quietly smcup; then
|
||||
ALT_SCREEN_ACTIVE=1
|
||||
fi
|
||||
|
||||
if run_tput_quietly civis; then
|
||||
CURSOR_HIDDEN=1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
leave_alt_screen() {
|
||||
if command_exists tput; then
|
||||
if [ "${CURSOR_HIDDEN}" = "1" ]; then
|
||||
run_tput_quietly cnorm || true
|
||||
fi
|
||||
|
||||
if [ "${ALT_SCREEN_ACTIVE}" = "1" ]; then
|
||||
run_tput_quietly rmcup || true
|
||||
fi
|
||||
fi
|
||||
|
||||
CURSOR_HIDDEN=0
|
||||
ALT_SCREEN_ACTIVE=0
|
||||
|
||||
return 0
|
||||
}
|
||||
27
scripts/easy-docker/lib/app/wizard/common.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_wizard_common_modules() {
|
||||
local wizard_dir=""
|
||||
wizard_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/constants.sh
|
||||
source "${wizard_dir}/common/constants.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/core.sh
|
||||
source "${wizard_dir}/common/core.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/helpers.sh
|
||||
source "${wizard_dir}/common/helpers.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose.sh
|
||||
source "${wizard_dir}/common/compose.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/frappe.sh
|
||||
source "${wizard_dir}/common/frappe.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps.sh
|
||||
source "${wizard_dir}/common/apps.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site.sh
|
||||
source "${wizard_dir}/common/site.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/ux.sh
|
||||
source "${wizard_dir}/common/ux.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/split_services.sh
|
||||
source "${wizard_dir}/split_services.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_wizard_common_modules
|
||||
15
scripts/easy-docker/lib/app/wizard/common/apps.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_wizard_app_modules() {
|
||||
local apps_dir=""
|
||||
apps_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/apps"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps/catalog.sh
|
||||
source "${apps_dir}/catalog.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps/metadata.sh
|
||||
source "${apps_dir}/metadata.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps/persistence.sh
|
||||
source "${apps_dir}/persistence.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_wizard_app_modules
|
||||
490
scripts/easy-docker/lib/app/wizard/common/apps/catalog.sh
Executable file
|
|
@ -0,0 +1,490 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
trim_predefined_catalog_field() {
|
||||
local result_var="${1}"
|
||||
local value="${2}"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
printf -v "${result_var}" "%s" "${value}"
|
||||
}
|
||||
|
||||
is_valid_predefined_app_id() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
*[!a-z0-9._-]*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
generate_predefined_app_id_from_label() {
|
||||
local result_var="${1}"
|
||||
local app_label="${2}"
|
||||
local generated_id=""
|
||||
|
||||
generated_id="$(
|
||||
printf '%s' "${app_label}" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed -E 's/[[:space:]]+/_/g; s/[^a-z0-9._-]+/_/g; s/_+/_/g; s/^_+//; s/_+$//'
|
||||
)"
|
||||
|
||||
if ! is_valid_predefined_app_id "${generated_id}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${generated_id}"
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_predefined_app_repo() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
https://* | http://* | ssh://* | git://* | git@*:* | file://*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_predefined_app_branch() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
*[!A-Za-z0-9._/-]* | .* | *..* | */ | /*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
csv_contains_branch() {
|
||||
local csv_values="${1}"
|
||||
local value="${2}"
|
||||
|
||||
case ",${csv_values}," in
|
||||
*,"${value}",*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_predefined_branches_csv() {
|
||||
local result_csv_var="${1}"
|
||||
local branches_csv_raw="${2}"
|
||||
local branch_token=""
|
||||
local normalized_csv=""
|
||||
local -a raw_tokens=()
|
||||
|
||||
IFS=',' read -r -a raw_tokens <<<"${branches_csv_raw}"
|
||||
for branch_token in "${raw_tokens[@]}"; do
|
||||
trim_predefined_catalog_field branch_token "${branch_token}"
|
||||
if [ -z "${branch_token}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_valid_predefined_app_branch "${branch_token}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if csv_contains_branch "${normalized_csv}" "${branch_token}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${normalized_csv}" ]; then
|
||||
normalized_csv="${branch_token}"
|
||||
else
|
||||
normalized_csv="${normalized_csv},${branch_token}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${normalized_csv}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_csv_var}" "%s" "${normalized_csv}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_predefined_apps_catalog_path() {
|
||||
local repo_root=""
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
printf '%s/scripts/easy-docker/config/apps.tsv\n' "${repo_root}"
|
||||
}
|
||||
|
||||
get_predefined_apps_catalog_entries() {
|
||||
local catalog_path=""
|
||||
local raw_line=""
|
||||
local line=""
|
||||
local app_id=""
|
||||
local app_label=""
|
||||
local app_repo=""
|
||||
local app_default_branch=""
|
||||
local app_branches_csv=""
|
||||
local normalized_branches_csv=""
|
||||
local first_branch=""
|
||||
local extra=""
|
||||
local seen_ids=","
|
||||
local seen_labels=","
|
||||
|
||||
catalog_path="$(get_predefined_apps_catalog_path)"
|
||||
if [ ! -f "${catalog_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r raw_line || [ -n "${raw_line}" ]; do
|
||||
trim_predefined_catalog_field line "${raw_line}"
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${line}" in
|
||||
\#*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${line}" == *$'\t'* ]]; then
|
||||
IFS=$'\t' read -r app_id app_label app_repo app_default_branch app_branches_csv extra <<<"${line}"
|
||||
else
|
||||
# Backward compatibility for older catalog rows.
|
||||
IFS='|' read -r app_id app_label app_repo app_default_branch app_branches_csv extra <<<"${line}"
|
||||
fi
|
||||
trim_predefined_catalog_field app_id "${app_id}"
|
||||
trim_predefined_catalog_field app_label "${app_label}"
|
||||
trim_predefined_catalog_field app_repo "${app_repo}"
|
||||
trim_predefined_catalog_field app_default_branch "${app_default_branch}"
|
||||
trim_predefined_catalog_field app_branches_csv "${app_branches_csv}"
|
||||
trim_predefined_catalog_field extra "${extra}"
|
||||
|
||||
if [ -n "${extra}" ] || [ -z "${app_id}" ] || [ -z "${app_label}" ] || [ -z "${app_repo}" ] || [ -z "${app_branches_csv}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_predefined_app_id "${app_id}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_predefined_app_repo "${app_repo}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! normalize_predefined_branches_csv normalized_branches_csv "${app_branches_csv}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${app_default_branch}" ]; then
|
||||
first_branch="${normalized_branches_csv%%,*}"
|
||||
app_default_branch="${first_branch}"
|
||||
fi
|
||||
|
||||
if ! is_valid_predefined_app_branch "${app_default_branch}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! csv_contains_branch "${normalized_branches_csv}" "${app_default_branch}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${seen_ids}" in
|
||||
*,"${app_id}",*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "${seen_labels}" in
|
||||
*,"${app_label}",*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
seen_ids="${seen_ids}${app_id},"
|
||||
seen_labels="${seen_labels}${app_label},"
|
||||
|
||||
printf '%s|%s|%s|%s|%s\n' "${app_id}" "${app_label}" "${app_repo}" "${app_default_branch}" "${normalized_branches_csv}"
|
||||
done <"${catalog_path}"
|
||||
}
|
||||
|
||||
parse_predefined_app_catalog_entry() {
|
||||
local entry="${1}"
|
||||
local app_id_var="${2}"
|
||||
local app_label_var="${3}"
|
||||
local app_repo_var="${4}"
|
||||
local app_default_branch_var="${5}"
|
||||
local app_branches_csv_var="${6}"
|
||||
local parsed_app_id=""
|
||||
local parsed_app_label=""
|
||||
local parsed_app_repo=""
|
||||
local parsed_app_default_branch=""
|
||||
local parsed_app_branches_csv=""
|
||||
|
||||
IFS='|' read -r parsed_app_id parsed_app_label parsed_app_repo parsed_app_default_branch parsed_app_branches_csv <<<"${entry}"
|
||||
printf -v "${app_id_var}" "%s" "${parsed_app_id}"
|
||||
printf -v "${app_label_var}" "%s" "${parsed_app_label}"
|
||||
printf -v "${app_repo_var}" "%s" "${parsed_app_repo}"
|
||||
printf -v "${app_default_branch_var}" "%s" "${parsed_app_default_branch}"
|
||||
printf -v "${app_branches_csv_var}" "%s" "${parsed_app_branches_csv}"
|
||||
}
|
||||
|
||||
get_predefined_app_field_by_field() {
|
||||
local lookup_field="${1}"
|
||||
local lookup_value="${2}"
|
||||
local result_field="${3}"
|
||||
local entry=""
|
||||
local app_id=""
|
||||
local app_label=""
|
||||
local app_repo=""
|
||||
local app_default_branch=""
|
||||
local app_branches_csv=""
|
||||
local lookup_candidate=""
|
||||
local result_value=""
|
||||
|
||||
trim_predefined_catalog_field lookup_value "${lookup_value}"
|
||||
if [ -z "${lookup_value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r entry; do
|
||||
if [ -z "${entry}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
parse_predefined_app_catalog_entry "${entry}" app_id app_label app_repo app_default_branch app_branches_csv
|
||||
|
||||
case "${lookup_field}" in
|
||||
id)
|
||||
lookup_candidate="${app_id}"
|
||||
;;
|
||||
label)
|
||||
lookup_candidate="${app_label}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
trim_predefined_catalog_field lookup_candidate "${lookup_candidate}"
|
||||
if [ "${lookup_candidate}" != "${lookup_value}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${result_field}" in
|
||||
id)
|
||||
result_value="${app_id}"
|
||||
;;
|
||||
label)
|
||||
result_value="${app_label}"
|
||||
;;
|
||||
repo)
|
||||
result_value="${app_repo}"
|
||||
;;
|
||||
default_branch)
|
||||
result_value="${app_default_branch}"
|
||||
;;
|
||||
branches_csv)
|
||||
result_value="${app_branches_csv}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "${result_value}"
|
||||
return 0
|
||||
done < <(get_predefined_apps_catalog_entries)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_predefined_app_id_by_label() {
|
||||
local label="${1}"
|
||||
get_predefined_app_field_by_field "label" "${label}" "id"
|
||||
}
|
||||
|
||||
get_predefined_app_repo_by_id() {
|
||||
local app_id_lookup="${1}"
|
||||
get_predefined_app_field_by_field "id" "${app_id_lookup}" "repo"
|
||||
}
|
||||
|
||||
get_predefined_app_label_by_id() {
|
||||
local app_id_lookup="${1}"
|
||||
get_predefined_app_field_by_field "id" "${app_id_lookup}" "label"
|
||||
}
|
||||
|
||||
get_predefined_app_default_branch_by_id() {
|
||||
local app_id_lookup="${1}"
|
||||
get_predefined_app_field_by_field "id" "${app_id_lookup}" "default_branch"
|
||||
}
|
||||
|
||||
get_predefined_app_branch_lines_by_id() {
|
||||
local result_var="${1}"
|
||||
local app_id_lookup="${2}"
|
||||
local app_branches_csv=""
|
||||
local branch=""
|
||||
local branch_lines=""
|
||||
local -a branches=()
|
||||
|
||||
app_branches_csv="$(get_predefined_app_field_by_field "id" "${app_id_lookup}" "branches_csv" || true)"
|
||||
if [ -z "${app_branches_csv}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a branches <<<"${app_branches_csv}"
|
||||
for branch in "${branches[@]}"; do
|
||||
trim_predefined_catalog_field branch "${branch}"
|
||||
if [ -z "${branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ -z "${branch_lines}" ]; then
|
||||
branch_lines="${branch}"
|
||||
else
|
||||
branch_lines="${branch_lines}"$'\n'"${branch}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${branch_lines}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${branch_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
predefined_app_catalog_has_id() {
|
||||
local app_id_lookup="${1}"
|
||||
|
||||
if [ -z "${app_id_lookup}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
get_predefined_app_field_by_field "id" "${app_id_lookup}" "id" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
predefined_app_catalog_has_label() {
|
||||
local app_label_lookup="${1}"
|
||||
|
||||
if [ -z "${app_label_lookup}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
get_predefined_app_field_by_field "label" "${app_label_lookup}" "label" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
append_predefined_app_catalog_entry() {
|
||||
local app_id="${1}"
|
||||
local app_label="${2}"
|
||||
local app_repo="${3}"
|
||||
local app_default_branch="${4}"
|
||||
local app_branches_csv="${5}"
|
||||
local normalized_branches_csv=""
|
||||
local first_branch=""
|
||||
local catalog_path=""
|
||||
local catalog_tmp_path=""
|
||||
local last_char=""
|
||||
|
||||
if ! get_predefined_apps_catalog_entries >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
trim_predefined_catalog_field app_id "${app_id}"
|
||||
trim_predefined_catalog_field app_label "${app_label}"
|
||||
trim_predefined_catalog_field app_repo "${app_repo}"
|
||||
trim_predefined_catalog_field app_default_branch "${app_default_branch}"
|
||||
trim_predefined_catalog_field app_branches_csv "${app_branches_csv}"
|
||||
|
||||
if ! is_valid_predefined_app_id "${app_id}"; then
|
||||
return 1
|
||||
fi
|
||||
if [ -z "${app_label}" ]; then
|
||||
return 1
|
||||
fi
|
||||
if ! is_valid_predefined_app_repo "${app_repo}"; then
|
||||
return 1
|
||||
fi
|
||||
if ! normalize_predefined_branches_csv normalized_branches_csv "${app_branches_csv}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${app_default_branch}" ]; then
|
||||
first_branch="${normalized_branches_csv%%,*}"
|
||||
app_default_branch="${first_branch}"
|
||||
fi
|
||||
if ! is_valid_predefined_app_branch "${app_default_branch}"; then
|
||||
return 1
|
||||
fi
|
||||
if ! csv_contains_branch "${normalized_branches_csv}" "${app_default_branch}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if predefined_app_catalog_has_id "${app_id}"; then
|
||||
return 1
|
||||
fi
|
||||
if predefined_app_catalog_has_label "${app_label}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
catalog_path="$(get_predefined_apps_catalog_path)"
|
||||
catalog_tmp_path="${catalog_path}.tmp"
|
||||
if [ ! -f "${catalog_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! cp -- "${catalog_path}" "${catalog_tmp_path}"; then
|
||||
rm -f -- "${catalog_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -s "${catalog_tmp_path}" ]; then
|
||||
if command_exists tail; then
|
||||
last_char="$(tail -c 1 "${catalog_tmp_path}" 2>/dev/null || true)"
|
||||
if [ -n "${last_char}" ]; then
|
||||
if ! printf '\n' >>"${catalog_tmp_path}"; then
|
||||
rm -f -- "${catalog_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if ! printf '\n' >>"${catalog_tmp_path}"; then
|
||||
rm -f -- "${catalog_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! printf '%s\t%s\t%s\t%s\t%s\n' "${app_id}" "${app_label}" "${app_repo}" "${app_default_branch}" "${normalized_branches_csv}" >>"${catalog_tmp_path}"; then
|
||||
rm -f -- "${catalog_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${catalog_tmp_path}" "${catalog_path}"; then
|
||||
rm -f -- "${catalog_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
294
scripts/easy-docker/lib/app/wizard/common/apps/metadata.sh
Executable file
|
|
@ -0,0 +1,294 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
persist_stack_apps_json_content() {
|
||||
local stack_dir="${1}"
|
||||
local apps_json_content="${2}"
|
||||
local apps_json_path=""
|
||||
local apps_json_tmp_path=""
|
||||
|
||||
apps_json_path="${stack_dir}/apps.json"
|
||||
apps_json_tmp_path="${apps_json_path}.tmp"
|
||||
|
||||
if ! printf '%s\n' "${apps_json_content}" >"${apps_json_tmp_path}"; then
|
||||
rm -f -- "${apps_json_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${apps_json_tmp_path}" "${apps_json_path}"; then
|
||||
rm -f -- "${apps_json_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_csv() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
easy_docker_run_jq -r '(.apps.predefined // []) | join(",")' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_custom_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
easy_docker_run_jq -r '(.apps.custom // [])[]? | select(has("repo") and has("branch")) | "\(.repo)|\(.branch)"' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
easy_docker_run_jq -r '(.apps.predefined_branches // {}) | to_entries[]? | "\(.key)|\(.value)"' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_for_id() {
|
||||
local metadata_path="${1}"
|
||||
local app_id_lookup="${2}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
easy_docker_run_jq -r --arg app_id "${app_id_lookup}" '.apps.predefined_branches[$app_id] // empty' "${metadata_path}"
|
||||
}
|
||||
|
||||
build_metadata_apps_json_object() {
|
||||
local result_var="${1}"
|
||||
local predefined_csv="${2}"
|
||||
local branch_lines="${3}"
|
||||
local custom_apps_lines="${4:-}"
|
||||
local app_id=""
|
||||
local app_branch=""
|
||||
local custom_repo=""
|
||||
local custom_branch=""
|
||||
local predefined_json_entries=""
|
||||
local branch_json_entries=""
|
||||
local custom_json_entries=""
|
||||
local escaped_app_id=""
|
||||
local escaped_branch=""
|
||||
local escaped_repo=""
|
||||
local entry_json=""
|
||||
local line=""
|
||||
local -a predefined_ids=()
|
||||
|
||||
if [ -n "${predefined_csv}" ]; then
|
||||
IFS=',' read -r -a predefined_ids <<<"${predefined_csv}"
|
||||
for app_id in "${predefined_ids[@]}"; do
|
||||
if [ -z "${app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_app_id="$(json_escape_string "${app_id}")"
|
||||
entry_json="$(printf ' "%s"' "${escaped_app_id}")"
|
||||
if [ -z "${predefined_json_entries}" ]; then
|
||||
predefined_json_entries="${entry_json}"
|
||||
else
|
||||
predefined_json_entries="${predefined_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
app_id="${line%%|*}"
|
||||
app_branch="${line#*|}"
|
||||
if [ -z "${app_id}" ] || [ -z "${app_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_app_id="$(json_escape_string "${app_id}")"
|
||||
escaped_branch="$(json_escape_string "${app_branch}")"
|
||||
entry_json="$(printf ' "%s": "%s"' "${escaped_app_id}" "${escaped_branch}")"
|
||||
if [ -z "${branch_json_entries}" ]; then
|
||||
branch_json_entries="${entry_json}"
|
||||
else
|
||||
branch_json_entries="${branch_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done <<EOF
|
||||
${branch_lines}
|
||||
EOF
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
custom_repo="${line%%|*}"
|
||||
custom_branch="${line#*|}"
|
||||
if [ -z "${custom_repo}" ] || [ -z "${custom_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_repo="$(json_escape_string "${custom_repo}")"
|
||||
escaped_branch="$(json_escape_string "${custom_branch}")"
|
||||
entry_json="$(printf ' {\n "repo": "%s",\n "branch": "%s"\n }' "${escaped_repo}" "${escaped_branch}")"
|
||||
if [ -z "${custom_json_entries}" ]; then
|
||||
custom_json_entries="${entry_json}"
|
||||
else
|
||||
custom_json_entries="${custom_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done <<EOF
|
||||
${custom_apps_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" '{\n "predefined": [\n%s\n ],\n "predefined_branches": {\n%s\n },\n "custom": [\n%s\n ]\n }' "${predefined_json_entries}" "${branch_json_entries}" "${custom_json_entries}"
|
||||
}
|
||||
|
||||
render_metadata_apps_json_object_from_metadata() {
|
||||
local result_var="${1}"
|
||||
local metadata_path="${2}"
|
||||
local predefined_csv=""
|
||||
local branch_lines=""
|
||||
local custom_lines=""
|
||||
local apps_json_object=""
|
||||
|
||||
predefined_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
|
||||
branch_lines="$(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)"
|
||||
custom_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
|
||||
build_metadata_apps_json_object apps_json_object "${predefined_csv}" "${branch_lines}" "${custom_lines}"
|
||||
printf -v "${result_var}" "%s" "${apps_json_object}"
|
||||
}
|
||||
|
||||
build_stack_apps_json_content_from_metadata_apps() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local preset_apps_csv=""
|
||||
local custom_apps_lines=""
|
||||
local predefined_branch=""
|
||||
local preset_branch=""
|
||||
local catalog_default_branch=""
|
||||
local app=""
|
||||
local line=""
|
||||
local repo=""
|
||||
local branch=""
|
||||
local url=""
|
||||
local escaped_url=""
|
||||
local escaped_branch=""
|
||||
local entry_json=""
|
||||
local entries_json=""
|
||||
local -a preset_apps=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
preset_apps_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
|
||||
custom_apps_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
|
||||
preset_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
|
||||
if [ -z "${preset_branch}" ]; then
|
||||
preset_branch="$(get_default_frappe_branch)"
|
||||
fi
|
||||
|
||||
if [ -n "${preset_apps_csv}" ]; then
|
||||
IFS=',' read -r -a preset_apps <<<"${preset_apps_csv}"
|
||||
for app in "${preset_apps[@]}"; do
|
||||
url="$(get_predefined_app_repo_by_id "${app}" || true)"
|
||||
if [ -z "${url}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
predefined_branch="$(get_metadata_apps_predefined_branch_for_id "${metadata_path}" "${app}" || true)"
|
||||
|
||||
if [ -z "${predefined_branch}" ]; then
|
||||
catalog_default_branch="$(get_predefined_app_default_branch_by_id "${app}" || true)"
|
||||
if [ -n "${catalog_default_branch}" ]; then
|
||||
predefined_branch="${catalog_default_branch}"
|
||||
else
|
||||
predefined_branch="${preset_branch}"
|
||||
fi
|
||||
fi
|
||||
|
||||
escaped_url="$(json_escape_string "${url}")"
|
||||
escaped_branch="$(json_escape_string "${predefined_branch}")"
|
||||
entry_json="$(printf ' {"url": "%s", "branch": "%s"}' "${escaped_url}" "${escaped_branch}")"
|
||||
if [ -z "${entries_json}" ]; then
|
||||
entries_json="${entry_json}"
|
||||
else
|
||||
entries_json="${entries_json}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
repo="${line%%|*}"
|
||||
branch="${line#*|}"
|
||||
if [ -z "${repo}" ] || [ -z "${branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_url="$(json_escape_string "${repo}")"
|
||||
escaped_branch="$(json_escape_string "${branch}")"
|
||||
entry_json="$(printf ' {"url": "%s", "branch": "%s"}' "${escaped_url}" "${escaped_branch}")"
|
||||
if [ -z "${entries_json}" ]; then
|
||||
entries_json="${entry_json}"
|
||||
else
|
||||
entries_json="${entries_json}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done <<EOF
|
||||
${custom_apps_lines}
|
||||
EOF
|
||||
|
||||
if [ -z "${entries_json}" ]; then
|
||||
printf -v "${result_var}" "[\n]\n"
|
||||
else
|
||||
printf -v "${result_var}" "[\n%s\n]\n" "${entries_json}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
persist_stack_apps_json_from_metadata_apps() {
|
||||
local stack_dir="${1}"
|
||||
local apps_json_content=""
|
||||
|
||||
if ! build_stack_apps_json_content_from_metadata_apps apps_json_content "${stack_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_apps_json_content "${stack_dir}" "${apps_json_content}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
193
scripts/easy-docker/lib/app/wizard/common/apps/persistence.sh
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
render_stack_metadata_top_level_entry_from_json_file() {
|
||||
local metadata_path="${1}"
|
||||
local metadata_key="${2}"
|
||||
local entry_value=""
|
||||
local rendered_entry=""
|
||||
local line=""
|
||||
local delta=0
|
||||
local depth=0
|
||||
local started=0
|
||||
|
||||
while IFS= read -r line || [ -n "${line}" ]; do
|
||||
if [ "${started}" -eq 0 ]; then
|
||||
case "${line}" in
|
||||
" \"${metadata_key}\":"*)
|
||||
entry_value="${line# \""${metadata_key}"\": }"
|
||||
entry_value="${entry_value%,}"
|
||||
if [[ "${entry_value}" == \{* || "${entry_value}" == \[* ]]; then
|
||||
rendered_entry=" \"${metadata_key}\": ${entry_value}"
|
||||
started=1
|
||||
delta="$(count_stack_metadata_json_structure_delta "${entry_value}")"
|
||||
depth=$((depth + delta))
|
||||
if [ "${depth}" -le 0 ]; then
|
||||
printf '%s' "${rendered_entry}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
printf ' "%s": %s' "${metadata_key}" "${entry_value}"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
delta="$(count_stack_metadata_json_structure_delta "${line}")"
|
||||
if [ $((depth + delta)) -le 0 ]; then
|
||||
rendered_entry="${rendered_entry}"$'\n'"${line%,}"
|
||||
printf '%s' "${rendered_entry}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rendered_entry="${rendered_entry}"$'\n'"${line}"
|
||||
depth=$((depth + delta))
|
||||
done <"${metadata_path}"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
count_stack_metadata_json_structure_delta() {
|
||||
local line="${1}"
|
||||
local opens=0
|
||||
local closes=0
|
||||
local matches=""
|
||||
|
||||
matches="${line//[^\{]/}"
|
||||
opens=$((opens + ${#matches}))
|
||||
matches="${line//[^\[]/}"
|
||||
opens=$((opens + ${#matches}))
|
||||
matches="${line//[^\}]/}"
|
||||
closes=$((closes + ${#matches}))
|
||||
matches="${line//[^\]]/}"
|
||||
closes=$((closes + ${#matches}))
|
||||
|
||||
printf '%s\n' "$((opens - closes))"
|
||||
}
|
||||
|
||||
build_stack_metadata_top_level_object_content() {
|
||||
local result_var="${1}"
|
||||
local metadata_path="${2}"
|
||||
local object_key="${3}"
|
||||
local object_json="${4}"
|
||||
shift 4
|
||||
local rendered_metadata=""
|
||||
local entry_json=""
|
||||
local metadata_key=""
|
||||
local index=0
|
||||
local total_keys=0
|
||||
local -a ordered_keys=("$@")
|
||||
|
||||
total_keys="${#ordered_keys[@]}"
|
||||
rendered_metadata="{"
|
||||
if [ "${total_keys}" -gt 0 ]; then
|
||||
rendered_metadata="${rendered_metadata}"$'\n'
|
||||
fi
|
||||
|
||||
for index in "${!ordered_keys[@]}"; do
|
||||
metadata_key="${ordered_keys[${index}]}"
|
||||
if [ "${metadata_key}" = "${object_key}" ]; then
|
||||
entry_json="$(printf ' "%s": %s' "${metadata_key}" "${object_json}")"
|
||||
else
|
||||
entry_json="$(render_stack_metadata_top_level_entry_from_json_file "${metadata_path}" "${metadata_key}")" || return 1
|
||||
fi
|
||||
|
||||
rendered_metadata="${rendered_metadata}${entry_json}"
|
||||
if [ "${index}" -lt $((total_keys - 1)) ]; then
|
||||
rendered_metadata="${rendered_metadata},"
|
||||
fi
|
||||
rendered_metadata="${rendered_metadata}"$'\n'
|
||||
done
|
||||
|
||||
rendered_metadata="${rendered_metadata}}"$'\n'
|
||||
if ! printf '%s' "${rendered_metadata}" | easy_docker_run_jq -e 'type == "object"' >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${rendered_metadata}"
|
||||
}
|
||||
|
||||
persist_stack_metadata_top_level_object() {
|
||||
local stack_dir="${1}"
|
||||
local object_key="${2}"
|
||||
local object_json="${3}"
|
||||
local insert_before_key="${4:-}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
local metadata_content=""
|
||||
local existing_key=""
|
||||
local inserted=0
|
||||
local -a existing_keys=()
|
||||
local -a ordered_keys=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${object_json}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_run_jq -e 'type == "object"' "${metadata_path}" >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
object_json="${object_json%$'\n'}"
|
||||
if ! printf '%s\n' "${object_json}" | easy_docker_run_jq -e 'type == "object"' >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
mapfile -t existing_keys < <(easy_docker_run_jq -r 'keys_unsorted[]' "${metadata_path}") || return 1
|
||||
|
||||
for existing_key in "${existing_keys[@]}"; do
|
||||
if [ "${existing_key}" = "${object_key}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${inserted}" -eq 0 ] && [ -n "${insert_before_key}" ] && [ "${existing_key}" = "${insert_before_key}" ]; then
|
||||
ordered_keys+=("${object_key}")
|
||||
inserted=1
|
||||
fi
|
||||
|
||||
ordered_keys+=("${existing_key}")
|
||||
done
|
||||
|
||||
if [ "${inserted}" -eq 0 ]; then
|
||||
ordered_keys+=("${object_key}")
|
||||
fi
|
||||
|
||||
build_stack_metadata_top_level_object_content metadata_content "${metadata_path}" "${object_key}" "${object_json}" "${ordered_keys[@]}" || return 1
|
||||
|
||||
if ! printf '%s' "${metadata_content}" >"${metadata_tmp_path}"; then
|
||||
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${metadata_tmp_path}" "${metadata_path}"; then
|
||||
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
persist_stack_metadata_apps_object() {
|
||||
local stack_dir="${1}"
|
||||
local apps_json_object="${2}"
|
||||
|
||||
persist_stack_metadata_top_level_object "${stack_dir}" "apps" "${apps_json_object}" "wizard"
|
||||
}
|
||||
|
||||
persist_stack_metadata_wizard_object() {
|
||||
local stack_dir="${1}"
|
||||
local wizard_json_object="${2}"
|
||||
|
||||
persist_stack_metadata_top_level_object "${stack_dir}" "wizard" "${wizard_json_object}"
|
||||
}
|
||||
19
scripts/easy-docker/lib/app/wizard/common/compose.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
EASY_DOCKER_BUILD_ERROR_DETAIL=""
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
load_easy_docker_compose_modules() {
|
||||
local compose_dir=""
|
||||
compose_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/compose"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/render.sh
|
||||
source "${compose_dir}/render.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start.sh
|
||||
source "${compose_dir}/start.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/build.sh
|
||||
source "${compose_dir}/build.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_compose_modules
|
||||
108
scripts/easy-docker/lib/app/wizard/common/compose/build.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
build_stack_custom_image() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local apps_json_path=""
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
local frappe_branch=""
|
||||
local frappe_path="https://github.com/frappe/frappe"
|
||||
local repo_root=""
|
||||
local containerfile_path=""
|
||||
local apps_refs_lines=""
|
||||
local app_ref_line=""
|
||||
local app_url=""
|
||||
local app_branch=""
|
||||
local git_error=""
|
||||
local image_ref=""
|
||||
|
||||
EASY_DOCKER_BUILD_ERROR_DETAIL=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
apps_json_path="${stack_dir}/apps.json"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 11
|
||||
fi
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 12
|
||||
fi
|
||||
if ! easy_docker_require_jq; then
|
||||
return 25
|
||||
fi
|
||||
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
|
||||
frappe_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
|
||||
if [ -z "${custom_image}" ]; then
|
||||
return 13
|
||||
fi
|
||||
if [ -z "${custom_tag}" ]; then
|
||||
return 14
|
||||
fi
|
||||
if [ -z "${frappe_branch}" ]; then
|
||||
return 15
|
||||
fi
|
||||
|
||||
# Keep apps.json aligned with current metadata app selection before build.
|
||||
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
return 16
|
||||
fi
|
||||
if [ ! -f "${apps_json_path}" ]; then
|
||||
return 17
|
||||
fi
|
||||
|
||||
if ! command_exists git; then
|
||||
return 22
|
||||
fi
|
||||
|
||||
apps_refs_lines="$(
|
||||
easy_docker_run_jq -r '.[]? | select((.url // "") != "" and (.branch // "") != "") | "\(.url)|\(.branch)"' "${apps_json_path}"
|
||||
)" || return 23
|
||||
if [ -z "${apps_refs_lines}" ]; then
|
||||
return 23
|
||||
fi
|
||||
|
||||
while IFS= read -r app_ref_line; do
|
||||
if [ -z "${app_ref_line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
app_url="${app_ref_line%%|*}"
|
||||
app_branch="${app_ref_line#*|}"
|
||||
if [ -z "${app_url}" ] || [ -z "${app_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if git_error="$(git ls-remote --exit-code --heads "${app_url}" "${app_branch}" 2>&1)"; then
|
||||
:
|
||||
else
|
||||
# shellcheck disable=SC2034 # Read by manage flow after build_stack_custom_image returns 24.
|
||||
EASY_DOCKER_BUILD_ERROR_DETAIL="$(printf '%s@%s :: %s' "${app_url}" "${app_branch}" "${git_error}")"
|
||||
return 24
|
||||
fi
|
||||
done <<EOF
|
||||
${apps_refs_lines}
|
||||
EOF
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
containerfile_path="${repo_root}/images/layered/Containerfile"
|
||||
if [ ! -f "${containerfile_path}" ]; then
|
||||
return 20
|
||||
fi
|
||||
|
||||
image_ref="${custom_image}:${custom_tag}"
|
||||
|
||||
docker build \
|
||||
-f "${containerfile_path}" \
|
||||
--build-arg "FRAPPE_BRANCH=${frappe_branch}" \
|
||||
--build-arg "FRAPPE_PATH=${frappe_path}" \
|
||||
--secret "id=apps_json,src=${apps_json_path}" \
|
||||
-t "${image_ref}" \
|
||||
"${repo_root}" || return 21
|
||||
|
||||
return 0
|
||||
}
|
||||
78
scripts/easy-docker/lib/app/wizard/common/compose/render.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
render_stack_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local generated_compose_path=""
|
||||
local generated_compose_tmp_path=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local compose_project_name=""
|
||||
local repo_root=""
|
||||
local -a compose_args=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
generated_compose_tmp_path="${generated_compose_path}.tmp"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
|
||||
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
|
||||
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${generated_compose_tmp_path}" "${generated_compose_path}"; then
|
||||
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
193
scripts/easy-docker/lib/app/wizard/common/compose/runtime/lifecycle.sh
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
start_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_project_name=""
|
||||
local fallback_erpnext_version=""
|
||||
local configured_pull_policy=""
|
||||
local runtime_pull_policy=""
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
local image_ref=""
|
||||
local image_inspect_error=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
easy_docker_compose_init_context "${stack_dir}" metadata_path env_path compose_project_name
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 31
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 32
|
||||
fi
|
||||
|
||||
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 33 34; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
easy_docker_compose_get_fallback_erpnext_version fallback_erpnext_version "${env_path}"
|
||||
|
||||
configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)"
|
||||
if [ -z "${configured_pull_policy}" ]; then
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
|
||||
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
|
||||
image_ref="${custom_image}:${custom_tag}"
|
||||
if image_inspect_error="$(docker image inspect "${image_ref}" 2>&1 >/dev/null)"; then
|
||||
runtime_pull_policy="if_not_present"
|
||||
else
|
||||
case "${image_inspect_error}" in
|
||||
*"No such image"* | *"No such object"*)
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 38.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_ref}"
|
||||
return 38
|
||||
;;
|
||||
*)
|
||||
if [ -z "${image_inspect_error}" ]; then
|
||||
image_inspect_error="docker image inspect failed for ${image_ref}"
|
||||
fi
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 39.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_inspect_error}"
|
||||
return 39
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! easy_docker_compose_collect_args compose_args "${metadata_path}" 35 36; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ] && [ -n "${runtime_pull_policy}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif [ -n "${runtime_pull_policy}" ]; then
|
||||
if ! PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
stop_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_project_name=""
|
||||
local fallback_erpnext_version=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
easy_docker_compose_init_context "${stack_dir}" metadata_path env_path compose_project_name
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 41
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 42
|
||||
fi
|
||||
|
||||
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 43 44; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
easy_docker_compose_get_fallback_erpnext_version fallback_erpnext_version "${env_path}"
|
||||
|
||||
if ! easy_docker_compose_collect_args compose_args "${metadata_path}" 45 46; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" stop; then
|
||||
return 47
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" stop; then
|
||||
return 47
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
delete_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_project_name=""
|
||||
local fallback_erpnext_version=""
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
local custom_image_ref=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
easy_docker_compose_init_context "${stack_dir}" metadata_path env_path compose_project_name
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 48
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 49
|
||||
fi
|
||||
|
||||
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 50 51; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
easy_docker_compose_get_fallback_erpnext_version fallback_erpnext_version "${env_path}"
|
||||
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
|
||||
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
|
||||
custom_image_ref="${custom_image}:${custom_tag}"
|
||||
fi
|
||||
|
||||
if ! easy_docker_compose_collect_args compose_args "${metadata_path}" 52 53; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
if [ -n "${custom_image_ref}" ]; then
|
||||
if docker image inspect "${custom_image_ref}" >/dev/null 2>&1; then
|
||||
if ! docker image rm "${custom_image_ref}"; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${custom_image_ref}"
|
||||
return 55
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! rollback_stack_directory "${stack_dir}"; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_stack_with_compose_from_metadata returns 56.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_dir}"
|
||||
return 56
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
100
scripts/easy-docker/lib/app/wizard/common/compose/runtime/shared.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
easy_docker_compose_init_context() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_var="${2}"
|
||||
local env_var="${3}"
|
||||
local project_var="${4}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_project_name=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
printf -v "${metadata_var}" "%s" "${metadata_path}"
|
||||
printf -v "${env_var}" "%s" "${env_path}"
|
||||
printf -v "${project_var}" "%s" "${compose_project_name}"
|
||||
}
|
||||
|
||||
easy_docker_compose_get_fallback_erpnext_version() {
|
||||
local result_var="${1}"
|
||||
local env_path="${2}"
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${fallback_erpnext_version}"
|
||||
}
|
||||
|
||||
easy_docker_compose_require_supported_topology() {
|
||||
local stack_dir="${1}"
|
||||
local missing_topology_code="${2}"
|
||||
local unsupported_topology_code="${3}"
|
||||
local stack_topology=""
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by callers after topology resolution fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
|
||||
return "${missing_topology_code}"
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
# shellcheck disable=SC2034 # Read by callers after unsupported topology is returned.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
|
||||
return "${unsupported_topology_code}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
easy_docker_compose_collect_args() {
|
||||
local result_array_name="${1}"
|
||||
local metadata_path="${2}"
|
||||
local missing_compose_code="${3}"
|
||||
local missing_file_code="${4}"
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local repo_root=""
|
||||
local -n compose_args_ref="${result_array_name}"
|
||||
|
||||
compose_args_ref=()
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return "${missing_compose_code}"
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by callers after compose file resolution fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
|
||||
return "${missing_file_code}"
|
||||
fi
|
||||
|
||||
compose_args_ref+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args_ref[@]}" -eq 0 ]; then
|
||||
return "${missing_compose_code}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
233
scripts/easy-docker/lib/app/wizard/common/compose/runtime/status.sh
Executable file
|
|
@ -0,0 +1,233 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_stack_compose_runtime_status_label() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local stack_topology=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local fallback_erpnext_version=""
|
||||
local container_ids_lines=""
|
||||
local container_id=""
|
||||
local container_status_lines=""
|
||||
local container_status_line=""
|
||||
local container_state=""
|
||||
local container_status_text=""
|
||||
local first_running_status=""
|
||||
local running_status_excerpt=""
|
||||
local running_status_varies=0
|
||||
local compose_status=0
|
||||
local total_containers_count=0
|
||||
local running_containers_count=0
|
||||
local exited_containers_count=0
|
||||
local created_containers_count=0
|
||||
local restarting_containers_count=0
|
||||
local paused_containers_count=0
|
||||
local dead_containers_count=0
|
||||
local other_containers_count=0
|
||||
local compose_project_name=""
|
||||
local repo_root=""
|
||||
local status_label=""
|
||||
local -a compose_args=()
|
||||
local -a docker_ps_args=()
|
||||
|
||||
easy_docker_compose_init_context "${stack_dir}" metadata_path env_path compose_project_name
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (metadata missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (topology missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services") ;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "Unsupported (${stack_topology})"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (env missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
easy_docker_compose_get_fallback_erpnext_version fallback_erpnext_version "${env_path}"
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (missing file: ${compose_file})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
container_ids_lines="$(
|
||||
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null
|
||||
)"
|
||||
compose_status=$?
|
||||
else
|
||||
container_ids_lines="$(
|
||||
docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null
|
||||
)"
|
||||
compose_status=$?
|
||||
fi
|
||||
|
||||
if [ "${compose_status}" -ne 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (docker compose status failed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${container_ids_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "Not created"
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker_ps_args=(-a --no-trunc --format "{{.ID}}|{{.State}}|{{.Status}}")
|
||||
while IFS= read -r container_id; do
|
||||
if [ -n "${container_id}" ]; then
|
||||
docker_ps_args+=(--filter "id=${container_id}")
|
||||
fi
|
||||
done <<EOF
|
||||
${container_ids_lines}
|
||||
EOF
|
||||
|
||||
container_status_lines="$(docker ps "${docker_ps_args[@]}" 2>/dev/null)"
|
||||
compose_status=$?
|
||||
if [ "${compose_status}" -ne 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (docker ps status failed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r container_status_line; do
|
||||
if [ -z "${container_status_line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
total_containers_count=$((total_containers_count + 1))
|
||||
IFS='|' read -r container_id container_state container_status_text <<EOF
|
||||
${container_status_line}
|
||||
EOF
|
||||
|
||||
case "${container_state}" in
|
||||
running)
|
||||
running_containers_count=$((running_containers_count + 1))
|
||||
if [ -z "${first_running_status}" ]; then
|
||||
first_running_status="${container_status_text}"
|
||||
elif [ "${container_status_text}" != "${first_running_status}" ]; then
|
||||
running_status_varies=1
|
||||
fi
|
||||
;;
|
||||
exited)
|
||||
exited_containers_count=$((exited_containers_count + 1))
|
||||
;;
|
||||
created)
|
||||
created_containers_count=$((created_containers_count + 1))
|
||||
;;
|
||||
restarting)
|
||||
restarting_containers_count=$((restarting_containers_count + 1))
|
||||
;;
|
||||
paused)
|
||||
paused_containers_count=$((paused_containers_count + 1))
|
||||
;;
|
||||
dead)
|
||||
dead_containers_count=$((dead_containers_count + 1))
|
||||
;;
|
||||
*)
|
||||
other_containers_count=$((other_containers_count + 1))
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
${container_status_lines}
|
||||
EOF
|
||||
|
||||
if [ "${total_containers_count}" -eq 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Not created"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${first_running_status}" ]; then
|
||||
case "${first_running_status}" in
|
||||
Up\ *)
|
||||
running_status_excerpt="${first_running_status#Up }"
|
||||
;;
|
||||
*)
|
||||
running_status_excerpt="${first_running_status}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "${running_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Running (${running_containers_count}/${total_containers_count} containers"
|
||||
if [ -n "${running_status_excerpt}" ]; then
|
||||
status_label="${status_label}, up ${running_status_excerpt}"
|
||||
if [ "${running_status_varies}" -eq 1 ]; then
|
||||
status_label="${status_label}+"
|
||||
fi
|
||||
fi
|
||||
status_label="${status_label})"
|
||||
elif [ "${running_containers_count}" -gt 0 ]; then
|
||||
status_label="Partial (${running_containers_count}/${total_containers_count} running"
|
||||
if [ "${restarting_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${restarting_containers_count} restarting"
|
||||
elif [ "${paused_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${paused_containers_count} paused"
|
||||
elif [ "${exited_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${exited_containers_count} stopped"
|
||||
elif [ "${created_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${created_containers_count} created"
|
||||
elif [ "${dead_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${dead_containers_count} dead"
|
||||
elif [ "${other_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${other_containers_count} other"
|
||||
fi
|
||||
|
||||
if [ -n "${running_status_excerpt}" ]; then
|
||||
status_label="${status_label}, up ${running_status_excerpt}"
|
||||
if [ "${running_status_varies}" -eq 1 ]; then
|
||||
status_label="${status_label}+"
|
||||
fi
|
||||
fi
|
||||
status_label="${status_label})"
|
||||
elif [ "${restarting_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Restarting (${total_containers_count} containers)"
|
||||
elif [ "${paused_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Paused (${total_containers_count} containers)"
|
||||
elif [ "${created_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Created (${total_containers_count} containers)"
|
||||
else
|
||||
status_label="Stopped (${total_containers_count} containers)"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${status_label}"
|
||||
return 0
|
||||
}
|
||||
19
scripts/easy-docker/lib/app/wizard/common/compose/start.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_compose_lifecycle_modules() {
|
||||
local lifecycle_dir=""
|
||||
lifecycle_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/start"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/start.sh
|
||||
source "${lifecycle_dir}/start.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/stop.sh
|
||||
source "${lifecycle_dir}/stop.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/restart.sh
|
||||
source "${lifecycle_dir}/restart.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/delete.sh
|
||||
source "${lifecycle_dir}/delete.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/status.sh
|
||||
source "${lifecycle_dir}/status.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_compose_lifecycle_modules
|
||||
132
scripts/easy-docker/lib/app/wizard/common/compose/start/delete.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
stack_directory_contains_only_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path="${2}"
|
||||
local remaining_entry=""
|
||||
|
||||
remaining_entry="$(
|
||||
find "${stack_dir}" -mindepth 1 ! -path "${metadata_path}" -print -quit 2>/dev/null || true
|
||||
)"
|
||||
if [ -n "${remaining_entry}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
delete_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local compose_project_name=""
|
||||
local stack_topology=""
|
||||
local repo_root=""
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
local custom_image_ref=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 48
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
if stack_directory_contains_only_metadata "${stack_dir}" "${metadata_path}"; then
|
||||
if ! rollback_stack_directory "${stack_dir}"; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_dir}"
|
||||
return 56
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 49
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
|
||||
return 50
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services") ;;
|
||||
*)
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
|
||||
return 51
|
||||
;;
|
||||
esac
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
|
||||
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
|
||||
custom_image_ref="${custom_image}:${custom_tag}"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
|
||||
return 53
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
if [ -n "${custom_image_ref}" ]; then
|
||||
if docker image inspect "${custom_image_ref}" >/dev/null 2>&1; then
|
||||
if ! docker image rm "${custom_image_ref}"; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${custom_image_ref}"
|
||||
return 55
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! rollback_stack_directory "${stack_dir}"; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_stack_with_compose_from_metadata returns 56.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_dir}"
|
||||
return 56
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
74
scripts/easy-docker/lib/app/wizard/common/compose/start/restart.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
restart_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local stop_status=0
|
||||
local start_status=0
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after restart_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
if stop_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
stop_status=$?
|
||||
case "${stop_status}" in
|
||||
41)
|
||||
return 57
|
||||
;;
|
||||
42)
|
||||
return 58
|
||||
;;
|
||||
43)
|
||||
return 59
|
||||
;;
|
||||
44)
|
||||
return 60
|
||||
;;
|
||||
45)
|
||||
return 61
|
||||
;;
|
||||
46)
|
||||
return 62
|
||||
;;
|
||||
*)
|
||||
return 63
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if start_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
start_status=$?
|
||||
case "${start_status}" in
|
||||
31)
|
||||
return 57
|
||||
;;
|
||||
32)
|
||||
return 58
|
||||
;;
|
||||
33)
|
||||
return 59
|
||||
;;
|
||||
34)
|
||||
return 60
|
||||
;;
|
||||
35)
|
||||
return 61
|
||||
;;
|
||||
36)
|
||||
return 62
|
||||
;;
|
||||
38)
|
||||
return 64
|
||||
;;
|
||||
39)
|
||||
return 65
|
||||
;;
|
||||
*)
|
||||
return 63
|
||||
;;
|
||||
esac
|
||||
}
|
||||
131
scripts/easy-docker/lib/app/wizard/common/compose/start/start.sh
Executable file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
start_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local configured_pull_policy=""
|
||||
local runtime_pull_policy=""
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
local image_ref=""
|
||||
local image_inspect_error=""
|
||||
local compose_project_name=""
|
||||
local stack_topology=""
|
||||
local repo_root=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 31
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 32
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 33.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
|
||||
return 33
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services") ;;
|
||||
*)
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
|
||||
return 34
|
||||
;;
|
||||
esac
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)"
|
||||
if [ -z "${configured_pull_policy}" ]; then
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
|
||||
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
|
||||
image_ref="${custom_image}:${custom_tag}"
|
||||
if image_inspect_error="$(docker image inspect "${image_ref}" 2>&1 >/dev/null)"; then
|
||||
runtime_pull_policy="if_not_present"
|
||||
else
|
||||
case "${image_inspect_error}" in
|
||||
*"No such image"* | *"No such object"*)
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 38.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_ref}"
|
||||
return 38
|
||||
;;
|
||||
*)
|
||||
if [ -z "${image_inspect_error}" ]; then
|
||||
image_inspect_error="docker image inspect failed for ${image_ref}"
|
||||
fi
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 39.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_inspect_error}"
|
||||
return 39
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 35
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 36.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
|
||||
return 36
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 35
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ] && [ -n "${runtime_pull_policy}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif [ -n "${runtime_pull_policy}" ]; then
|
||||
if ! PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then
|
||||
return 37
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
239
scripts/easy-docker/lib/app/wizard/common/compose/start/status.sh
Executable file
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_stack_compose_runtime_status_label() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local stack_topology=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local container_ids_lines=""
|
||||
local container_id=""
|
||||
local container_status_lines=""
|
||||
local container_status_line=""
|
||||
local container_state=""
|
||||
local container_status_text=""
|
||||
local first_running_status=""
|
||||
local running_status_excerpt=""
|
||||
local running_status_varies=0
|
||||
local compose_status=0
|
||||
local total_containers_count=0
|
||||
local running_containers_count=0
|
||||
local exited_containers_count=0
|
||||
local created_containers_count=0
|
||||
local restarting_containers_count=0
|
||||
local paused_containers_count=0
|
||||
local dead_containers_count=0
|
||||
local other_containers_count=0
|
||||
local compose_project_name=""
|
||||
local repo_root=""
|
||||
local status_label=""
|
||||
local -a compose_args=()
|
||||
local -a docker_ps_args=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (metadata missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (topology missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services") ;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "Unsupported (${stack_topology})"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (env missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (missing file: ${compose_file})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
container_ids_lines="$(
|
||||
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null
|
||||
)"
|
||||
compose_status=$?
|
||||
else
|
||||
container_ids_lines="$(
|
||||
docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null
|
||||
)"
|
||||
compose_status=$?
|
||||
fi
|
||||
|
||||
if [ "${compose_status}" -ne 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (docker compose status failed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${container_ids_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "Not created"
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker_ps_args=(-a --no-trunc --format "{{.ID}}|{{.State}}|{{.Status}}")
|
||||
while IFS= read -r container_id; do
|
||||
if [ -n "${container_id}" ]; then
|
||||
docker_ps_args+=(--filter "id=${container_id}")
|
||||
fi
|
||||
done <<EOF
|
||||
${container_ids_lines}
|
||||
EOF
|
||||
|
||||
container_status_lines="$(docker ps "${docker_ps_args[@]}" 2>/dev/null)"
|
||||
compose_status=$?
|
||||
if [ "${compose_status}" -ne 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Unknown (docker ps status failed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r container_status_line; do
|
||||
if [ -z "${container_status_line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
total_containers_count=$((total_containers_count + 1))
|
||||
IFS='|' read -r container_id container_state container_status_text <<EOF
|
||||
${container_status_line}
|
||||
EOF
|
||||
|
||||
case "${container_state}" in
|
||||
running)
|
||||
running_containers_count=$((running_containers_count + 1))
|
||||
if [ -z "${first_running_status}" ]; then
|
||||
first_running_status="${container_status_text}"
|
||||
elif [ "${container_status_text}" != "${first_running_status}" ]; then
|
||||
running_status_varies=1
|
||||
fi
|
||||
;;
|
||||
exited)
|
||||
exited_containers_count=$((exited_containers_count + 1))
|
||||
;;
|
||||
created)
|
||||
created_containers_count=$((created_containers_count + 1))
|
||||
;;
|
||||
restarting)
|
||||
restarting_containers_count=$((restarting_containers_count + 1))
|
||||
;;
|
||||
paused)
|
||||
paused_containers_count=$((paused_containers_count + 1))
|
||||
;;
|
||||
dead)
|
||||
dead_containers_count=$((dead_containers_count + 1))
|
||||
;;
|
||||
*)
|
||||
other_containers_count=$((other_containers_count + 1))
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
${container_status_lines}
|
||||
EOF
|
||||
|
||||
if [ "${total_containers_count}" -eq 0 ]; then
|
||||
printf -v "${result_var}" "%s" "Not created"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${first_running_status}" ]; then
|
||||
case "${first_running_status}" in
|
||||
Up\ *)
|
||||
running_status_excerpt="${first_running_status#Up }"
|
||||
;;
|
||||
*)
|
||||
running_status_excerpt="${first_running_status}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "${running_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Running (${running_containers_count}/${total_containers_count} containers"
|
||||
if [ -n "${running_status_excerpt}" ]; then
|
||||
status_label="${status_label}, up ${running_status_excerpt}"
|
||||
if [ "${running_status_varies}" -eq 1 ]; then
|
||||
status_label="${status_label}+"
|
||||
fi
|
||||
fi
|
||||
status_label="${status_label})"
|
||||
elif [ "${running_containers_count}" -gt 0 ]; then
|
||||
status_label="Partial (${running_containers_count}/${total_containers_count} running"
|
||||
if [ "${restarting_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${restarting_containers_count} restarting"
|
||||
elif [ "${paused_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${paused_containers_count} paused"
|
||||
elif [ "${exited_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${exited_containers_count} stopped"
|
||||
elif [ "${created_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${created_containers_count} created"
|
||||
elif [ "${dead_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${dead_containers_count} dead"
|
||||
elif [ "${other_containers_count}" -gt 0 ]; then
|
||||
status_label="${status_label}, ${other_containers_count} other"
|
||||
fi
|
||||
|
||||
if [ -n "${running_status_excerpt}" ]; then
|
||||
status_label="${status_label}, up ${running_status_excerpt}"
|
||||
if [ "${running_status_varies}" -eq 1 ]; then
|
||||
status_label="${status_label}+"
|
||||
fi
|
||||
fi
|
||||
status_label="${status_label})"
|
||||
elif [ "${restarting_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Restarting (${total_containers_count} containers)"
|
||||
elif [ "${paused_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Paused (${total_containers_count} containers)"
|
||||
elif [ "${created_containers_count}" -eq "${total_containers_count}" ]; then
|
||||
status_label="Created (${total_containers_count} containers)"
|
||||
else
|
||||
status_label="Stopped (${total_containers_count} containers)"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${status_label}"
|
||||
return 0
|
||||
}
|
||||
89
scripts/easy-docker/lib/app/wizard/common/compose/start/stop.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
stop_stack_with_compose_from_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local compose_project_name=""
|
||||
local stack_topology=""
|
||||
local repo_root=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata fails.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 41
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 42
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 43.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
|
||||
return 43
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host" | "split-services") ;;
|
||||
*)
|
||||
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 44.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
|
||||
return 44
|
||||
;;
|
||||
esac
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 45
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 46.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
|
||||
return 46
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 45
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" stop; then
|
||||
return 47
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" stop; then
|
||||
return 47
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
9
scripts/easy-docker/lib/app/wizard/common/constants.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Shared flow/status constants used across sourced wizard modules.
|
||||
# shellcheck disable=SC2034
|
||||
readonly FLOW_CONTINUE=0
|
||||
readonly FLOW_BACK_TO_MAIN=10
|
||||
readonly FLOW_EXIT_APP=11
|
||||
readonly FLOW_ABORT_INPUT=12
|
||||
readonly FLOW_OPEN_MANAGE_STACK=13
|
||||
372
scripts/easy-docker/lib/app/wizard/common/core.sh
Executable file
|
|
@ -0,0 +1,372 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_easy_docker_repo_root() {
|
||||
local app_lib_dir=""
|
||||
|
||||
if [ -n "${EASY_DOCKER_REPO_ROOT_OVERRIDE:-}" ]; then
|
||||
printf '%s\n' "${EASY_DOCKER_REPO_ROOT_OVERRIDE}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
app_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# core.sh lives in scripts/easy-docker/lib/app/wizard/common
|
||||
# so we need 6 levels up to reach repository root.
|
||||
(cd "${app_lib_dir}/../../../../../.." && pwd)
|
||||
}
|
||||
|
||||
get_easy_docker_stacks_dir() {
|
||||
printf '%s/.easy-docker/stacks\n' "$(get_easy_docker_repo_root)"
|
||||
}
|
||||
|
||||
get_current_utc_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ"
|
||||
}
|
||||
|
||||
is_valid_stack_name() {
|
||||
local stack_name="${1}"
|
||||
|
||||
if [ -z "${stack_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${stack_name}" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
create_stack_directory_with_metadata() {
|
||||
local stack_dir_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local setup_type="${3:-production}"
|
||||
local frappe_branch="${4:-}"
|
||||
local stacks_dir=""
|
||||
local created_stack_dir=""
|
||||
local metadata_path=""
|
||||
local created_at=""
|
||||
|
||||
stacks_dir="$(get_easy_docker_stacks_dir)"
|
||||
created_stack_dir="${stacks_dir}/${stack_name}"
|
||||
metadata_path="${created_stack_dir}/metadata.json"
|
||||
|
||||
if ! mkdir -p "${stacks_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -e "${created_stack_dir}" ]; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [ -z "${frappe_branch}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mkdir -p "${created_stack_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
created_at="$(get_current_utc_timestamp)"
|
||||
|
||||
if ! cat >"${metadata_path}" <<EOF; then
|
||||
{
|
||||
"schema_version": 1,
|
||||
"stack_name": "${stack_name}",
|
||||
"setup_type": "${setup_type}",
|
||||
"frappe_branch": "${frappe_branch}",
|
||||
"created_at": "${created_at}"
|
||||
}
|
||||
EOF
|
||||
rollback_stack_directory "${created_stack_dir}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${stack_dir_var}" "%s" "${created_stack_dir}"
|
||||
return 0
|
||||
}
|
||||
|
||||
rollback_stack_directory() {
|
||||
local stack_dir="${1}"
|
||||
local stacks_dir=""
|
||||
|
||||
if [ -z "${stack_dir}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
stacks_dir="$(get_easy_docker_stacks_dir)"
|
||||
case "${stack_dir}" in
|
||||
"${stacks_dir}"/*) ;;
|
||||
*)
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -d "${stack_dir}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -rf -- "${stack_dir}"
|
||||
}
|
||||
|
||||
get_metadata_string_field() {
|
||||
local metadata_path="${1}"
|
||||
local field_name="${2}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
easy_docker_run_jq -r --arg field_name "${field_name}" '[.. | objects | .[$field_name]? | select(type == "string")][0] // empty' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_env_file_key_value() {
|
||||
local env_file="${1}"
|
||||
local key="${2}"
|
||||
local value=""
|
||||
|
||||
if [ ! -f "${env_file}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
value="$(
|
||||
awk -v key="${key}" '
|
||||
/^[[:space:]]*#/ { next }
|
||||
$0 !~ /=/ { next }
|
||||
{
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
pos = index(line, "=")
|
||||
if (pos == 0) {
|
||||
next
|
||||
}
|
||||
k = substr(line, 1, pos - 1)
|
||||
sub(/^[[:space:]]*export[[:space:]]+/, "", k)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", k)
|
||||
if (k != key) {
|
||||
next
|
||||
}
|
||||
v = substr(line, pos + 1)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
|
||||
print v
|
||||
exit
|
||||
}
|
||||
' "${env_file}"
|
||||
)"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
\"*\")
|
||||
value="${value#\"}"
|
||||
value="${value%\"}"
|
||||
;;
|
||||
\'*\')
|
||||
value="${value#\'}"
|
||||
value="${value%\'}"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "${value}"
|
||||
}
|
||||
|
||||
get_default_erpnext_version() {
|
||||
local repo_root=""
|
||||
local source_env_file=""
|
||||
local value=""
|
||||
|
||||
if [ -n "${ERPNEXT_VERSION:-}" ]; then
|
||||
printf '%s\n' "${ERPNEXT_VERSION}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
for source_env_file in "${repo_root}/.env" "${repo_root}/example.env"; do
|
||||
value="$(get_env_file_key_value "${source_env_file}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -n "${value}" ]; then
|
||||
printf '%s\n' "${value}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_default_frappe_branch() {
|
||||
local repo_root=""
|
||||
local source_env_file=""
|
||||
local value=""
|
||||
|
||||
if [ -n "${FRAPPE_BRANCH:-}" ]; then
|
||||
printf '%s\n' "${FRAPPE_BRANCH}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
for source_env_file in "${repo_root}/.env" "${repo_root}/example.env"; do
|
||||
value="$(get_env_file_key_value "${source_env_file}" "FRAPPE_BRANCH" || true)"
|
||||
if [ -n "${value}" ]; then
|
||||
printf '%s\n' "${value}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
printf 'version-15\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_frappe_branch() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local value=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
value="$(get_metadata_string_field "${metadata_path}" "frappe_branch" || true)"
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${value}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_env_path() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local stack_name=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
stack_name="$(get_metadata_string_field "${metadata_path}" "stack_name" || true)"
|
||||
if [ -z "${stack_name}" ]; then
|
||||
stack_name="${stack_dir##*/}"
|
||||
fi
|
||||
|
||||
printf '%s/%s.env\n' "${stack_dir}" "${stack_name}"
|
||||
}
|
||||
|
||||
get_stack_compose_project_name() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local stack_name=""
|
||||
local project_name=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
stack_name="$(get_metadata_string_field "${metadata_path}" "stack_name" || true)"
|
||||
if [ -z "${stack_name}" ]; then
|
||||
stack_name="${stack_dir##*/}"
|
||||
fi
|
||||
|
||||
project_name="$(
|
||||
printf '%s' "${stack_name}" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed 's/[^a-z0-9_-]/-/g; s/--*/-/g; s/^-*//; s/-*$//'
|
||||
)"
|
||||
if [ -z "${project_name}" ]; then
|
||||
project_name="stack"
|
||||
fi
|
||||
|
||||
printf 'easydocker-%s\n' "${project_name}"
|
||||
}
|
||||
|
||||
get_stack_generated_compose_path() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
printf '%s/compose.generated.yaml\n' "${stack_dir}"
|
||||
}
|
||||
|
||||
get_stack_dir_by_name() {
|
||||
local stack_name="${1}"
|
||||
local stacks_dir=""
|
||||
local stack_dir=""
|
||||
local metadata_path=""
|
||||
local candidate_name=""
|
||||
|
||||
stacks_dir="$(get_easy_docker_stacks_dir)"
|
||||
if [ ! -d "${stacks_dir}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for stack_dir in "${stacks_dir}"/*; do
|
||||
if [ ! -d "${stack_dir}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
candidate_name="$(get_metadata_string_field "${metadata_path}" "stack_name" || true)"
|
||||
if [ "${candidate_name}" = "${stack_name}" ]; then
|
||||
printf '%s\n' "${stack_dir}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_stack_topology() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local topology=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
topology="$(get_metadata_string_field "${metadata_path}" "topology" || true)"
|
||||
if [ -z "${topology}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${topology}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_setup_type() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local setup_type=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
setup_type="$(get_metadata_string_field "${metadata_path}" "setup_type" || true)"
|
||||
if [ -z "${setup_type}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${setup_type}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_metadata_compose_files_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
easy_docker_run_jq -r '([.. | objects | .compose_files? | select(type == "array")] | .[0] // [])[]?' "${metadata_path}"
|
||||
}
|
||||
165
scripts/easy-docker/lib/app/wizard/common/frappe.sh
Executable file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
trim_frappe_catalog_field() {
|
||||
local result_var="${1}"
|
||||
local value="${2}"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
printf -v "${result_var}" "%s" "${value}"
|
||||
}
|
||||
|
||||
is_valid_frappe_catalog_id() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
*[!a-z0-9._-]*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_frappe_branch_name() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
*[!A-Za-z0-9._/-]* | .* | *..* | */ | /*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_frappe_versions_catalog_path() {
|
||||
local repo_root=""
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
printf '%s/scripts/easy-docker/config/frappe.tsv\n' "${repo_root}"
|
||||
}
|
||||
|
||||
get_frappe_versions_catalog_entries() {
|
||||
local catalog_path=""
|
||||
local raw_line=""
|
||||
local line=""
|
||||
local version_id=""
|
||||
local version_label=""
|
||||
local frappe_branch=""
|
||||
local extra=""
|
||||
local seen_ids=","
|
||||
local seen_labels=","
|
||||
|
||||
catalog_path="$(get_frappe_versions_catalog_path)"
|
||||
if [ ! -f "${catalog_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r raw_line || [ -n "${raw_line}" ]; do
|
||||
trim_frappe_catalog_field line "${raw_line}"
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${line}" in
|
||||
\#*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${line}" == *$'\t'* ]]; then
|
||||
IFS=$'\t' read -r version_id version_label frappe_branch extra <<<"${line}"
|
||||
else
|
||||
IFS='|' read -r version_id version_label frappe_branch extra <<<"${line}"
|
||||
fi
|
||||
|
||||
trim_frappe_catalog_field version_id "${version_id}"
|
||||
trim_frappe_catalog_field version_label "${version_label}"
|
||||
trim_frappe_catalog_field frappe_branch "${frappe_branch}"
|
||||
trim_frappe_catalog_field extra "${extra}"
|
||||
|
||||
if [ -n "${extra}" ] || [ -z "${version_id}" ] || [ -z "${version_label}" ] || [ -z "${frappe_branch}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_frappe_catalog_id "${version_id}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_frappe_branch_name "${frappe_branch}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${seen_ids}" in
|
||||
*,"${version_id}",*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "${seen_labels}" in
|
||||
*,"${version_label}",*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
seen_ids="${seen_ids}${version_id},"
|
||||
seen_labels="${seen_labels}${version_label},"
|
||||
|
||||
printf '%s|%s|%s\n' "${version_id}" "${version_label}" "${frappe_branch}"
|
||||
done <"${catalog_path}"
|
||||
}
|
||||
|
||||
get_frappe_version_branch_by_label() {
|
||||
local label_lookup="${1}"
|
||||
local entry=""
|
||||
local version_id=""
|
||||
local version_label=""
|
||||
local frappe_branch=""
|
||||
|
||||
while IFS= read -r entry; do
|
||||
if [ -z "${entry}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r version_id version_label frappe_branch <<<"${entry}"
|
||||
if [ "${version_label}" = "${label_lookup}" ]; then
|
||||
printf '%s\n' "${frappe_branch}"
|
||||
return 0
|
||||
fi
|
||||
done < <(get_frappe_versions_catalog_entries)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_frappe_version_label_by_branch() {
|
||||
local branch_lookup="${1}"
|
||||
local entry=""
|
||||
local version_id=""
|
||||
local version_label=""
|
||||
local frappe_branch=""
|
||||
|
||||
while IFS= read -r entry; do
|
||||
if [ -z "${entry}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r version_id version_label frappe_branch <<<"${entry}"
|
||||
if [ "${frappe_branch}" = "${branch_lookup}" ]; then
|
||||
printf '%s\n' "${version_label}"
|
||||
return 0
|
||||
fi
|
||||
done < <(get_frappe_versions_catalog_entries)
|
||||
|
||||
return 1
|
||||
}
|
||||
92
scripts/easy-docker/lib/app/wizard/common/helpers.sh
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
json_escape_string() {
|
||||
local raw_value="${1}"
|
||||
|
||||
raw_value="${raw_value//\\/\\\\}"
|
||||
raw_value="${raw_value//\"/\\\"}"
|
||||
raw_value="${raw_value//$'\n'/\\n}"
|
||||
raw_value="${raw_value//$'\r'/\\r}"
|
||||
raw_value="${raw_value//$'\t'/\\t}"
|
||||
|
||||
printf '%s' "${raw_value}"
|
||||
}
|
||||
|
||||
build_compose_files_json_array() {
|
||||
local compose_files_lines="${1}"
|
||||
local line=""
|
||||
local first=1
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${first}" -eq 1 ]; then
|
||||
printf ' "%s"' "${line}"
|
||||
first=0
|
||||
else
|
||||
printf ',\n "%s"' "${line}"
|
||||
fi
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
}
|
||||
|
||||
build_env_json_object() {
|
||||
local env_lines="${1}"
|
||||
local line=""
|
||||
local key=""
|
||||
local value=""
|
||||
local escaped_key=""
|
||||
local escaped_value=""
|
||||
local first=1
|
||||
|
||||
printf '{'
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${line}" in
|
||||
*=*) ;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
escaped_key="$(json_escape_string "${key}")"
|
||||
escaped_value="$(json_escape_string "${value}")"
|
||||
|
||||
if [ "${first}" -eq 1 ]; then
|
||||
printf '\n "%s": "%s"' "${escaped_key}" "${escaped_value}"
|
||||
first=0
|
||||
else
|
||||
printf ',\n "%s": "%s"' "${escaped_key}" "${escaped_value}"
|
||||
fi
|
||||
done <<EOF
|
||||
${env_lines}
|
||||
EOF
|
||||
|
||||
if [ "${first}" -eq 1 ]; then
|
||||
printf '}'
|
||||
else
|
||||
printf '\n }'
|
||||
fi
|
||||
}
|
||||
|
||||
append_env_line() {
|
||||
local existing_lines="${1}"
|
||||
local key="${2}"
|
||||
local value="${3}"
|
||||
|
||||
if [ -z "${existing_lines}" ]; then
|
||||
printf '%s=%s' "${key}" "${value}"
|
||||
return
|
||||
fi
|
||||
|
||||
printf '%s\n%s=%s' "${existing_lines}" "${key}" "${value}"
|
||||
}
|
||||
20
scripts/easy-docker/lib/app/wizard/common/site.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL=""
|
||||
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
|
||||
|
||||
load_easy_docker_site_modules() {
|
||||
local site_dir=""
|
||||
site_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/site"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/metadata.sh
|
||||
source "${site_dir}/metadata.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh
|
||||
source "${site_dir}/bootstrap.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/backup.sh
|
||||
source "${site_dir}/backup.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/apps.sh
|
||||
source "${site_dir}/apps.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_site_modules
|
||||
86
scripts/easy-docker/lib/app/wizard/common/site/apps.sh
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_site_apps_modules() {
|
||||
local apps_dir=""
|
||||
apps_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/apps"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/apps/lifecycle.sh
|
||||
source "${apps_dir}/lifecycle.sh"
|
||||
}
|
||||
|
||||
append_stack_installable_app_line() {
|
||||
local result_var="${1}"
|
||||
local existing_lines="${2:-}"
|
||||
local app_name="${3:-}"
|
||||
|
||||
if [ -z "${app_name}" ]; then
|
||||
printf -v "${result_var}" "%s" "${existing_lines}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r existing_app; do
|
||||
if [ "${existing_app}" = "${app_name}" ]; then
|
||||
printf -v "${result_var}" "%s" "${existing_lines}"
|
||||
return 0
|
||||
fi
|
||||
done <<EOF
|
||||
${existing_lines}
|
||||
EOF
|
||||
|
||||
if [ -z "${existing_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "${app_name}"
|
||||
else
|
||||
printf -v "${result_var}" "%s\n%s" "${existing_lines}" "${app_name}"
|
||||
fi
|
||||
}
|
||||
|
||||
get_stack_selected_installable_apps() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local predefined_apps_csv=""
|
||||
local app_name=""
|
||||
local installable_app_lines=""
|
||||
local deferred_erpnext=""
|
||||
local ordered_app_lines=""
|
||||
local -a predefined_apps=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
predefined_apps_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
|
||||
if [ -z "${predefined_apps_csv}" ]; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a predefined_apps <<<"${predefined_apps_csv}"
|
||||
for app_name in "${predefined_apps[@]}"; do
|
||||
if [ -z "${app_name}" ] || [ "${app_name}" = "frappe" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${app_name}" = "erpnext" ]; then
|
||||
deferred_erpnext="${app_name}"
|
||||
continue
|
||||
fi
|
||||
|
||||
append_stack_installable_app_line installable_app_lines "${installable_app_lines}" "${app_name}"
|
||||
done
|
||||
|
||||
if [ -n "${deferred_erpnext}" ]; then
|
||||
ordered_app_lines="${deferred_erpnext}"
|
||||
if [ -n "${installable_app_lines}" ]; then
|
||||
ordered_app_lines="${ordered_app_lines}"$'\n'"${installable_app_lines}"
|
||||
fi
|
||||
else
|
||||
ordered_app_lines="${installable_app_lines}"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${ordered_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
load_easy_docker_site_apps_modules
|
||||
437
scripts/easy-docker/lib/app/wizard/common/site/apps/lifecycle.sh
Executable file
|
|
@ -0,0 +1,437 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
stack_site_app_lines_contain() {
|
||||
local app_lines="${1:-}"
|
||||
local app_name="${2:-}"
|
||||
|
||||
if [ -z "${app_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${app_lines}" | grep -F -x -- "${app_name}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
remove_stack_site_app_line() {
|
||||
local result_var="${1}"
|
||||
local existing_lines="${2:-}"
|
||||
local app_name="${3:-}"
|
||||
local existing_app=""
|
||||
local remaining_lines=""
|
||||
|
||||
while IFS= read -r existing_app; do
|
||||
if [ -z "${existing_app}" ] || [ "${existing_app}" = "${app_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${remaining_lines}" ]; then
|
||||
remaining_lines="${existing_app}"
|
||||
else
|
||||
remaining_lines="${remaining_lines}"$'\n'"${existing_app}"
|
||||
fi
|
||||
done <<EOF
|
||||
${existing_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" "%s" "${remaining_lines}"
|
||||
}
|
||||
|
||||
get_stack_site_managed_runtime_app_lines() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name="${3}"
|
||||
local runtime_app_lines=""
|
||||
local app_name=""
|
||||
local resolved_managed_app_lines=""
|
||||
local runtime_status=0
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if runtime_app_lines="$(get_stack_site_runtime_app_names_lines "${stack_dir}" "${site_name}")"; then
|
||||
:
|
||||
runtime_status=0
|
||||
else
|
||||
runtime_status=$?
|
||||
fi
|
||||
if [ "${runtime_status}" -eq 54 ] || [ "${runtime_status}" -eq 52 ]; then
|
||||
return "${runtime_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${runtime_app_lines}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r app_name; do
|
||||
if [ -z "${app_name}" ] || [ "${app_name}" = "frappe" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
append_stack_installable_app_line resolved_managed_app_lines "${resolved_managed_app_lines}" "${app_name}"
|
||||
done <<EOF
|
||||
${runtime_app_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" "%s" "${resolved_managed_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
persist_stack_site_app_operation_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local apps_installed_lines="${3:-}"
|
||||
local last_action="${4:-manage-site-apps}"
|
||||
local last_error="${5:-}"
|
||||
local error_log_path="${6:-}"
|
||||
local created_at=""
|
||||
local updated_at=""
|
||||
|
||||
created_at="$(get_stack_site_created_at "${stack_dir}" || true)"
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
|
||||
persist_stack_site_metadata \
|
||||
"${stack_dir}" \
|
||||
"single-site" \
|
||||
"${site_name}" \
|
||||
"${apps_installed_lines}" \
|
||||
"${last_action}" \
|
||||
"${last_error}" \
|
||||
"${error_log_path}" \
|
||||
"${created_at}" \
|
||||
"${updated_at}"
|
||||
}
|
||||
|
||||
get_configured_stack_site_installable_app_lines() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name=""
|
||||
local backend_status=0
|
||||
local selected_app_lines=""
|
||||
local available_app_lines=""
|
||||
local installed_app_lines=""
|
||||
local candidate_app=""
|
||||
local resolved_installable_app_lines=""
|
||||
local inspect_status=0
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 82
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 83
|
||||
fi
|
||||
|
||||
if stack_backend_service_is_running "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
backend_status=$?
|
||||
case "${backend_status}" in
|
||||
54)
|
||||
return 84
|
||||
;;
|
||||
52)
|
||||
return 82
|
||||
;;
|
||||
*)
|
||||
return 81
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then
|
||||
return 84
|
||||
fi
|
||||
|
||||
if available_app_lines="$(get_stack_runtime_available_app_lines "${stack_dir}")"; then
|
||||
:
|
||||
else
|
||||
inspect_status=$?
|
||||
case "${inspect_status}" in
|
||||
54)
|
||||
return 84
|
||||
;;
|
||||
52)
|
||||
return 82
|
||||
;;
|
||||
*)
|
||||
return 87
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if get_stack_site_managed_runtime_app_lines installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
inspect_status=$?
|
||||
case "${inspect_status}" in
|
||||
54)
|
||||
return 84
|
||||
;;
|
||||
52)
|
||||
return 82
|
||||
;;
|
||||
61)
|
||||
return 83
|
||||
;;
|
||||
*)
|
||||
return 87
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
while IFS= read -r candidate_app; do
|
||||
if [ -z "${candidate_app}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! stack_site_app_lines_contain "${available_app_lines}" "${candidate_app}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if stack_site_app_lines_contain "${installed_app_lines}" "${candidate_app}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
append_stack_installable_app_line resolved_installable_app_lines "${resolved_installable_app_lines}" "${candidate_app}"
|
||||
done <<EOF
|
||||
${selected_app_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" "%s" "${resolved_installable_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_configured_stack_site_uninstallable_app_lines() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name=""
|
||||
local backend_status=0
|
||||
local resolved_installed_app_lines=""
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 92
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 93
|
||||
fi
|
||||
|
||||
if stack_backend_service_is_running "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
backend_status=$?
|
||||
case "${backend_status}" in
|
||||
54)
|
||||
return 94
|
||||
;;
|
||||
52)
|
||||
return 92
|
||||
;;
|
||||
*)
|
||||
return 91
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if get_stack_site_managed_runtime_app_lines resolved_installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
backend_status=$?
|
||||
case "${backend_status}" in
|
||||
54)
|
||||
return 94
|
||||
;;
|
||||
52)
|
||||
return 92
|
||||
;;
|
||||
61)
|
||||
return 93
|
||||
;;
|
||||
*)
|
||||
return 97
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${resolved_installed_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
install_app_on_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local app_name="${2:-}"
|
||||
local site_name=""
|
||||
local installable_app_lines=""
|
||||
local current_installed_app_lines=""
|
||||
local updated_installed_app_lines=""
|
||||
local install_command=""
|
||||
local install_output=""
|
||||
local command_status=0
|
||||
local installable_status=0
|
||||
|
||||
reset_easy_docker_site_error_state
|
||||
|
||||
if [ -z "${app_name}" ]; then
|
||||
return 86
|
||||
fi
|
||||
|
||||
if get_configured_stack_site_installable_app_lines installable_app_lines "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
installable_status=$?
|
||||
return "${installable_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${installable_app_lines}" ]; then
|
||||
return 85
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! get_stack_site_managed_runtime_app_lines current_installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
current_installed_app_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)"
|
||||
fi
|
||||
|
||||
if ! stack_site_app_lines_contain "${installable_app_lines}" "${app_name}"; then
|
||||
return 86
|
||||
fi
|
||||
|
||||
install_command="$(
|
||||
printf "bench --site %s install-app %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${app_name}")"
|
||||
)"
|
||||
|
||||
if run_stack_backend_bash_command_capture install_output "${stack_dir}" "${install_command}"; then
|
||||
:
|
||||
else
|
||||
command_status=$?
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench install-app failed for '%s'." "${app_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-install-app-error" "${install_output}" >/dev/null 2>&1 || true
|
||||
if ! persist_stack_site_app_operation_metadata \
|
||||
"${stack_dir}" \
|
||||
"${site_name}" \
|
||||
"${current_installed_app_lines}" \
|
||||
"install-app" \
|
||||
"${EASY_DOCKER_SITE_ERROR_DETAIL}" \
|
||||
"${EASY_DOCKER_SITE_ERROR_LOG_PATH}"; then
|
||||
return 89
|
||||
fi
|
||||
case "${command_status}" in
|
||||
54)
|
||||
return 84
|
||||
;;
|
||||
52)
|
||||
return 82
|
||||
;;
|
||||
*)
|
||||
return 88
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! get_stack_site_managed_runtime_app_lines updated_installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
updated_installed_app_lines="${current_installed_app_lines}"
|
||||
append_stack_installable_app_line updated_installed_app_lines "${updated_installed_app_lines}" "${app_name}"
|
||||
fi
|
||||
|
||||
if ! persist_stack_site_app_operation_metadata \
|
||||
"${stack_dir}" \
|
||||
"${site_name}" \
|
||||
"${updated_installed_app_lines}" \
|
||||
"install-app" \
|
||||
"" \
|
||||
""; then
|
||||
return 89
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
uninstall_app_from_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local app_name="${2:-}"
|
||||
local site_name=""
|
||||
local uninstallable_app_lines=""
|
||||
local current_installed_app_lines=""
|
||||
local updated_installed_app_lines=""
|
||||
local uninstall_command=""
|
||||
local uninstall_output=""
|
||||
local command_status=0
|
||||
local uninstallable_status=0
|
||||
|
||||
reset_easy_docker_site_error_state
|
||||
|
||||
if [ -z "${app_name}" ] || [ "${app_name}" = "frappe" ]; then
|
||||
return 96
|
||||
fi
|
||||
|
||||
if get_configured_stack_site_uninstallable_app_lines uninstallable_app_lines "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
uninstallable_status=$?
|
||||
return "${uninstallable_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${uninstallable_app_lines}" ]; then
|
||||
return 95
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
current_installed_app_lines="${uninstallable_app_lines}"
|
||||
|
||||
if ! stack_site_app_lines_contain "${uninstallable_app_lines}" "${app_name}"; then
|
||||
return 96
|
||||
fi
|
||||
|
||||
uninstall_command="$(
|
||||
printf "bench --site %s uninstall-app %s --yes" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${app_name}")"
|
||||
)"
|
||||
|
||||
if run_stack_backend_bash_command_capture uninstall_output "${stack_dir}" "${uninstall_command}"; then
|
||||
:
|
||||
else
|
||||
command_status=$?
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench uninstall-app failed for '%s'." "${app_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-uninstall-app-error" "${uninstall_output}" >/dev/null 2>&1 || true
|
||||
if ! persist_stack_site_app_operation_metadata \
|
||||
"${stack_dir}" \
|
||||
"${site_name}" \
|
||||
"${current_installed_app_lines}" \
|
||||
"uninstall-app" \
|
||||
"${EASY_DOCKER_SITE_ERROR_DETAIL}" \
|
||||
"${EASY_DOCKER_SITE_ERROR_LOG_PATH}"; then
|
||||
return 99
|
||||
fi
|
||||
case "${command_status}" in
|
||||
54)
|
||||
return 94
|
||||
;;
|
||||
52)
|
||||
return 92
|
||||
;;
|
||||
*)
|
||||
return 98
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! get_stack_site_managed_runtime_app_lines updated_installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
remove_stack_site_app_line updated_installed_app_lines "${current_installed_app_lines}" "${app_name}"
|
||||
fi
|
||||
|
||||
if ! persist_stack_site_app_operation_metadata \
|
||||
"${stack_dir}" \
|
||||
"${site_name}" \
|
||||
"${updated_installed_app_lines}" \
|
||||
"uninstall-app" \
|
||||
"" \
|
||||
""; then
|
||||
return 99
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
11
scripts/easy-docker/lib/app/wizard/common/site/backup.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_site_backup_modules() {
|
||||
local backup_dir=""
|
||||
backup_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backup"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/backup/lifecycle.sh
|
||||
source "${backup_dir}/lifecycle.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_site_backup_modules
|
||||
103
scripts/easy-docker/lib/app/wizard/common/site/backup/lifecycle.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
backup_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
local backup_command=""
|
||||
local backup_output=""
|
||||
local command_status=0
|
||||
local created_at=""
|
||||
local updated_at=""
|
||||
local apps_installed_lines=""
|
||||
local last_backup_at=""
|
||||
local existing_last_backup_at=""
|
||||
local backend_status=0
|
||||
|
||||
reset_easy_docker_site_error_state
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 72
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 73
|
||||
fi
|
||||
|
||||
if stack_backend_service_is_running "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
backend_status=$?
|
||||
case "${backend_status}" in
|
||||
54)
|
||||
return 74
|
||||
;;
|
||||
52)
|
||||
return 72
|
||||
;;
|
||||
*)
|
||||
return 71
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
created_at="$(get_stack_site_created_at "${stack_dir}" || true)"
|
||||
apps_installed_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)"
|
||||
existing_last_backup_at="$(get_stack_site_last_backup_at "${stack_dir}" || true)"
|
||||
|
||||
backup_command="$(
|
||||
printf "bench --site %s backup --with-files" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
if run_stack_backend_bash_command_capture backup_output "${stack_dir}" "${backup_command}"; then
|
||||
:
|
||||
else
|
||||
command_status=$?
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench backup failed for '%s'." "${site_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-backup-error" "${backup_output}" >/dev/null 2>&1 || true
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata \
|
||||
"${stack_dir}" \
|
||||
"single-site" \
|
||||
"${site_name}" \
|
||||
"${apps_installed_lines}" \
|
||||
"backup-site" \
|
||||
"${EASY_DOCKER_SITE_ERROR_DETAIL}" \
|
||||
"${EASY_DOCKER_SITE_ERROR_LOG_PATH}" \
|
||||
"${created_at}" \
|
||||
"${updated_at}" \
|
||||
"${existing_last_backup_at}"; then
|
||||
return 76
|
||||
fi
|
||||
case "${command_status}" in
|
||||
54)
|
||||
return 74
|
||||
;;
|
||||
52)
|
||||
return 72
|
||||
;;
|
||||
*)
|
||||
return 75
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
last_backup_at="$(get_current_utc_timestamp)"
|
||||
updated_at="${last_backup_at}"
|
||||
if ! persist_stack_site_metadata \
|
||||
"${stack_dir}" \
|
||||
"single-site" \
|
||||
"${site_name}" \
|
||||
"${apps_installed_lines}" \
|
||||
"backup-site" \
|
||||
"" \
|
||||
"" \
|
||||
"${created_at}" \
|
||||
"${updated_at}" \
|
||||
"${last_backup_at}"; then
|
||||
return 76
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
19
scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_site_bootstrap_modules() {
|
||||
local bootstrap_dir=""
|
||||
bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/bootstrap"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap/validation.sh
|
||||
source "${bootstrap_dir}/validation.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap/runtime.sh
|
||||
source "${bootstrap_dir}/runtime.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap/errors.sh
|
||||
source "${bootstrap_dir}/errors.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap/state.sh
|
||||
source "${bootstrap_dir}/state.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/bootstrap/lifecycle.sh
|
||||
source "${bootstrap_dir}/lifecycle.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_site_bootstrap_modules
|
||||
89
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/errors.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
reset_easy_docker_site_error_state() {
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL=""
|
||||
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
|
||||
}
|
||||
|
||||
build_stack_site_error_log_relative_path() {
|
||||
local result_var="${1}"
|
||||
local action_name="${2:-site-error}"
|
||||
local raw_timestamp=""
|
||||
local safe_timestamp=""
|
||||
local relative_path=""
|
||||
|
||||
raw_timestamp="$(get_current_utc_timestamp)"
|
||||
safe_timestamp="$(printf '%s' "${raw_timestamp}" | tr ':' '-')"
|
||||
relative_path="$(printf 'logs/%s-%s.log' "${action_name}" "${safe_timestamp}")"
|
||||
printf -v "${result_var}" "%s" "${relative_path}"
|
||||
}
|
||||
|
||||
write_stack_site_error_log() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local action_name="${3:-site-error}"
|
||||
local error_output="${4:-}"
|
||||
local relative_path=""
|
||||
local log_dir=""
|
||||
local absolute_path=""
|
||||
|
||||
if [ -z "${error_output}" ]; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
build_stack_site_error_log_relative_path relative_path "${action_name}"
|
||||
log_dir="${stack_dir}/logs"
|
||||
absolute_path="${stack_dir}/${relative_path}"
|
||||
|
||||
if ! mkdir -p "${log_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "${error_output}" >"${absolute_path}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${relative_path}"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_stack_backend_bash_command_capture() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local backend_command="${3}"
|
||||
local command_output=""
|
||||
local command_status=0
|
||||
|
||||
reset_easy_docker_site_error_state
|
||||
command_output="$(run_stack_backend_bash_command "${stack_dir}" "${backend_command}" 2>&1)"
|
||||
command_status=$?
|
||||
|
||||
if [ -n "${command_output}" ]; then
|
||||
printf '%s\n' "${command_output}"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${command_output}"
|
||||
return "${command_status}"
|
||||
}
|
||||
|
||||
capture_stack_site_error_log() {
|
||||
local stack_dir="${1}"
|
||||
local action_name="${2:-site-error}"
|
||||
local error_output="${3:-}"
|
||||
local log_path=""
|
||||
|
||||
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
|
||||
if [ -z "${error_output}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! write_stack_site_error_log log_path "${stack_dir}" "${action_name}" "${error_output}"; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="${EASY_DOCKER_SITE_ERROR_DETAIL:-Failed to write site error log.}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after site bootstrap failures.
|
||||
EASY_DOCKER_SITE_ERROR_LOG_PATH="${log_path}"
|
||||
return 0
|
||||
}
|
||||
536
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/lifecycle.sh
Executable file
|
|
@ -0,0 +1,536 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
drop_stack_site_database() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local db_password=""
|
||||
local db_root_username=""
|
||||
local drop_db_command=""
|
||||
local drop_db_output=""
|
||||
local db_drop_status=0
|
||||
|
||||
db_password="$(get_stack_database_root_password "${stack_dir}")"
|
||||
db_root_username="$(get_stack_database_root_username "${stack_dir}" || true)"
|
||||
if [ -z "${db_root_username}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
drop_db_command="$(
|
||||
printf "bench drop-site %s --no-backup --force --db-root-username %s --db-root-password %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${db_root_username}")" \
|
||||
"$(shell_quote_site_command_arg "${db_password}")"
|
||||
)"
|
||||
|
||||
if run_stack_backend_bash_command_capture drop_db_output "${stack_dir}" "${drop_db_command}"; then
|
||||
return 0
|
||||
else
|
||||
db_drop_status=$?
|
||||
fi
|
||||
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench drop-site failed for '%s'." "${site_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-delete-error" "${drop_db_output}" >/dev/null 2>&1 || true
|
||||
return "${db_drop_status}"
|
||||
}
|
||||
|
||||
remove_stack_site_directory() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local remove_command=""
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
remove_command="$(
|
||||
printf "rm -rf -- sites/%s archived_sites/%s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
if ! run_stack_backend_bash_command "${stack_dir}" "${remove_command}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
cleanup_partial_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local artifact_status=0
|
||||
local db_name=""
|
||||
local has_site_config=1
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if stack_site_has_partial_artifacts "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
artifact_status=$?
|
||||
case "${artifact_status}" in
|
||||
61)
|
||||
return 61
|
||||
;;
|
||||
54 | 52)
|
||||
return "${artifact_status}"
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if stack_site_config_exists "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
artifact_status=$?
|
||||
case "${artifact_status}" in
|
||||
61)
|
||||
return 61
|
||||
;;
|
||||
54 | 52)
|
||||
return "${artifact_status}"
|
||||
;;
|
||||
*)
|
||||
has_site_config=0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "${has_site_config}" -eq 1 ]; then
|
||||
db_name="$(get_stack_site_database_name "${stack_dir}" "${site_name}" || true)"
|
||||
if [ -z "${db_name}" ]; then
|
||||
return 60
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${has_site_config}" -eq 1 ] && ! drop_stack_site_database "${stack_dir}" "${site_name}"; then
|
||||
return 60
|
||||
fi
|
||||
|
||||
if ! remove_stack_site_directory "${stack_dir}" "${site_name}"; then
|
||||
return 60
|
||||
fi
|
||||
|
||||
if stack_site_has_partial_artifacts "${stack_dir}" "${site_name}"; then
|
||||
return 60
|
||||
fi
|
||||
|
||||
artifact_status=$?
|
||||
case "${artifact_status}" in
|
||||
54 | 52)
|
||||
return "${artifact_status}"
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
delete_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
local delete_status=0
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if ! stack_backend_service_is_running "${stack_dir}"; then
|
||||
return 51
|
||||
fi
|
||||
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
delete_status=$?
|
||||
case "${delete_status}" in
|
||||
54 | 52 | 61)
|
||||
return "${delete_status}"
|
||||
;;
|
||||
*)
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! clear_stack_site_metadata "${stack_dir}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
create_first_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local admin_password="${3}"
|
||||
local create_site_command=""
|
||||
local create_site_output=""
|
||||
local database_id=""
|
||||
local db_password=""
|
||||
local db_root_username=""
|
||||
local db_host="db"
|
||||
local db_port="3306"
|
||||
|
||||
database_id="$(get_stack_database_id "${stack_dir}" || true)"
|
||||
db_password="$(get_stack_database_root_password "${stack_dir}")"
|
||||
db_root_username="$(get_stack_database_root_username "${stack_dir}" || true)"
|
||||
if [ -z "${db_root_username}" ]; then
|
||||
return 57
|
||||
fi
|
||||
|
||||
case "${database_id}" in
|
||||
mariadb)
|
||||
create_site_command="$(
|
||||
printf "bench new-site %s --mariadb-user-host-login-scope='%%' --admin-password %s --db-root-username %s --db-root-password %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${admin_password}")" \
|
||||
"$(shell_quote_site_command_arg "${db_root_username}")" \
|
||||
"$(shell_quote_site_command_arg "${db_password}")"
|
||||
)"
|
||||
;;
|
||||
postgres)
|
||||
db_port="5432"
|
||||
create_site_command="$(
|
||||
printf "bench new-site %s --db-type postgres --db-host %s --db-port %s --admin-password %s --db-root-username %s --db-root-password %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${db_host}")" \
|
||||
"$(shell_quote_site_command_arg "${db_port}")" \
|
||||
"$(shell_quote_site_command_arg "${admin_password}")" \
|
||||
"$(shell_quote_site_command_arg "${db_root_username}")" \
|
||||
"$(shell_quote_site_command_arg "${db_password}")"
|
||||
)"
|
||||
;;
|
||||
*)
|
||||
return 57
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! run_stack_backend_bash_command_capture create_site_output "${stack_dir}" "${create_site_command}"; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="bench new-site failed."
|
||||
capture_stack_site_error_log "${stack_dir}" "site-create-error" "${create_site_output}" >/dev/null 2>&1 || true
|
||||
return 55
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
install_stack_apps_on_site() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name="${3}"
|
||||
local selected_app_lines=""
|
||||
local installed_app_lines=""
|
||||
local app_name=""
|
||||
local install_app_command=""
|
||||
local install_app_output=""
|
||||
local available_app_lines=""
|
||||
local -a selected_apps=()
|
||||
|
||||
if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${selected_app_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
available_app_lines="$(get_stack_runtime_available_app_lines "${stack_dir}" || true)"
|
||||
if [ -z "${available_app_lines}" ]; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="Could not inspect available apps in the backend image."
|
||||
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "easy-docker could not list /home/frappe/frappe-bench/apps before install-app." >/dev/null 2>&1 || true
|
||||
return 63
|
||||
fi
|
||||
|
||||
mapfile -t selected_apps <<<"${selected_app_lines}"
|
||||
for app_name in "${selected_apps[@]}"; do
|
||||
if [ -z "${app_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "${available_app_lines}" | grep -F -x -- "${app_name}" >/dev/null 2>&1; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "Selected app '%s' is not available in the backend image." "${app_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "$(printf "Selected app '%s' was requested in stack metadata but is missing from /home/frappe/frappe-bench/apps.\nAvailable apps:\n%s" "${app_name}" "${available_app_lines}")" >/dev/null 2>&1 || true
|
||||
if [ -n "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" ]; then
|
||||
printf 'Details written to %s\n' "${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}" >&2
|
||||
fi
|
||||
printf -v "${result_var}" "%s" "${installed_app_lines}"
|
||||
return 63
|
||||
fi
|
||||
|
||||
install_app_command="$(
|
||||
printf "bench --site %s install-app %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${app_name}")"
|
||||
)"
|
||||
|
||||
if ! run_stack_backend_bash_command_capture install_app_output "${stack_dir}" "${install_app_command}"; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench install-app failed for '%s'." "${app_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "${install_app_output}" >/dev/null 2>&1 || true
|
||||
printf -v "${result_var}" "%s" "${installed_app_lines}"
|
||||
return 56
|
||||
fi
|
||||
|
||||
if [ -z "${installed_app_lines}" ]; then
|
||||
installed_app_lines="${app_name}"
|
||||
else
|
||||
installed_app_lines="${installed_app_lines}"$'\n'"${app_name}"
|
||||
fi
|
||||
|
||||
if ! persist_stack_site_metadata \
|
||||
"${stack_dir}" \
|
||||
"single-site" \
|
||||
"${site_name}" \
|
||||
"${installed_app_lines}" \
|
||||
"install-apps" \
|
||||
"" \
|
||||
"" \
|
||||
"$(get_stack_site_created_at "${stack_dir}" || true)" \
|
||||
"$(get_current_utc_timestamp)"; then
|
||||
return 58
|
||||
fi
|
||||
done
|
||||
|
||||
printf -v "${result_var}" "%s" "${installed_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
stack_site_should_enable_developer_mode() {
|
||||
local stack_dir="${1}"
|
||||
local setup_type=""
|
||||
|
||||
setup_type="$(get_stack_setup_type "${stack_dir}" || true)"
|
||||
case "${setup_type}" in
|
||||
development)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
enable_stack_site_developer_mode() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local developer_mode_command=""
|
||||
local developer_mode_output=""
|
||||
|
||||
developer_mode_command="$(
|
||||
printf "bench --site %s set-config developer_mode 1 && bench --site %s clear-cache" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
if run_stack_backend_bash_command_capture developer_mode_output "${stack_dir}" "${developer_mode_command}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "Could not enable developer mode for '%s'." "${site_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-developer-mode-error" "${developer_mode_output}" >/dev/null 2>&1 || true
|
||||
return 66
|
||||
}
|
||||
|
||||
run_stack_site_migrate() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local migrate_command=""
|
||||
local migrate_output=""
|
||||
|
||||
migrate_command="$(
|
||||
printf "bench --site %s migrate" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
if ! run_stack_backend_bash_command_capture migrate_output "${stack_dir}" "${migrate_command}"; then
|
||||
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench migrate failed for '%s'." "${site_name}")"
|
||||
capture_stack_site_error_log "${stack_dir}" "site-migrate-error" "${migrate_output}" >/dev/null 2>&1 || true
|
||||
return 64
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
bootstrap_first_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local admin_password="${3}"
|
||||
local created_at=""
|
||||
local updated_at=""
|
||||
local installed_app_lines=""
|
||||
local site_create_status=0
|
||||
local developer_mode_status=0
|
||||
local app_install_status=0
|
||||
local site_migrate_status=0
|
||||
local cleanup_status=0
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
if ! stack_site_bootstrap_supports_database "${stack_dir}"; then
|
||||
return 57
|
||||
fi
|
||||
|
||||
if stack_has_site_configured "${stack_dir}"; then
|
||||
return 53
|
||||
fi
|
||||
|
||||
if ! stack_backend_service_is_running "${stack_dir}"; then
|
||||
return 51
|
||||
fi
|
||||
|
||||
if ! repair_stack_site_runtime_state "${stack_dir}"; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
if ! stack_database_service_is_reachable "${stack_dir}"; then
|
||||
return 59
|
||||
fi
|
||||
|
||||
created_at="$(get_current_utc_timestamp)"
|
||||
updated_at="${created_at}"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
cleanup_status=$?
|
||||
case "${cleanup_status}" in
|
||||
54 | 52)
|
||||
return "${cleanup_status}"
|
||||
;;
|
||||
60)
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Partial site artifacts could not be removed automatically. Manual cleanup is required." "" "" >/dev/null 2>&1 || true
|
||||
return 60
|
||||
;;
|
||||
*)
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Unexpected cleanup failure before create-site." "" "${created_at}" >/dev/null 2>&1 || true
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
updated_at="${created_at}"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
if create_first_stack_site "${stack_dir}" "${site_name}" "${admin_password}"; then
|
||||
:
|
||||
else
|
||||
site_create_status=$?
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed. Partial site data was cleaned up automatically." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
return "${site_create_status}"
|
||||
fi
|
||||
|
||||
cleanup_status=$?
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
case "${cleanup_status}" in
|
||||
54 | 52)
|
||||
return "${cleanup_status}"
|
||||
;;
|
||||
*)
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if stack_site_should_enable_developer_mode "${stack_dir}"; then
|
||||
if enable_stack_site_developer_mode "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
developer_mode_status=$?
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "enable-developer-mode" "${EASY_DOCKER_SITE_ERROR_DETAIL:-Developer mode activation failed. Partial site data was cleaned up automatically.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
return "${developer_mode_status}"
|
||||
fi
|
||||
|
||||
cleanup_status=$?
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "enable-developer-mode" "${EASY_DOCKER_SITE_ERROR_DETAIL:-Developer mode activation failed and partial site data could not be cleaned up automatically. Manual cleanup is required.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
case "${cleanup_status}" in
|
||||
54 | 52)
|
||||
return "${cleanup_status}"
|
||||
;;
|
||||
*)
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
if install_stack_apps_on_site installed_app_lines "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
app_install_status=$?
|
||||
case "${app_install_status}" in
|
||||
56 | 63)
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed. Partial site data was cleaned up automatically.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
else
|
||||
cleanup_status=$?
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed and partial site data could not be cleaned up automatically. Manual cleanup is required.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
case "${cleanup_status}" in
|
||||
54 | 52)
|
||||
return "${cleanup_status}"
|
||||
;;
|
||||
*)
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
58)
|
||||
return 58
|
||||
;;
|
||||
*)
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "Unknown app installation failure." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
;;
|
||||
esac
|
||||
return "${app_install_status}"
|
||||
fi
|
||||
|
||||
if run_stack_site_migrate "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
site_migrate_status=$?
|
||||
case "${site_migrate_status}" in
|
||||
64)
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "migrate-site" "${EASY_DOCKER_SITE_ERROR_DETAIL:-Site migration failed.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
;;
|
||||
*)
|
||||
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "migrate-site" "Unknown site migration failure." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
|
||||
;;
|
||||
esac
|
||||
return "${site_migrate_status}"
|
||||
fi
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "${installed_app_lines}" "migrate-site" "" "" "${created_at}" "${updated_at}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
157
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/runtime.sh
Executable file
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
run_stack_backend_bash_command() {
|
||||
local stack_dir="${1}"
|
||||
local backend_command="${2}"
|
||||
local wrapped_backend_command=""
|
||||
local metadata_path=""
|
||||
local env_path=""
|
||||
local compose_files_lines=""
|
||||
local compose_file=""
|
||||
local source_compose_path=""
|
||||
local env_erpnext_version=""
|
||||
local fallback_erpnext_version=""
|
||||
local compose_project_name=""
|
||||
local stack_topology=""
|
||||
local repo_root=""
|
||||
local -a compose_args=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
single-host | split-services) ;;
|
||||
*)
|
||||
return 52
|
||||
;;
|
||||
esac
|
||||
|
||||
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
|
||||
if [ -z "${env_erpnext_version}" ]; then
|
||||
fallback_erpnext_version="$(get_default_erpnext_version || true)"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
repo_root="$(get_easy_docker_repo_root)"
|
||||
while IFS= read -r compose_file; do
|
||||
if [ -z "${compose_file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
source_compose_path="${repo_root}/${compose_file}"
|
||||
if [ ! -f "${source_compose_path}" ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
wrapped_backend_command="$(printf "cd /home/frappe/frappe-bench && %s" "${backend_command}")"
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" </dev/null
|
||||
return $?
|
||||
fi
|
||||
|
||||
docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" </dev/null
|
||||
}
|
||||
|
||||
stack_backend_service_is_running() {
|
||||
local stack_dir="${1}"
|
||||
local backend_ready_status=0
|
||||
|
||||
if run_stack_backend_bash_command "${stack_dir}" "true" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
backend_ready_status=$?
|
||||
if [ "${backend_ready_status}" -eq 54 ] || [ "${backend_ready_status}" -eq 52 ]; then
|
||||
return "${backend_ready_status}"
|
||||
fi
|
||||
|
||||
# If exec fails, the backend service is not ready for site actions yet.
|
||||
return 1
|
||||
}
|
||||
|
||||
stack_database_service_is_reachable() {
|
||||
local stack_dir="${1}"
|
||||
local reachability_command=""
|
||||
local db_ready_status=0
|
||||
|
||||
IFS= read -r -d '' reachability_command <<'EOF' || true
|
||||
python - <<'PY'
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path("/home/frappe/frappe-bench/sites/common_site_config.json")
|
||||
with config_path.open(encoding="utf-8") as handle:
|
||||
config = json.load(handle)
|
||||
|
||||
db_host = config.get("db_host")
|
||||
db_port = int(config.get("db_port", 3306))
|
||||
socket.create_connection((db_host, db_port), 5).close()
|
||||
PY
|
||||
EOF
|
||||
|
||||
if run_stack_backend_bash_command "${stack_dir}" "${reachability_command}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
db_ready_status=$?
|
||||
if [ "${db_ready_status}" -eq 54 ] || [ "${db_ready_status}" -eq 52 ]; then
|
||||
return "${db_ready_status}"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_stack_common_db_endpoint() {
|
||||
local stack_dir="${1}"
|
||||
local read_command=""
|
||||
|
||||
read_command="$(
|
||||
cat <<'EOF'
|
||||
python - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
path = Path("sites/common_site_config.json")
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
config = json.load(handle)
|
||||
print(f"{config.get('db_host', '')}|{config.get('db_port', 3306)}")
|
||||
PY
|
||||
EOF
|
||||
)"
|
||||
|
||||
run_stack_backend_bash_command "${stack_dir}" "${read_command}"
|
||||
}
|
||||
|
||||
get_stack_runtime_available_app_lines() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
run_stack_backend_bash_command "${stack_dir}" "ls -1 apps"
|
||||
}
|
||||
253
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/state.sh
Executable file
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
stack_site_exists_in_bench() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local exists_command=""
|
||||
local exists_status=0
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
exists_command="$(
|
||||
printf "bench list-sites | grep -F -x -- %s >/dev/null" "$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
if run_stack_backend_bash_command "${stack_dir}" "${exists_command}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
exists_status=$?
|
||||
if [ "${exists_status}" -eq 54 ] || [ "${exists_status}" -eq 52 ]; then
|
||||
return "${exists_status}"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stack_site_directory_exists() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local exists_command=""
|
||||
local exists_status=0
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
exists_command="$(
|
||||
printf "test -d sites/%s" "$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
if run_stack_backend_bash_command "${stack_dir}" "${exists_command}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
exists_status=$?
|
||||
if [ "${exists_status}" -eq 54 ] || [ "${exists_status}" -eq 52 ]; then
|
||||
return "${exists_status}"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stack_site_config_exists() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local exists_command=""
|
||||
local exists_status=0
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
exists_command="$(
|
||||
printf "test -f sites/%s/site_config.json" "$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
if run_stack_backend_bash_command "${stack_dir}" "${exists_command}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
exists_status=$?
|
||||
if [ "${exists_status}" -eq 54 ] || [ "${exists_status}" -eq 52 ]; then
|
||||
return "${exists_status}"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_stack_site_database_name() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local read_command=""
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
read_command="$(
|
||||
printf "python - <<'PY'\nimport json\nfrom pathlib import Path\npath = Path('sites') / %s / 'site_config.json'\nwith path.open(encoding='utf-8') as handle:\n print(json.load(handle).get('db_name', ''))\nPY" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
run_stack_backend_bash_command "${stack_dir}" "${read_command}"
|
||||
}
|
||||
|
||||
get_stack_site_runtime_app_names_lines() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
local list_apps_command=""
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
list_apps_command="$(
|
||||
printf "bench --site %s list-apps | awk 'NF { print \$1 }'" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")"
|
||||
)"
|
||||
|
||||
run_stack_backend_bash_command "${stack_dir}" "${list_apps_command}"
|
||||
}
|
||||
|
||||
get_stack_site_runtime_selected_apps_lines() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name="${3}"
|
||||
local selected_app_lines=""
|
||||
local runtime_app_lines=""
|
||||
local selected_app_name=""
|
||||
local installed_app_lines=""
|
||||
|
||||
if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${selected_app_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
runtime_app_lines="$(get_stack_site_runtime_app_names_lines "${stack_dir}" "${site_name}" || true)"
|
||||
if [ -z "${runtime_app_lines}" ]; then
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r selected_app_name; do
|
||||
if [ -z "${selected_app_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "${runtime_app_lines}" | grep -F -x -- "${selected_app_name}" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${installed_app_lines}" ]; then
|
||||
installed_app_lines="${selected_app_name}"
|
||||
else
|
||||
installed_app_lines="${installed_app_lines}"$'\n'"${selected_app_name}"
|
||||
fi
|
||||
done <<EOF
|
||||
${selected_app_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" "%s" "${installed_app_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
repair_stack_site_runtime_state() {
|
||||
local stack_dir="${1}"
|
||||
local database_id=""
|
||||
local redis_id=""
|
||||
local db_host=""
|
||||
local db_port=""
|
||||
local repair_command=""
|
||||
|
||||
database_id="$(get_stack_database_id "${stack_dir}" || true)"
|
||||
redis_id="$(get_stack_redis_id "${stack_dir}" || true)"
|
||||
|
||||
case "${database_id}" in
|
||||
mariadb)
|
||||
db_host="db"
|
||||
db_port="3306"
|
||||
;;
|
||||
postgres)
|
||||
db_host="db"
|
||||
db_port="5432"
|
||||
;;
|
||||
*)
|
||||
return 57
|
||||
;;
|
||||
esac
|
||||
|
||||
repair_command="$(
|
||||
cat <<EOF
|
||||
mkdir -p sites
|
||||
test -f sites/common_site_config.json || printf '{}' > sites/common_site_config.json
|
||||
ls -1 apps > sites/apps.txt
|
||||
bench set-config -g db_host ${db_host}
|
||||
bench set-config -gp db_port ${db_port}
|
||||
EOF
|
||||
)"
|
||||
|
||||
case "${redis_id}" in
|
||||
enabled)
|
||||
repair_command="${repair_command}"$'\n'"bench set-config -g redis_cache redis://redis-cache:6379"
|
||||
repair_command="${repair_command}"$'\n'"bench set-config -g redis_queue redis://redis-queue:6379"
|
||||
repair_command="${repair_command}"$'\n'"bench set-config -g redis_socketio redis://redis-queue:6379"
|
||||
;;
|
||||
"" | disabled)
|
||||
:
|
||||
;;
|
||||
*)
|
||||
return 62
|
||||
;;
|
||||
esac
|
||||
|
||||
repair_command="${repair_command}"$'\n'"bench set-config -gp socketio_port 9000"
|
||||
repair_command="${repair_command}"$'\n'"bench set-config -g chromium_path /usr/bin/chromium-headless-shell"
|
||||
|
||||
if ! run_stack_backend_bash_command "${stack_dir}" "${repair_command}"; then
|
||||
return 62
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
stack_site_has_partial_artifacts() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if stack_site_exists_in_bench "${stack_dir}" "${site_name}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case $? in
|
||||
61)
|
||||
return 61
|
||||
;;
|
||||
54 | 52)
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
|
||||
if stack_site_directory_exists "${stack_dir}" "${site_name}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case $? in
|
||||
61)
|
||||
return 61
|
||||
;;
|
||||
54 | 52)
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
138
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/validation.sh
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
is_valid_stack_site_name() {
|
||||
local site_name="${1}"
|
||||
|
||||
if [ -z "${site_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${site_name}" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_stack_site_cleanup_name() {
|
||||
local site_name="${1}"
|
||||
|
||||
if ! is_valid_stack_site_name "${site_name}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${site_name}" in
|
||||
"." | ".." | "/" | "")
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
shell_quote_site_command_arg() {
|
||||
local raw_value="${1}"
|
||||
|
||||
printf "'%s'" "$(printf '%s' "${raw_value}" | sed "s/'/'\"'\"'/g")"
|
||||
}
|
||||
|
||||
get_stack_primary_site_name_suggestion() {
|
||||
local stack_dir="${1}"
|
||||
local env_path=""
|
||||
local site_domains=""
|
||||
local primary_domain=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
site_domains="$(get_env_file_key_value "${env_path}" "SITE_DOMAINS" || true)"
|
||||
primary_domain="${site_domains%%,*}"
|
||||
primary_domain="${primary_domain%% *}"
|
||||
|
||||
if [ -n "${primary_domain}" ]; then
|
||||
printf '%s\n' "${primary_domain}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s.localhost\n' "${stack_dir##*/}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_database_id() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_string_field "${stack_dir}/metadata.json" "database_id"
|
||||
}
|
||||
|
||||
get_stack_redis_id() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_string_field "${stack_dir}/metadata.json" "redis_id"
|
||||
}
|
||||
|
||||
get_stack_database_root_password() {
|
||||
local stack_dir="${1}"
|
||||
local env_path=""
|
||||
local db_password=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
db_password="$(get_env_file_key_value "${env_path}" "DB_PASSWORD" || true)"
|
||||
if [ -z "${db_password}" ]; then
|
||||
db_password="123"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${db_password}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_database_root_username() {
|
||||
local stack_dir="${1}"
|
||||
local database_id=""
|
||||
|
||||
database_id="$(get_stack_database_id "${stack_dir}" || true)"
|
||||
case "${database_id}" in
|
||||
mariadb)
|
||||
printf 'root\n'
|
||||
return 0
|
||||
;;
|
||||
postgres)
|
||||
printf 'postgres\n'
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
stack_site_bootstrap_supports_database() {
|
||||
local stack_dir="${1}"
|
||||
local database_id=""
|
||||
|
||||
database_id="$(get_stack_database_id "${stack_dir}" || true)"
|
||||
case "${database_id}" in
|
||||
mariadb | postgres)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
stack_supports_single_site_management() {
|
||||
local stack_dir="${1}"
|
||||
local stack_topology=""
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
case "${stack_topology}" in
|
||||
single-host)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
13
scripts/easy-docker/lib/app/wizard/common/site/metadata.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_site_metadata_modules() {
|
||||
local metadata_dir=""
|
||||
metadata_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/metadata"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/metadata/read.sh
|
||||
source "${metadata_dir}/read.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/metadata/write.sh
|
||||
source "${metadata_dir}/write.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_site_metadata_modules
|
||||
102
scripts/easy-docker/lib/app/wizard/common/site/metadata/read.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_metadata_site_string_field() {
|
||||
local metadata_path="${1}"
|
||||
local field_name="${2}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
easy_docker_run_jq -r --arg field_name "${field_name}" '.site[$field_name] // empty' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_site_apps_installed_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! easy_docker_require_jq; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
easy_docker_run_jq -r '(.site.apps_installed // [])[]? | select(type == "string")' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_stack_site_name() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "name"
|
||||
}
|
||||
|
||||
get_stack_site_last_error() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "last_error"
|
||||
}
|
||||
|
||||
get_stack_site_created_at() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "created_at"
|
||||
}
|
||||
|
||||
get_stack_site_last_backup_at() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "last_backup_at"
|
||||
}
|
||||
|
||||
get_stack_site_apps_installed_lines() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_apps_installed_lines "${stack_dir}/metadata.json"
|
||||
}
|
||||
|
||||
stack_has_site_record() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if [ -n "${site_name}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stack_has_site_configured() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
local last_error=""
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
last_error="$(get_stack_site_last_error "${stack_dir}" || true)"
|
||||
|
||||
if [ -n "${site_name}" ] && [ -z "${last_error}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_stack_site_menu_entry() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name=""
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if [ -z "${site_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${site_name}"
|
||||
return 0
|
||||
}
|
||||
111
scripts/easy-docker/lib/app/wizard/common/site/metadata/write.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
build_stack_site_apps_installed_json_array() {
|
||||
local result_var="${1}"
|
||||
local apps_installed_lines="${2:-}"
|
||||
local app_name=""
|
||||
local escaped_app_name=""
|
||||
local entries_json=""
|
||||
|
||||
while IFS= read -r app_name; do
|
||||
if [ -z "${app_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_app_name="$(json_escape_string "${app_name}")"
|
||||
if [ -z "${entries_json}" ]; then
|
||||
entries_json="$(printf ' "%s"' "${escaped_app_name}")"
|
||||
else
|
||||
entries_json="${entries_json}"$',\n'"$(printf ' "%s"' "${escaped_app_name}")"
|
||||
fi
|
||||
done <<EOF
|
||||
${apps_installed_lines}
|
||||
EOF
|
||||
|
||||
if [ -z "${entries_json}" ]; then
|
||||
printf -v "${result_var}" '[\n ]'
|
||||
else
|
||||
printf -v "${result_var}" '[\n%s\n ]' "${entries_json}"
|
||||
fi
|
||||
}
|
||||
|
||||
build_stack_site_metadata_json_object() {
|
||||
local result_var="${1}"
|
||||
local site_mode="${2:-single-site}"
|
||||
local site_name="${3:-}"
|
||||
local apps_installed_lines="${4:-}"
|
||||
local last_action="${5:-}"
|
||||
local last_error="${6:-}"
|
||||
local error_log_path="${7:-}"
|
||||
local created_at="${8:-}"
|
||||
local updated_at="${9:-}"
|
||||
local last_backup_at="${10:-}"
|
||||
local apps_installed_json_array=""
|
||||
|
||||
build_stack_site_apps_installed_json_array apps_installed_json_array "${apps_installed_lines}"
|
||||
|
||||
printf -v "${result_var}" '{\n "mode": "%s",\n "name": "%s",\n "apps_installed": %s,\n "last_action": "%s",\n "last_error": "%s",\n "error_log_path": "%s",\n "created_at": "%s",\n "updated_at": "%s",\n "last_backup_at": "%s"\n }' \
|
||||
"$(json_escape_string "${site_mode}")" \
|
||||
"$(json_escape_string "${site_name}")" \
|
||||
"${apps_installed_json_array}" \
|
||||
"$(json_escape_string "${last_action}")" \
|
||||
"$(json_escape_string "${last_error}")" \
|
||||
"$(json_escape_string "${error_log_path}")" \
|
||||
"$(json_escape_string "${created_at}")" \
|
||||
"$(json_escape_string "${updated_at}")" \
|
||||
"$(json_escape_string "${last_backup_at}")"
|
||||
}
|
||||
|
||||
persist_stack_site_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local site_mode="${2:-single-site}"
|
||||
local site_name="${3:-}"
|
||||
local apps_installed_lines="${4:-}"
|
||||
local last_action="${5:-}"
|
||||
local last_error="${6:-}"
|
||||
local error_log_path="${7:-}"
|
||||
local created_at="${8:-}"
|
||||
local updated_at="${9:-}"
|
||||
local last_backup_at="${10-__KEEP_CURRENT__}"
|
||||
local metadata_path=""
|
||||
local site_json_object=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${last_backup_at}" = "__KEEP_CURRENT__" ]; then
|
||||
last_backup_at="$(get_metadata_site_string_field "${metadata_path}" "last_backup_at" || true)"
|
||||
fi
|
||||
|
||||
build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}" "${last_backup_at}"
|
||||
|
||||
if ! persist_stack_metadata_top_level_object "${stack_dir}" "site" "${site_json_object}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
mark_stack_site_failed() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2:-}"
|
||||
local apps_installed_lines="${3:-}"
|
||||
local last_action="${4:-bootstrap-site}"
|
||||
local last_error="${5:-Unknown site bootstrap failure}"
|
||||
local error_log_path="${6:-}"
|
||||
local created_at="${7:-}"
|
||||
local updated_at=""
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
|
||||
}
|
||||
|
||||
clear_stack_site_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local updated_at=""
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
persist_stack_site_metadata "${stack_dir}" "single-site" "" "" "delete-site" "" "" "" "${updated_at}" ""
|
||||
}
|
||||
153
scripts/easy-docker/lib/app/wizard/common/ux.sh
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
list_existing_stack_names() {
|
||||
local setup_type_filter="${1:-}"
|
||||
local stacks_dir=""
|
||||
local stack_dir=""
|
||||
local metadata_path=""
|
||||
local stack_name=""
|
||||
local stack_setup_type=""
|
||||
|
||||
stacks_dir="$(get_easy_docker_stacks_dir)"
|
||||
if [ ! -d "${stacks_dir}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for stack_dir in "${stacks_dir}"/*; do
|
||||
if [ ! -d "${stack_dir}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
stack_name="$(get_metadata_string_field "${metadata_path}" "stack_name" || true)"
|
||||
if [ -z "${stack_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
stack_setup_type="$(get_metadata_string_field "${metadata_path}" "setup_type" || true)"
|
||||
if [ -z "${stack_setup_type}" ]; then
|
||||
stack_setup_type="production"
|
||||
fi
|
||||
|
||||
case "${setup_type_filter}" in
|
||||
"" | all) ;;
|
||||
*)
|
||||
if [ "${stack_setup_type}" != "${setup_type_filter}" ]; then
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "${stack_name}"
|
||||
done | sort
|
||||
}
|
||||
|
||||
stack_name_in_array() {
|
||||
local stack_name="${1}"
|
||||
shift
|
||||
local candidate=""
|
||||
|
||||
for candidate in "$@"; do
|
||||
if [ "${candidate}" = "${stack_name}" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
prompt_stack_name_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local input_value=""
|
||||
local input_status=0
|
||||
|
||||
input_value="$(prompt_new_stack_name)"
|
||||
input_status=$?
|
||||
if [ "${input_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
input_value="$(printf '%s' "${input_value}" | tr -d '\r\n')"
|
||||
|
||||
case "${input_value}" in
|
||||
/cancel | /CANCEL | /Cancel)
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf -v "${result_var}" "%s" "${input_value}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
}
|
||||
|
||||
prompt_frappe_branch_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local options_lines=""
|
||||
local selection=""
|
||||
local selection_status=0
|
||||
local selected_label=""
|
||||
local selected_branch=""
|
||||
local default_branch=""
|
||||
local default_label=""
|
||||
local version_entry=""
|
||||
local version_id=""
|
||||
local version_label=""
|
||||
local version_branch=""
|
||||
local -a version_entries=()
|
||||
|
||||
mapfile -t version_entries < <(get_frappe_versions_catalog_entries || true)
|
||||
for version_entry in "${version_entries[@]}"; do
|
||||
IFS='|' read -r version_id version_label version_branch <<<"${version_entry}"
|
||||
if [ -z "${version_id}" ] || [ -z "${version_label}" ] || [ -z "${version_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${options_lines}" ]; then
|
||||
options_lines="${version_label}"
|
||||
else
|
||||
options_lines="$(printf '%s\n%s' "${options_lines}" "${version_label}")"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${options_lines}" ]; then
|
||||
show_warning_and_wait "No Frappe version profiles available in scripts/easy-docker/config/frappe.tsv." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
default_branch="$(get_default_frappe_branch || true)"
|
||||
default_label="$(get_frappe_version_label_by_branch "${default_branch}" || true)"
|
||||
|
||||
selection="$(show_frappe_version_profile_menu "${stack_name}" "${options_lines}" "${default_label}")"
|
||||
selection_status=$?
|
||||
if [ "${selection_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
selected_label="$(printf '%s' "${selection}" | tr -d '\r\n')"
|
||||
case "${selected_label}" in
|
||||
"" | "Back")
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
esac
|
||||
|
||||
selected_branch="$(get_frappe_version_branch_by_label "${selected_label}" || true)"
|
||||
if [ -z "${selected_branch}" ]; then
|
||||
show_warning_and_wait "Could not resolve branch for selected profile: ${selected_label}" 2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${selected_branch}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
}
|
||||
|
||||
show_warning_and_wait() {
|
||||
local message="${1}"
|
||||
local seconds="${2:-1}"
|
||||
|
||||
show_warning_message "${message}"
|
||||
sleep "${seconds}"
|
||||
}
|
||||
19
scripts/easy-docker/lib/app/wizard/env.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_wizard_env_modules() {
|
||||
local wizard_dir=""
|
||||
wizard_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/validation.sh
|
||||
source "${wizard_dir}/env/validation.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/apps.sh
|
||||
source "${wizard_dir}/env/apps.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/update.sh
|
||||
source "${wizard_dir}/env/update.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/collect.sh
|
||||
source "${wizard_dir}/env/collect.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/split_services.sh
|
||||
source "${wizard_dir}/env/split_services.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_wizard_env_modules
|
||||
586
scripts/easy-docker/lib/app/wizard/env/apps.sh
vendored
Executable file
|
|
@ -0,0 +1,586 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
csv_contains_value() {
|
||||
local csv_values="${1}"
|
||||
local value="${2}"
|
||||
|
||||
case ",${csv_values}," in
|
||||
*,"${value}",*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
append_csv_unique() {
|
||||
local result_var="${1}"
|
||||
local csv_values="${2}"
|
||||
local value="${3}"
|
||||
local updated_csv="${csv_values}"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
printf -v "${result_var}" "%s" "${updated_csv}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if csv_contains_value "${updated_csv}" "${value}"; then
|
||||
printf -v "${result_var}" "%s" "${updated_csv}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${updated_csv}" ]; then
|
||||
updated_csv="${value}"
|
||||
else
|
||||
updated_csv="${updated_csv},${value}"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${updated_csv}"
|
||||
}
|
||||
|
||||
lines_contains_line() {
|
||||
local lines="${1}"
|
||||
local target_line="${2}"
|
||||
local line=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "${line}" = "${target_line}" ]; then
|
||||
return 0
|
||||
fi
|
||||
done <<EOF
|
||||
${lines}
|
||||
EOF
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
append_line_unique() {
|
||||
local result_var="${1}"
|
||||
local lines="${2}"
|
||||
local new_line="${3}"
|
||||
|
||||
if [ -z "${new_line}" ]; then
|
||||
printf -v "${result_var}" "%s" "${lines}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if lines_contains_line "${lines}" "${new_line}"; then
|
||||
printf -v "${result_var}" "%s" "${lines}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${lines}" ]; then
|
||||
printf -v "${result_var}" "%s" "${new_line}"
|
||||
else
|
||||
printf -v "${result_var}" "%s\n%s" "${lines}" "${new_line}"
|
||||
fi
|
||||
}
|
||||
|
||||
build_predefined_apps_metadata_json_object() {
|
||||
local result_var="${1}"
|
||||
local predefined_csv="${2}"
|
||||
local branch_lines="${3}"
|
||||
local custom_apps_lines="${4:-}"
|
||||
local app_id=""
|
||||
local app_branch=""
|
||||
local custom_repo=""
|
||||
local custom_branch=""
|
||||
local predefined_json_entries=""
|
||||
local branch_json_entries=""
|
||||
local custom_json_entries=""
|
||||
local escaped_app_id=""
|
||||
local escaped_branch=""
|
||||
local escaped_repo=""
|
||||
local entry_json=""
|
||||
local line=""
|
||||
local -a predefined_ids=()
|
||||
|
||||
if [ -n "${predefined_csv}" ]; then
|
||||
IFS=',' read -r -a predefined_ids <<<"${predefined_csv}"
|
||||
for app_id in "${predefined_ids[@]}"; do
|
||||
if [ -z "${app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_app_id="$(json_escape_string "${app_id}")"
|
||||
entry_json="$(printf ' "%s"' "${escaped_app_id}")"
|
||||
if [ -z "${predefined_json_entries}" ]; then
|
||||
predefined_json_entries="${entry_json}"
|
||||
else
|
||||
predefined_json_entries="${predefined_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
app_id="${line%%|*}"
|
||||
app_branch="${line#*|}"
|
||||
if [ -z "${app_id}" ] || [ -z "${app_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_app_id="$(json_escape_string "${app_id}")"
|
||||
escaped_branch="$(json_escape_string "${app_branch}")"
|
||||
entry_json="$(printf ' "%s": "%s"' "${escaped_app_id}" "${escaped_branch}")"
|
||||
if [ -z "${branch_json_entries}" ]; then
|
||||
branch_json_entries="${entry_json}"
|
||||
else
|
||||
branch_json_entries="${branch_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done <<EOF
|
||||
${branch_lines}
|
||||
EOF
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
custom_repo="${line%%|*}"
|
||||
custom_branch="${line#*|}"
|
||||
if [ -z "${custom_repo}" ] || [ -z "${custom_branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
escaped_repo="$(json_escape_string "${custom_repo}")"
|
||||
escaped_branch="$(json_escape_string "${custom_branch}")"
|
||||
entry_json="$(printf ' {\n "repo": "%s",\n "branch": "%s"\n }' "${escaped_repo}" "${escaped_branch}")"
|
||||
if [ -z "${custom_json_entries}" ]; then
|
||||
custom_json_entries="${entry_json}"
|
||||
else
|
||||
custom_json_entries="${custom_json_entries}"$',\n'"${entry_json}"
|
||||
fi
|
||||
done <<EOF
|
||||
${custom_apps_lines}
|
||||
EOF
|
||||
|
||||
printf -v "${result_var}" '{\n "predefined": [\n%s\n ],\n "predefined_branches": {\n%s\n },\n "custom": [\n%s\n ]\n }' "${predefined_json_entries}" "${branch_json_entries}" "${custom_json_entries}"
|
||||
}
|
||||
|
||||
get_predefined_branch_from_lines() {
|
||||
local lines="${1}"
|
||||
local app_id_lookup="${2}"
|
||||
local line=""
|
||||
local app_id=""
|
||||
local app_branch=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "${line}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
app_id="${line%%|*}"
|
||||
app_branch="${line#*|}"
|
||||
if [ "${app_id}" = "${app_id_lookup}" ] && [ -n "${app_branch}" ]; then
|
||||
printf '%s\n' "${app_branch}"
|
||||
return 0
|
||||
fi
|
||||
done <<EOF
|
||||
${lines}
|
||||
EOF
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
choose_predefined_app_branch() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local app_id="${3}"
|
||||
local app_label="${4}"
|
||||
local repo_url="${5}"
|
||||
local preferred_branch="${6:-}"
|
||||
local branches_lines=""
|
||||
local branch=""
|
||||
local status_text=""
|
||||
local selection=""
|
||||
local default_hint=""
|
||||
local -a branch_options=()
|
||||
|
||||
if ! get_predefined_app_branch_lines_by_id branches_lines "${app_id}"; then
|
||||
show_warning_and_wait "No branch list configured for ${app_label} (${app_id}) in apps.tsv." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r branch; do
|
||||
if [ -z "${branch}" ]; then
|
||||
continue
|
||||
fi
|
||||
branch_options+=("${branch}")
|
||||
done <<EOF
|
||||
${branches_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#branch_options[@]}" -eq 0 ]; then
|
||||
show_warning_and_wait "No branches available for ${app_label} (${repo_url})." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "${preferred_branch}" ]; then
|
||||
default_hint="$(printf "Suggested default: %s" "${preferred_branch}")"
|
||||
fi
|
||||
|
||||
render_main_screen 1 >&2
|
||||
status_text="$(printf "Stack: %s\n\nSelect branch for %s (%s)\nRepo: %s\n%s" "${stack_dir##*/}" "${app_label}" "${app_id}" "${repo_url}" "${default_hint}")"
|
||||
render_box_message "${status_text}" "0 2" >&2
|
||||
|
||||
if selection="$(
|
||||
gum choose \
|
||||
--height 16 \
|
||||
--header "Branch selection (${app_label})" \
|
||||
--cursor.foreground 63 \
|
||||
--selected.foreground 45 \
|
||||
"${branch_options[@]}" \
|
||||
"Back to app selection"
|
||||
)"; then
|
||||
:
|
||||
else
|
||||
return 2
|
||||
fi
|
||||
|
||||
case "${selection}" in
|
||||
"Back to app selection" | "")
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "${selection}"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
prompt_custom_modular_apps_data() {
|
||||
local result_apps_metadata_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local options_lines=""
|
||||
local selected_labels_csv=""
|
||||
local selection_raw=""
|
||||
local selection_lines=""
|
||||
local prompt_status=0
|
||||
local selected_predefined_csv=""
|
||||
local parsed_predefined_csv=""
|
||||
local selected_label=""
|
||||
local predefined_app_id=""
|
||||
local predefined_app_label=""
|
||||
local predefined_repo_url=""
|
||||
local selected_branch=""
|
||||
local preferred_branch=""
|
||||
local available_branch_lines=""
|
||||
local existing_branch_lines=""
|
||||
local existing_custom_lines=""
|
||||
local selected_branch_lines=""
|
||||
local selected_app_count=0
|
||||
local assembled_apps_metadata_json_object=""
|
||||
local -a predefined_catalog_entries=()
|
||||
local -a selected_predefined_ids=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ -f "${metadata_path}" ]; then
|
||||
selected_predefined_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
|
||||
existing_branch_lines="$(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)"
|
||||
existing_custom_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
options_lines=""
|
||||
selected_labels_csv=""
|
||||
predefined_catalog_entries=()
|
||||
|
||||
mapfile -t predefined_catalog_entries < <(get_predefined_apps_catalog_entries || true)
|
||||
for selected_label in "${predefined_catalog_entries[@]}"; do
|
||||
IFS='|' read -r predefined_app_id predefined_app_label predefined_repo_url _ _ <<<"${selected_label}"
|
||||
if [ -z "${predefined_app_id}" ] || [ -z "${predefined_app_label}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${options_lines}" ]; then
|
||||
options_lines="$(printf '%s' "${predefined_app_label}")"
|
||||
else
|
||||
options_lines="$(printf '%s\n%s' "${options_lines}" "${predefined_app_label}")"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${selected_predefined_csv}" ]; then
|
||||
IFS=',' read -r -a selected_predefined_ids <<<"${selected_predefined_csv}"
|
||||
for predefined_app_id in "${selected_predefined_ids[@]}"; do
|
||||
if [ -z "${predefined_app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
predefined_app_label="$(get_predefined_app_label_by_id "${predefined_app_id}" || true)"
|
||||
if [ -z "${predefined_app_label}" ]; then
|
||||
continue
|
||||
fi
|
||||
append_csv_unique selected_labels_csv "${selected_labels_csv}" "${predefined_app_label}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "${options_lines}" ]; then
|
||||
show_warning_and_wait "No apps available in catalog." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
if selection_raw="$(show_custom_modular_apps_multi_select "${stack_dir}" "${options_lines}" "${selected_labels_csv}")"; then
|
||||
prompt_status=0
|
||||
else
|
||||
prompt_status=$?
|
||||
fi
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [ -z "${selection_raw}" ]; then
|
||||
show_warning_message "Select at least one app."
|
||||
continue
|
||||
fi
|
||||
|
||||
parsed_predefined_csv=""
|
||||
# gum choose can return multiple values separated by newlines or commas
|
||||
# depending on version/configuration. Normalize to one label per line.
|
||||
selection_lines="$(printf '%s' "${selection_raw}" | tr ',' '\n')"
|
||||
|
||||
while IFS= read -r selected_label; do
|
||||
trim_predefined_catalog_field selected_label "${selected_label}"
|
||||
if [ -z "${selected_label}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
predefined_app_id="$(get_predefined_app_id_by_label "${selected_label}" || true)"
|
||||
if [ -z "${predefined_app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
append_csv_unique parsed_predefined_csv "${parsed_predefined_csv}" "${predefined_app_id}"
|
||||
done <<EOF
|
||||
${selection_lines}
|
||||
EOF
|
||||
|
||||
selected_predefined_csv="${parsed_predefined_csv}"
|
||||
|
||||
if [ -z "${selected_predefined_csv}" ]; then
|
||||
show_warning_message "Select at least one app."
|
||||
continue
|
||||
fi
|
||||
|
||||
selected_branch_lines=""
|
||||
selected_app_count=0
|
||||
IFS=',' read -r -a selected_predefined_ids <<<"${selected_predefined_csv}"
|
||||
for predefined_app_id in "${selected_predefined_ids[@]}"; do
|
||||
if [ -z "${predefined_app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
predefined_app_label="$(get_predefined_app_label_by_id "${predefined_app_id}" || true)"
|
||||
if [ -z "${predefined_app_label}" ]; then
|
||||
predefined_app_label="${predefined_app_id}"
|
||||
fi
|
||||
predefined_repo_url="$(get_predefined_app_repo_by_id "${predefined_app_id}" || true)"
|
||||
if [ -z "${predefined_repo_url}" ]; then
|
||||
show_warning_and_wait "Missing repo URL for app '${predefined_app_id}'." 3
|
||||
continue 2
|
||||
fi
|
||||
|
||||
preferred_branch="$(get_predefined_branch_from_lines "${existing_branch_lines}" "${predefined_app_id}" || true)"
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
|
||||
fi
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_predefined_app_default_branch_by_id "${predefined_app_id}" || true)"
|
||||
fi
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_default_frappe_branch)"
|
||||
fi
|
||||
|
||||
available_branch_lines=""
|
||||
if get_predefined_app_branch_lines_by_id available_branch_lines "${predefined_app_id}"; then
|
||||
if [ -n "${preferred_branch}" ] && ! lines_contains_line "${available_branch_lines}" "${preferred_branch}"; then
|
||||
preferred_branch="$(get_predefined_app_default_branch_by_id "${predefined_app_id}" || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if choose_predefined_app_branch selected_branch "${stack_dir}" "${predefined_app_id}" "${predefined_app_label}" "${predefined_repo_url}" "${preferred_branch}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -eq 2 ]; then
|
||||
continue 2
|
||||
fi
|
||||
continue 2
|
||||
fi
|
||||
|
||||
append_line_unique selected_branch_lines "${selected_branch_lines}" "${predefined_app_id}|${selected_branch}"
|
||||
selected_app_count=$((selected_app_count + 1))
|
||||
done
|
||||
|
||||
if [ "${selected_app_count}" -eq 0 ]; then
|
||||
show_warning_message "No valid apps selected."
|
||||
continue
|
||||
fi
|
||||
|
||||
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}" "${existing_custom_lines}"
|
||||
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
update_stack_custom_modular_apps() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local apps_metadata_json_object=""
|
||||
local prompt_status=0
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 3
|
||||
fi
|
||||
|
||||
if prompt_custom_modular_apps_data apps_metadata_json_object "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${apps_metadata_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_metadata_apps_object "${stack_dir}" "${apps_metadata_json_object}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt_selected_stack_app_branches_data() {
|
||||
local result_apps_metadata_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local metadata_path=""
|
||||
local selected_predefined_csv=""
|
||||
local predefined_app_id=""
|
||||
local predefined_app_label=""
|
||||
local predefined_repo_url=""
|
||||
local selected_branch=""
|
||||
local preferred_branch=""
|
||||
local available_branch_lines=""
|
||||
local existing_branch_lines=""
|
||||
local existing_custom_lines=""
|
||||
local selected_branch_lines=""
|
||||
local selected_app_count=0
|
||||
local assembled_apps_metadata_json_object=""
|
||||
local prompt_status=0
|
||||
local -a selected_predefined_ids=()
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 3
|
||||
fi
|
||||
|
||||
selected_predefined_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
|
||||
existing_branch_lines="$(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)"
|
||||
existing_custom_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
|
||||
if [ -z "${selected_predefined_csv}" ]; then
|
||||
return 4
|
||||
fi
|
||||
|
||||
selected_branch_lines=""
|
||||
IFS=',' read -r -a selected_predefined_ids <<<"${selected_predefined_csv}"
|
||||
for predefined_app_id in "${selected_predefined_ids[@]}"; do
|
||||
if [ -z "${predefined_app_id}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
predefined_app_label="$(get_predefined_app_label_by_id "${predefined_app_id}" || true)"
|
||||
if [ -z "${predefined_app_label}" ]; then
|
||||
predefined_app_label="${predefined_app_id}"
|
||||
fi
|
||||
|
||||
predefined_repo_url="$(get_predefined_app_repo_by_id "${predefined_app_id}" || true)"
|
||||
if [ -z "${predefined_repo_url}" ]; then
|
||||
show_warning_and_wait "Missing repo URL for app '${predefined_app_id}'." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
preferred_branch="$(get_predefined_branch_from_lines "${existing_branch_lines}" "${predefined_app_id}" || true)"
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
|
||||
fi
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_predefined_app_default_branch_by_id "${predefined_app_id}" || true)"
|
||||
fi
|
||||
if [ -z "${preferred_branch}" ]; then
|
||||
preferred_branch="$(get_default_frappe_branch)"
|
||||
fi
|
||||
|
||||
available_branch_lines=""
|
||||
if get_predefined_app_branch_lines_by_id available_branch_lines "${predefined_app_id}"; then
|
||||
if [ -n "${preferred_branch}" ] && ! lines_contains_line "${available_branch_lines}" "${preferred_branch}"; then
|
||||
preferred_branch="$(get_predefined_app_default_branch_by_id "${predefined_app_id}" || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if choose_predefined_app_branch selected_branch "${stack_dir}" "${predefined_app_id}" "${predefined_app_label}" "${predefined_repo_url}" "${preferred_branch}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -eq 2 ]; then
|
||||
return 2
|
||||
fi
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
append_line_unique selected_branch_lines "${selected_branch_lines}" "${predefined_app_id}|${selected_branch}"
|
||||
selected_app_count=$((selected_app_count + 1))
|
||||
done
|
||||
|
||||
if [ "${selected_app_count}" -eq 0 ]; then
|
||||
return 4
|
||||
fi
|
||||
|
||||
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}" "${existing_custom_lines}"
|
||||
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
|
||||
return 0
|
||||
}
|
||||
|
||||
update_stack_selected_app_branches() {
|
||||
local stack_dir="${1}"
|
||||
local metadata_path=""
|
||||
local apps_metadata_json_object=""
|
||||
local prompt_status=0
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 3
|
||||
fi
|
||||
|
||||
if prompt_selected_stack_app_branches_data apps_metadata_json_object "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${apps_metadata_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_metadata_apps_object "${stack_dir}" "${apps_metadata_json_object}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
265
scripts/easy-docker/lib/app/wizard/env/collect.sh
vendored
Executable file
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
collect_stack_image_and_apps_env_lines() {
|
||||
local result_env_var="${1}"
|
||||
local result_apps_metadata_var="${2}"
|
||||
local stack_dir="${3}"
|
||||
local built_env_lines=""
|
||||
local custom_image_value=""
|
||||
local custom_tag_value=""
|
||||
local built_apps_metadata_json_object=""
|
||||
local prompt_status=0
|
||||
|
||||
if prompt_env_value_with_validation custom_image_value "${stack_dir}" "CUSTOM_IMAGE" "Required for custom modular image mode.\nExample: ghcr.io/acme/frappe-custom\nType /back to return." "ghcr.io/acme/frappe-custom" "required" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
built_env_lines="$(append_env_line "${built_env_lines}" "CUSTOM_IMAGE" "${custom_image_value}")"
|
||||
|
||||
if prompt_env_value_with_validation custom_tag_value "${stack_dir}" "CUSTOM_TAG" "Required for custom modular image mode.\nExample: v1.0.0\nType /back to return." "v1.0.0" "required" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
built_env_lines="$(append_env_line "${built_env_lines}" "CUSTOM_TAG" "${custom_tag_value}")"
|
||||
|
||||
if prompt_custom_modular_apps_data built_apps_metadata_json_object "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${built_apps_metadata_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_env_var}" "%s" "${built_env_lines}"
|
||||
printf -v "${result_apps_metadata_var}" "%s" "${built_apps_metadata_json_object}"
|
||||
return 0
|
||||
}
|
||||
|
||||
collect_single_host_env_lines() {
|
||||
local result_env_var="${1}"
|
||||
local result_apps_metadata_var="${2}"
|
||||
local stack_dir="${3}"
|
||||
local proxy_mode_id="${4}"
|
||||
local database_id="${5}"
|
||||
local redis_id="${6}"
|
||||
local collected_single_host_env_lines=""
|
||||
local collected_single_host_apps_metadata_json_object=""
|
||||
local value=""
|
||||
local domains_value=""
|
||||
local domain_lines=""
|
||||
local site_domains_value=""
|
||||
local sites_rule_value=""
|
||||
local nginx_proxy_hosts_value=""
|
||||
local prompt_status=0
|
||||
|
||||
if collect_stack_image_and_apps_env_lines collected_single_host_env_lines collected_single_host_apps_metadata_json_object "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
case "${proxy_mode_id}" in
|
||||
traefik-https)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for Traefik HTTPS routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
sites_rule_value="$(domain_lines_to_sites_rule "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITES_RULE" "${sites_rule_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
nginxproxy-https)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
nginxproxy-http)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
traefik-http)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
caddy-external | no-proxy)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 8080 for no-proxy frontend publishing.\nType /back to return." "8080" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown proxy mode id: ${proxy_mode_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${database_id}" in
|
||||
postgres)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Required for PostgreSQL database service.\nType /back to return." "changeit" "required" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "DB_PASSWORD" "${value}")"
|
||||
;;
|
||||
mariadb)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Optional but recommended for MariaDB.\nPress Enter to use default from override.\nType /back to return." "changeit" "optional" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "DB_PASSWORD" "${value}")"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown database id: ${database_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${redis_id}" in
|
||||
enabled)
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
|
||||
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
|
||||
;;
|
||||
disabled | "")
|
||||
:
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf -v "${result_env_var}" "%s" "${collected_single_host_env_lines}"
|
||||
printf -v "${result_apps_metadata_var}" "%s" "${collected_single_host_apps_metadata_json_object}"
|
||||
return 0
|
||||
}
|
||||
319
scripts/easy-docker/lib/app/wizard/env/split_services.sh
vendored
Executable file
|
|
@ -0,0 +1,319 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
collect_split_services_env_lines() {
|
||||
local result_env_var="${1}"
|
||||
local result_apps_metadata_var="${2}"
|
||||
local stack_dir="${3}"
|
||||
local proxy_mode_id="${4}"
|
||||
local data_mode_id="${5}"
|
||||
local database_id="${6}"
|
||||
local redis_id="${7}"
|
||||
local collected_split_services_env_lines=""
|
||||
local collected_split_services_apps_metadata_json_object=""
|
||||
local value=""
|
||||
local domains_value=""
|
||||
local domain_lines=""
|
||||
local site_domains_value=""
|
||||
local sites_rule_value=""
|
||||
local nginx_proxy_hosts_value=""
|
||||
local db_port=""
|
||||
local prompt_status=0
|
||||
|
||||
if collect_stack_image_and_apps_env_lines collected_split_services_env_lines collected_split_services_apps_metadata_json_object "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
case "${proxy_mode_id}" in
|
||||
traefik-https)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for Traefik HTTPS routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
sites_rule_value="$(domain_lines_to_sites_rule "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITES_RULE" "${sites_rule_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
nginxproxy-https)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
nginxproxy-http)
|
||||
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
|
||||
show_warning_message "Could not parse SITE_DOMAINS."
|
||||
return 1
|
||||
fi
|
||||
|
||||
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
|
||||
|
||||
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
traefik-http)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
caddy-external | no-proxy)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 8080 for no-proxy frontend publishing.\nType /back to return." "8080" "optional" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown proxy mode id: ${proxy_mode_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${data_mode_id}" in
|
||||
managed)
|
||||
case "${database_id}" in
|
||||
postgres)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Required for PostgreSQL database service.\nType /back to return." "changeit" "required" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
|
||||
;;
|
||||
mariadb)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Optional but recommended for MariaDB.\nPress Enter to use default from override.\nType /back to return." "changeit" "optional" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
if [ -n "${value}" ]; then
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown database id: ${database_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${redis_id}" in
|
||||
managed)
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
|
||||
;;
|
||||
external)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_CACHE" "Required for external Redis cache.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_QUEUE" "Required for external Redis queue.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "${value}")"
|
||||
;;
|
||||
disabled | "")
|
||||
:
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
external)
|
||||
case "${database_id}" in
|
||||
postgres)
|
||||
db_port="5432"
|
||||
;;
|
||||
mariadb)
|
||||
db_port="3306"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown database id: ${database_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_HOST" "Required for external database.\nUse a hostname or IP address.\nType /back to return." "db.example.internal" "required" "host"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_HOST" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PORT" "Required for external database.\nPress Enter to keep the default port.\nType /back to return." "${db_port}" "required" "port"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PORT" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Required for external database access.\nType /back to return." "changeit" "required" "none"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
|
||||
|
||||
case "${redis_id}" in
|
||||
managed)
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
|
||||
;;
|
||||
external)
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_CACHE" "Required for external Redis cache.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "${value}")"
|
||||
|
||||
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_QUEUE" "Required for external Redis queue.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "${value}")"
|
||||
;;
|
||||
disabled | "")
|
||||
:
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown data mode id: ${data_mode_id}" 2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf -v "${result_env_var}" "%s" "${collected_split_services_env_lines}"
|
||||
printf -v "${result_apps_metadata_var}" "%s" "${collected_split_services_apps_metadata_json_object}"
|
||||
return 0
|
||||
}
|
||||
146
scripts/easy-docker/lib/app/wizard/env/update.sh
vendored
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
is_valid_docker_image_tag() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ] || [ "${#value}" -gt 128 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
.* | -*)
|
||||
return 1
|
||||
;;
|
||||
*[!A-Za-z0-9_.-]*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_custom_image_name() {
|
||||
local stack_dir="${1}"
|
||||
local env_path=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
get_env_file_key_value "${env_path}" "CUSTOM_IMAGE"
|
||||
}
|
||||
|
||||
get_stack_custom_image_tag() {
|
||||
local stack_dir="${1}"
|
||||
local env_path=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
get_env_file_key_value "${env_path}" "CUSTOM_TAG"
|
||||
}
|
||||
|
||||
get_stack_custom_image_ref() {
|
||||
local stack_dir="${1}"
|
||||
local custom_image=""
|
||||
local custom_tag=""
|
||||
|
||||
custom_image="$(get_stack_custom_image_name "${stack_dir}" || true)"
|
||||
custom_tag="$(get_stack_custom_image_tag "${stack_dir}" || true)"
|
||||
if [ -z "${custom_image}" ] || [ -z "${custom_tag}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s:%s\n' "${custom_image}" "${custom_tag}"
|
||||
}
|
||||
|
||||
persist_env_file_key_value() {
|
||||
local env_path="${1}"
|
||||
local key="${2}"
|
||||
local value="${3}"
|
||||
local tmp_path=""
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
tmp_path="${env_path}.tmp"
|
||||
if ! awk -v key="${key}" -v value="${value}" '
|
||||
BEGIN {
|
||||
updated = 0
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
|
||||
if (line ~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=") {
|
||||
print key "=" value
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
|
||||
print line
|
||||
}
|
||||
END {
|
||||
if (!updated) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "${env_path}" >"${tmp_path}"; then
|
||||
rm -f -- "${tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${tmp_path}" "${env_path}"; then
|
||||
rm -f -- "${tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
set_stack_custom_image_tag() {
|
||||
local stack_dir="${1}"
|
||||
local custom_tag="${2:-}"
|
||||
local env_path=""
|
||||
local custom_image=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 31
|
||||
fi
|
||||
|
||||
if ! is_valid_docker_image_tag "${custom_tag}"; then
|
||||
return 32
|
||||
fi
|
||||
|
||||
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
|
||||
if [ -z "${custom_image}" ]; then
|
||||
return 33
|
||||
fi
|
||||
|
||||
if ! persist_env_file_key_value "${env_path}" "CUSTOM_TAG" "${custom_tag}"; then
|
||||
return 34
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt_stack_custom_image_tag_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local current_image=""
|
||||
local current_tag=""
|
||||
local guidance_text=""
|
||||
local custom_tag=""
|
||||
local prompt_status=0
|
||||
|
||||
current_image="$(get_stack_custom_image_name "${stack_dir}" || true)"
|
||||
current_tag="$(get_stack_custom_image_tag "${stack_dir}" || true)"
|
||||
guidance_text="$(printf "Current custom image: %s\nCurrent custom tag: %s\n\nEnter the next CUSTOM_TAG for the rebuilt image.\nExample: v1.4.3 or 2026-04-02-appupdate.\nType /back to return." "${current_image:-n/a}" "${current_tag:-n/a}")"
|
||||
|
||||
if prompt_env_value_with_validation custom_tag "${stack_dir}" "CUSTOM_TAG" "${guidance_text}" "${current_tag}" "required" "image_tag"; then
|
||||
:
|
||||
else
|
||||
prompt_status=$?
|
||||
return "${prompt_status}"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${custom_tag}"
|
||||
return 0
|
||||
}
|
||||
522
scripts/easy-docker/lib/app/wizard/env/validation.sh
vendored
Executable file
|
|
@ -0,0 +1,522 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
is_valid_email_address() {
|
||||
local value="${1}"
|
||||
|
||||
case "${value}" in
|
||||
*@*.*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_port_number() {
|
||||
local value="${1}"
|
||||
|
||||
if ! [[ "${value}" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${value}" -lt 1 ] || [ "${value}" -gt 65535 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_host_value() {
|
||||
local value="${1}"
|
||||
|
||||
if [ -z "${value}" ] || [ "${#value}" -gt 253 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if printf '%s' "${value}" | grep -Eq '[[:space:]/,:;?!@#]'; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
.* | *. | *..*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${value}" in
|
||||
[A-Za-z0-9]*) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${value}" in
|
||||
*[A-Za-z0-9]) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_host_port_value() {
|
||||
local value="${1}"
|
||||
local host=""
|
||||
local port=""
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${value}" in
|
||||
*:*)
|
||||
host="${value%:*}"
|
||||
port="${value##*:}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "${host}" ] || [ -z "${port}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_host_value "${host}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_port_number "${port}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
EASY_DOCKER_LAST_INVALID_DOMAIN=""
|
||||
|
||||
reset_domain_validation_feedback() {
|
||||
EASY_DOCKER_LAST_INVALID_DOMAIN=""
|
||||
}
|
||||
|
||||
trim_domain_token() {
|
||||
local result_var="${1}"
|
||||
local value="${2}"
|
||||
|
||||
while true; do
|
||||
case "${value}" in
|
||||
' '*)
|
||||
value="${value# }"
|
||||
;;
|
||||
*' ')
|
||||
value="${value% }"
|
||||
;;
|
||||
$'\t'*)
|
||||
value="${value#$'\t'}"
|
||||
;;
|
||||
*$'\t')
|
||||
value="${value%$'\t'}"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf -v "${result_var}" "%s" "${value}"
|
||||
return 0
|
||||
}
|
||||
|
||||
normalize_domain_token() {
|
||||
local result_var="${1}"
|
||||
local raw_token="${2}"
|
||||
local token="${raw_token}"
|
||||
|
||||
trim_domain_token token "${token}"
|
||||
if [ -z "${token}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
case "${token}" in
|
||||
\"*\")
|
||||
token="${token#\"}"
|
||||
token="${token%\"}"
|
||||
;;
|
||||
\'*\')
|
||||
token="${token#\'}"
|
||||
token="${token%\'}"
|
||||
;;
|
||||
\`*\`)
|
||||
token="${token#\`}"
|
||||
token="${token%\`}"
|
||||
;;
|
||||
\[*\])
|
||||
token="${token#\[}"
|
||||
token="${token%\]}"
|
||||
;;
|
||||
\(*\))
|
||||
token="${token#(}"
|
||||
token="${token%)}"
|
||||
;;
|
||||
\"*)
|
||||
token="${token#\"}"
|
||||
;;
|
||||
\'*)
|
||||
token="${token#\'}"
|
||||
;;
|
||||
\`*)
|
||||
token="${token#\`}"
|
||||
;;
|
||||
\[*)
|
||||
token="${token#\[}"
|
||||
;;
|
||||
\(*)
|
||||
token="${token#(}"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while true; do
|
||||
case "${token}" in
|
||||
*\")
|
||||
token="${token%\"}"
|
||||
;;
|
||||
*\')
|
||||
token="${token%\'}"
|
||||
;;
|
||||
*\`)
|
||||
token="${token%\`}"
|
||||
;;
|
||||
*\])
|
||||
token="${token%\]}"
|
||||
;;
|
||||
*\))
|
||||
token="${token%)}"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
trim_domain_token token "${token}"
|
||||
if [ -z "${token}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${token}"
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_domain_name() {
|
||||
local domain="${1}"
|
||||
local normalized_domain=""
|
||||
local label=""
|
||||
local tld=""
|
||||
local last_index=0
|
||||
local -a labels=()
|
||||
|
||||
if ! normalize_domain_token normalized_domain "${domain}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${normalized_domain}" in
|
||||
*[[:space:],:/?#!@]* | *";"* | .* | *. | *..* | *\**)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${normalized_domain}" = "localhost" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "${#normalized_domain}" -lt 5 ] || [ "${#normalized_domain}" -gt 253 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local IFS='.'
|
||||
read -r -a labels <<<"${normalized_domain}"
|
||||
if [ "${#labels[@]}" -lt 2 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for label in "${labels[@]}"; do
|
||||
if [ -z "${label}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${#label}" -gt 63 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${label}" in
|
||||
[A-Za-z0-9]*) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${label}" in
|
||||
*[A-Za-z0-9]) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${label}" in
|
||||
*[!A-Za-z0-9-]*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
last_index=$((${#labels[@]} - 1))
|
||||
tld="${labels[last_index]}"
|
||||
if [ "${tld}" = "localhost" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! [[ "${tld}" =~ ^[A-Za-z]{2,63}$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
parse_domains_input_to_lines() {
|
||||
local result_var="${1}"
|
||||
local raw_value="${2}"
|
||||
local sanitized_value=""
|
||||
local token=""
|
||||
local normalized_token=""
|
||||
local parsed_domain_lines=""
|
||||
local -a tokens=()
|
||||
local IFS=$' \t\n'
|
||||
|
||||
reset_domain_validation_feedback
|
||||
|
||||
sanitized_value="${raw_value//$'\r'/ }"
|
||||
sanitized_value="${sanitized_value//$'\n'/ }"
|
||||
sanitized_value="${sanitized_value//$'\t'/ }"
|
||||
sanitized_value="${sanitized_value//,/ }"
|
||||
sanitized_value="${sanitized_value//;/ }"
|
||||
|
||||
read -r -a tokens <<<"${sanitized_value}"
|
||||
if [ "${#tokens[@]}" -eq 0 ]; then
|
||||
EASY_DOCKER_LAST_INVALID_DOMAIN="${raw_value}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for token in "${tokens[@]}"; do
|
||||
if ! normalize_domain_token normalized_token "${token}"; then
|
||||
EASY_DOCKER_LAST_INVALID_DOMAIN="${token}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_valid_domain_name "${normalized_token}"; then
|
||||
EASY_DOCKER_LAST_INVALID_DOMAIN="${normalized_token}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${parsed_domain_lines}" ]; then
|
||||
parsed_domain_lines="${normalized_token}"
|
||||
else
|
||||
parsed_domain_lines="${parsed_domain_lines}"$'\n'"${normalized_token}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${parsed_domain_lines}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${parsed_domain_lines}"
|
||||
return 0
|
||||
}
|
||||
|
||||
domain_lines_to_csv() {
|
||||
local domain_lines="${1}"
|
||||
local domain=""
|
||||
local csv_value=""
|
||||
|
||||
while IFS= read -r domain; do
|
||||
if [ -z "${domain}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${csv_value}" ]; then
|
||||
csv_value="${domain}"
|
||||
else
|
||||
csv_value="${csv_value},${domain}"
|
||||
fi
|
||||
done <<EOF
|
||||
${domain_lines}
|
||||
EOF
|
||||
|
||||
printf '%s' "${csv_value}"
|
||||
}
|
||||
|
||||
domain_lines_to_sites_rule() {
|
||||
local domain_lines="${1}"
|
||||
local domain=""
|
||||
local sites_rule=""
|
||||
local rule_part=""
|
||||
|
||||
while IFS= read -r domain; do
|
||||
if [ -z "${domain}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
rule_part="$(printf "Host(\`%s\`)" "${domain}")"
|
||||
if [ -z "${sites_rule}" ]; then
|
||||
sites_rule="${rule_part}"
|
||||
else
|
||||
sites_rule="${sites_rule} || ${rule_part}"
|
||||
fi
|
||||
done <<EOF
|
||||
${domain_lines}
|
||||
EOF
|
||||
|
||||
printf '%s' "${sites_rule}"
|
||||
}
|
||||
|
||||
is_valid_domain_list_value() {
|
||||
local value="${1}"
|
||||
local domain_lines=""
|
||||
|
||||
if ! parse_domains_input_to_lines domain_lines "${value}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${domain_lines}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_domains_value() {
|
||||
local value="${1}"
|
||||
|
||||
if ! is_valid_domain_list_value "${value}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_image_tag_value() {
|
||||
local value="${1}"
|
||||
|
||||
if ! is_valid_docker_image_tag "${value}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt_env_value_with_validation() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local variable_name="${3}"
|
||||
local guidance_text="${4}"
|
||||
local placeholder="${5}"
|
||||
local required_mode="${6}"
|
||||
local validation_kind="${7}"
|
||||
local input_value=""
|
||||
local normalized_value=""
|
||||
local invalid_domain_input=""
|
||||
local validation_feedback=""
|
||||
local prompt_status=0
|
||||
local is_first_prompt=1
|
||||
|
||||
while true; do
|
||||
input_value="$(prompt_single_host_env_value "${stack_dir}" "${variable_name}" "${guidance_text}" "${placeholder}" "${is_first_prompt}" "${validation_feedback}")"
|
||||
prompt_status=$?
|
||||
is_first_prompt=0
|
||||
validation_feedback=""
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
normalized_value="$(printf '%s' "${input_value}" | tr -d '\r\n')"
|
||||
|
||||
case "${normalized_value}" in
|
||||
/back | /BACK | /Back | /cancel | /CANCEL | /Cancel)
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "${normalized_value}" ]; then
|
||||
if [ "${required_mode}" = "required" ]; then
|
||||
validation_feedback="Value required for ${variable_name}."
|
||||
continue
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${validation_kind}" in
|
||||
email)
|
||||
if ! is_valid_email_address "${normalized_value}"; then
|
||||
validation_feedback="Invalid email format for ${variable_name}."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
port)
|
||||
if ! is_valid_port_number "${normalized_value}"; then
|
||||
validation_feedback="Invalid port for ${variable_name}. Use 1-65535."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
host)
|
||||
if ! is_valid_host_value "${normalized_value}"; then
|
||||
validation_feedback="Invalid host for ${variable_name}. Use a hostname or IP address without spaces."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
hostport)
|
||||
if ! is_valid_host_port_value "${normalized_value}"; then
|
||||
validation_feedback="Invalid endpoint for ${variable_name}. Use host:port, for example redis.example.internal:6379."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
domains)
|
||||
if ! is_valid_domains_value "${normalized_value}"; then
|
||||
invalid_domain_input="${EASY_DOCKER_LAST_INVALID_DOMAIN}"
|
||||
if [ -z "${invalid_domain_input}" ]; then
|
||||
invalid_domain_input="${normalized_value}"
|
||||
fi
|
||||
validation_feedback="Domain '${invalid_domain_input}' cannot be used for ${variable_name}. Use a hostname like example.com, app.example.com, localhost, or dev.localhost."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
nginx_hosts)
|
||||
if ! is_valid_domains_value "${normalized_value}"; then
|
||||
validation_feedback="Invalid ${variable_name}. Use domains separated by comma or space."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
image_tag)
|
||||
if ! is_valid_image_tag_value "${normalized_value}"; then
|
||||
validation_feedback="Invalid image tag for ${variable_name}. Use letters, numbers, dots, dashes, or underscores."
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
none | "") ;;
|
||||
*)
|
||||
show_warning_message "Unknown validation rule: ${validation_kind}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf -v "${result_var}" "%s" "${normalized_value}"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
21
scripts/easy-docker/lib/app/wizard/flows.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_wizard_flow_modules() {
|
||||
local wizard_dir=""
|
||||
wizard_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/single_host.sh
|
||||
source "${wizard_dir}/flows/single_host.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/split_services.sh
|
||||
source "${wizard_dir}/flows/split_services.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage.sh
|
||||
source "${wizard_dir}/flows/manage.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/navigation.sh
|
||||
source "${wizard_dir}/flows/navigation.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/tools.sh
|
||||
source "${wizard_dir}/flows/tools.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/setup.sh
|
||||
source "${wizard_dir}/flows/setup.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_wizard_flow_modules
|
||||
17
scripts/easy-docker/lib/app/wizard/flows/manage.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
load_easy_docker_manage_flow_modules() {
|
||||
local manage_dir=""
|
||||
manage_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/manage"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/build.sh
|
||||
source "${manage_dir}/build.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/prompts.sh
|
||||
source "${manage_dir}/prompts.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/site.sh
|
||||
source "${manage_dir}/site.sh"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/stack.sh
|
||||
source "${manage_dir}/stack.sh"
|
||||
}
|
||||
|
||||
load_easy_docker_manage_flow_modules
|
||||
78
scripts/easy-docker/lib/app/wizard/flows/manage/build.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
refresh_stack_generated_compose_with_feedback() {
|
||||
local stack_dir="${1}"
|
||||
local generated_compose_path=""
|
||||
local render_compose_status=0
|
||||
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
if render_stack_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Generated compose refreshed successfully: ${generated_compose_path}" 3
|
||||
return 0
|
||||
fi
|
||||
|
||||
render_compose_status=$?
|
||||
show_warning_and_wait "The image build succeeded, but generated compose could not be refreshed (${render_compose_status}) for ${generated_compose_path}." 4
|
||||
return "${render_compose_status}"
|
||||
}
|
||||
|
||||
run_build_stack_custom_image_with_feedback() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
local build_image_status=0
|
||||
|
||||
show_warning_message "Starting docker build for stack: ${stack_name}"
|
||||
if build_stack_custom_image "${stack_dir}"; then
|
||||
show_warning_and_wait "Custom image build finished successfully for stack: ${stack_name}" 3
|
||||
refresh_stack_generated_compose_with_feedback "${stack_dir}" || true
|
||||
return 0
|
||||
else
|
||||
build_image_status=$?
|
||||
fi
|
||||
case "${build_image_status}" in
|
||||
11)
|
||||
show_warning_and_wait "Custom image build failed: missing metadata.json in ${stack_dir}." 4
|
||||
;;
|
||||
12)
|
||||
show_warning_and_wait "Custom image build failed: stack env file not found in ${stack_dir}." 4
|
||||
;;
|
||||
13)
|
||||
show_warning_and_wait "Custom image build failed: CUSTOM_IMAGE is missing in stack env file." 4
|
||||
;;
|
||||
14)
|
||||
show_warning_and_wait "Custom image build failed: CUSTOM_TAG is missing in stack env file." 4
|
||||
;;
|
||||
15)
|
||||
show_warning_and_wait "Custom image build failed: frappe_branch missing in metadata.json." 4
|
||||
;;
|
||||
16)
|
||||
show_warning_and_wait "Custom image build failed: could not generate apps.json from metadata app selection." 4
|
||||
;;
|
||||
17)
|
||||
show_warning_and_wait "Custom image build failed: apps.json not found after generation." 4
|
||||
;;
|
||||
20)
|
||||
show_warning_and_wait "Custom image build failed: images/layered/Containerfile not found." 4
|
||||
;;
|
||||
21)
|
||||
show_warning_and_wait "Custom image build failed: docker build returned an error. Check the output above." 4
|
||||
;;
|
||||
22)
|
||||
show_warning_and_wait "Custom image build failed: git is required for app branch precheck (git ls-remote)." 4
|
||||
;;
|
||||
23)
|
||||
show_warning_and_wait "Custom image build failed: could not parse app entries from apps.json." 4
|
||||
;;
|
||||
24)
|
||||
show_warning_and_wait "Custom image build failed: app branch precheck failed -> ${EASY_DOCKER_BUILD_ERROR_DETAIL}" 6
|
||||
;;
|
||||
25)
|
||||
show_warning_and_wait "Custom image build failed: jq is required for stack metadata and apps.json processing." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Custom image build failed (${build_image_status})." 4
|
||||
;;
|
||||
esac
|
||||
|
||||
return "${build_image_status}"
|
||||
}
|
||||
95
scripts/easy-docker/lib/app/wizard/flows/manage/prompts.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
prompt_manage_stack_site_name_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local stack_dir="${3}"
|
||||
local input_site_name=""
|
||||
local suggestion=""
|
||||
local prompt_status=0
|
||||
|
||||
suggestion="$(get_stack_primary_site_name_suggestion "${stack_dir}" || true)"
|
||||
while true; do
|
||||
input_site_name="$(prompt_stack_site_name "${stack_name}" "${suggestion}")"
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
input_site_name="$(printf '%s' "${input_site_name}" | tr -d '\r\n')"
|
||||
case "${input_site_name}" in
|
||||
"")
|
||||
show_warning_and_wait "Site name is required." 2
|
||||
;;
|
||||
/back | /Back | /BACK)
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
*)
|
||||
if ! is_valid_stack_site_name "${input_site_name}"; then
|
||||
show_warning_and_wait "Site name may only contain letters, numbers, dots, dashes, and underscores." 3
|
||||
continue
|
||||
fi
|
||||
printf -v "${result_var}" "%s" "${input_site_name}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
prompt_manage_stack_site_admin_password_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local input_admin_password=""
|
||||
local prompt_status=0
|
||||
|
||||
while true; do
|
||||
input_admin_password="$(prompt_stack_site_admin_password "${stack_name}")"
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
input_admin_password="$(printf '%s' "${input_admin_password}" | tr -d '\r\n')"
|
||||
case "${input_admin_password}" in
|
||||
"")
|
||||
show_warning_and_wait "Administrator password is required." 2
|
||||
;;
|
||||
/back | /Back | /BACK)
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "${input_admin_password}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
prompt_manage_stack_delete_keyword_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local delete_confirmation=""
|
||||
local prompt_status=0
|
||||
|
||||
while true; do
|
||||
delete_confirmation="$(prompt_manage_stack_delete_keyword "${stack_name}")"
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
delete_confirmation="$(printf '%s' "${delete_confirmation}" | tr -d '\r\n')"
|
||||
case "${delete_confirmation}" in
|
||||
/back | /Back | /BACK | "")
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
delete)
|
||||
printf -v "${result_var}" "%s" "${delete_confirmation}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Type exactly delete to confirm stack removal." 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
569
scripts/easy-docker/lib/app/wizard/flows/manage/site.sh
Executable file
|
|
@ -0,0 +1,569 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
migrate_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
local current_site_apps_lines=""
|
||||
local created_at=""
|
||||
local updated_at=""
|
||||
local backend_status=0
|
||||
|
||||
reset_easy_docker_site_error_state
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 53
|
||||
fi
|
||||
|
||||
if stack_backend_service_is_running "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
backend_status=$?
|
||||
case "${backend_status}" in
|
||||
54)
|
||||
return 54
|
||||
;;
|
||||
52)
|
||||
return 52
|
||||
;;
|
||||
*)
|
||||
return 51
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! run_stack_site_migrate "${stack_dir}" "${site_name}"; then
|
||||
return $?
|
||||
fi
|
||||
|
||||
current_site_apps_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)"
|
||||
created_at="$(get_stack_site_created_at "${stack_dir}" || true)"
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata \
|
||||
"${stack_dir}" \
|
||||
"single-site" \
|
||||
"${site_name}" \
|
||||
"${current_site_apps_lines}" \
|
||||
"migrate-site" \
|
||||
"" \
|
||||
"" \
|
||||
"${created_at}" \
|
||||
"${updated_at}"; then
|
||||
return 65
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
handle_manage_stack_site_flow() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_action=""
|
||||
local site_name=""
|
||||
local admin_password=""
|
||||
local site_flow_status=0
|
||||
local existing_site_entry=""
|
||||
local existing_site_name=""
|
||||
local existing_site_created_at=""
|
||||
local existing_site_apps_lines=""
|
||||
local existing_site_apps_csv=""
|
||||
local existing_site_last_backup_at=""
|
||||
local existing_site_details_action=""
|
||||
local existing_site_apps_action=""
|
||||
local existing_site_migrate_confirm=""
|
||||
local existing_site_app_lines=""
|
||||
local existing_site_app_selection=""
|
||||
local existing_site_app_confirmation=""
|
||||
local site_delete_confirmation=""
|
||||
|
||||
while true; do
|
||||
existing_site_entry=""
|
||||
get_stack_site_menu_entry existing_site_entry "${stack_dir}" || true
|
||||
|
||||
site_action="$(show_manage_stack_site_menu "${stack_name}" "${stack_dir}" "${existing_site_entry}" || true)"
|
||||
case "${site_action}" in
|
||||
"Create new site")
|
||||
if ! prompt_manage_stack_site_name_with_cancel site_name "${stack_name}" "${stack_dir}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! prompt_manage_stack_site_admin_password_with_cancel admin_password "${stack_name}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_message "Creating the first site for stack: ${stack_name}"
|
||||
if bootstrap_first_stack_site "${stack_dir}" "${site_name}" "${admin_password}"; then
|
||||
show_warning_and_wait "Site created successfully, selected stack apps were installed, and bench migrate completed: ${site_name}" 3
|
||||
continue
|
||||
else
|
||||
site_flow_status=$?
|
||||
fi
|
||||
|
||||
case "${site_flow_status}" in
|
||||
51)
|
||||
show_warning_and_wait "Cannot manage site: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot manage site for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
53)
|
||||
show_warning_and_wait "A site is already configured for this stack. Phase 1 supports one site per stack." 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "Cannot manage site because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
55)
|
||||
show_warning_and_wait "Could not create the site. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above for bench new-site details.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
56)
|
||||
show_warning_and_wait "The site was created, but app installation failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
57)
|
||||
show_warning_and_wait "Site bootstrap currently supports only supported single-host database stacks." 4
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "The site metadata could not be written to metadata.json." 4
|
||||
;;
|
||||
59)
|
||||
show_warning_and_wait "Cannot create site: stack services are not ready yet. Wait and try again." 4
|
||||
;;
|
||||
60)
|
||||
show_warning_and_wait "Site creation failed and automatic cleanup could not remove all partial data. Manual cleanup is required." 5
|
||||
;;
|
||||
61)
|
||||
show_warning_and_wait "Cannot create site because the site name was empty or unsafe for cleanup operations." 4
|
||||
;;
|
||||
62)
|
||||
show_warning_and_wait "Cannot prepare the stack for site creation because the bench runtime files could not be repaired." 4
|
||||
;;
|
||||
63)
|
||||
show_warning_and_wait "Cannot install the selected stack apps because at least one app is missing from the backend image. ${EASY_DOCKER_SITE_ERROR_DETAIL} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 7
|
||||
;;
|
||||
64)
|
||||
show_warning_and_wait "The site was created and apps were installed, but bench migrate failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 7
|
||||
;;
|
||||
66)
|
||||
show_warning_and_wait "Site bootstrap failed while enabling developer mode for this development stack. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 7
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Site bootstrap failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Back" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
if [ -n "${existing_site_entry}" ] && [ "${site_action}" = "${existing_site_entry}" ]; then
|
||||
existing_site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
existing_site_created_at="$(get_stack_site_created_at "${stack_dir}" || true)"
|
||||
existing_site_apps_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)"
|
||||
existing_site_last_backup_at="$(get_stack_site_last_backup_at "${stack_dir}" || true)"
|
||||
if stack_backend_service_is_running "${stack_dir}" >/dev/null 2>&1; then
|
||||
get_stack_site_managed_runtime_app_lines existing_site_apps_lines "${stack_dir}" "${existing_site_name}" || true
|
||||
fi
|
||||
if [ -n "${existing_site_apps_lines}" ]; then
|
||||
existing_site_apps_csv="$(printf '%s' "${existing_site_apps_lines}" | tr '\n' ',' | sed 's/,$//')"
|
||||
else
|
||||
existing_site_apps_csv="None"
|
||||
fi
|
||||
|
||||
existing_site_details_action="$(
|
||||
show_manage_stack_site_details \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" \
|
||||
"${existing_site_created_at}" \
|
||||
"${existing_site_apps_csv}" \
|
||||
"${existing_site_last_backup_at}" || true
|
||||
)"
|
||||
case "${existing_site_details_action}" in
|
||||
"Manage apps on this site")
|
||||
while true; do
|
||||
existing_site_apps_action="$(
|
||||
show_manage_stack_site_apps_menu \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" || true
|
||||
)"
|
||||
case "${existing_site_apps_action}" in
|
||||
"Install app on this site")
|
||||
if ! get_configured_stack_site_installable_app_lines existing_site_app_lines "${stack_dir}"; then
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
81)
|
||||
show_warning_and_wait "Cannot install app: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
82)
|
||||
show_warning_and_wait "Cannot install app for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
83)
|
||||
show_warning_and_wait "Cannot install app because no configured site was found in metadata.json." 4
|
||||
;;
|
||||
84)
|
||||
show_warning_and_wait "Cannot install app because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
87)
|
||||
show_warning_and_wait "Cannot inspect installable apps on this site right now. Check backend readiness and try again." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not prepare installable site apps (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${existing_site_app_lines}" ]; then
|
||||
show_warning_and_wait "No additional selected stack apps are available to install on this site. No further apps are currently available in this stack environment." 4
|
||||
continue
|
||||
fi
|
||||
|
||||
existing_site_app_selection="$(
|
||||
show_manage_stack_site_app_selection \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" \
|
||||
"Install app on this site" \
|
||||
"${existing_site_app_lines}" || true
|
||||
)"
|
||||
case "${existing_site_app_selection}" in
|
||||
"Back" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_message "Installing app on site: ${existing_site_app_selection}"
|
||||
if install_app_on_configured_stack_site "${stack_dir}" "${existing_site_app_selection}"; then
|
||||
show_warning_and_wait "App installed successfully on site ${existing_site_name}: ${existing_site_app_selection}" 4
|
||||
continue
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
81)
|
||||
show_warning_and_wait "Cannot install app: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
82)
|
||||
show_warning_and_wait "Cannot install app for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
83)
|
||||
show_warning_and_wait "Cannot install app because no configured site was found in metadata.json." 4
|
||||
;;
|
||||
84)
|
||||
show_warning_and_wait "Cannot install app because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
85)
|
||||
show_warning_and_wait "No additional selected stack apps are available to install on this site. No further apps are currently available in this stack environment." 4
|
||||
;;
|
||||
86)
|
||||
show_warning_and_wait "The selected app is not currently installable on this site." 4
|
||||
;;
|
||||
87)
|
||||
show_warning_and_wait "Cannot inspect current site apps right now. Check backend readiness and try again." 4
|
||||
;;
|
||||
88)
|
||||
show_warning_and_wait "App installation failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
89)
|
||||
show_warning_and_wait "The app command finished, but site metadata could not be written to metadata.json." 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "App installation failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Uninstall app from this site")
|
||||
if ! get_configured_stack_site_uninstallable_app_lines existing_site_app_lines "${stack_dir}"; then
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
91)
|
||||
show_warning_and_wait "Cannot uninstall app: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
92)
|
||||
show_warning_and_wait "Cannot uninstall app for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
93)
|
||||
show_warning_and_wait "Cannot uninstall app because no configured site was found in metadata.json." 4
|
||||
;;
|
||||
94)
|
||||
show_warning_and_wait "Cannot uninstall app because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
97)
|
||||
show_warning_and_wait "Cannot inspect uninstallable apps on this site right now. Check backend readiness and try again." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not prepare uninstallable site apps (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${existing_site_app_lines}" ]; then
|
||||
show_warning_and_wait "No installed site apps are currently available for uninstall." 4
|
||||
continue
|
||||
fi
|
||||
|
||||
existing_site_app_selection="$(
|
||||
show_manage_stack_site_app_selection \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" \
|
||||
"Uninstall app from this site" \
|
||||
"${existing_site_app_lines}" || true
|
||||
)"
|
||||
case "${existing_site_app_selection}" in
|
||||
"Back" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
existing_site_app_confirmation="$(
|
||||
show_manage_stack_site_app_uninstall_confirmation \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" \
|
||||
"${existing_site_app_selection}" || true
|
||||
)"
|
||||
case "${existing_site_app_confirmation}" in
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
"Yes")
|
||||
show_warning_message "Uninstalling app from site: ${existing_site_app_selection}"
|
||||
if uninstall_app_from_configured_stack_site "${stack_dir}" "${existing_site_app_selection}"; then
|
||||
show_warning_and_wait "App uninstalled successfully from site ${existing_site_name}: ${existing_site_app_selection}" 4
|
||||
continue
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
91)
|
||||
show_warning_and_wait "Cannot uninstall app: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
92)
|
||||
show_warning_and_wait "Cannot uninstall app for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
93)
|
||||
show_warning_and_wait "Cannot uninstall app because no configured site was found in metadata.json." 4
|
||||
;;
|
||||
94)
|
||||
show_warning_and_wait "Cannot uninstall app because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
95)
|
||||
show_warning_and_wait "No installed site apps are currently available for uninstall." 4
|
||||
;;
|
||||
96)
|
||||
show_warning_and_wait "The selected app cannot be uninstalled here. frappe stays blocked, but erpnext is allowed." 4
|
||||
;;
|
||||
97)
|
||||
show_warning_and_wait "Cannot inspect current site apps right now. Check backend readiness and try again." 4
|
||||
;;
|
||||
98)
|
||||
show_warning_and_wait "App uninstall failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
99)
|
||||
show_warning_and_wait "The app command finished, but site metadata could not be written to metadata.json." 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "App uninstall failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown uninstall app confirmation action: ${existing_site_app_confirmation}" 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown site apps action: ${existing_site_apps_action}" 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
done
|
||||
continue
|
||||
;;
|
||||
"Migrate site now")
|
||||
existing_site_migrate_confirm="$(
|
||||
show_manage_stack_site_migrate_confirmation \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" || true
|
||||
)"
|
||||
case "${existing_site_migrate_confirm}" in
|
||||
"Yes")
|
||||
show_warning_message "Migrating site for stack: ${stack_name}"
|
||||
if migrate_configured_stack_site "${stack_dir}"; then
|
||||
show_warning_and_wait "Site migrated successfully: ${existing_site_name}" 3
|
||||
continue
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
51)
|
||||
show_warning_and_wait "Cannot migrate site: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot migrate site for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
53)
|
||||
show_warning_and_wait "Cannot migrate site because the configured site name was empty or unsafe for cleanup operations." 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "Cannot migrate site because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
64)
|
||||
show_warning_and_wait "Site migration failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
65)
|
||||
show_warning_and_wait "The migrate command finished, but the site metadata could not be written to metadata.json." 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Site migration failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown site migrate confirmation action: ${existing_site_migrate_confirm}" 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Backup site now")
|
||||
show_warning_message "Creating backup for site: ${existing_site_name}"
|
||||
if backup_configured_stack_site "${stack_dir}"; then
|
||||
existing_site_last_backup_at="$(get_stack_site_last_backup_at "${stack_dir}" || true)"
|
||||
show_warning_and_wait "Site backup completed successfully: ${existing_site_name}${existing_site_last_backup_at:+ (last backup at ${existing_site_last_backup_at})}" 4
|
||||
continue
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
71)
|
||||
show_warning_and_wait "Cannot back up site: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
72)
|
||||
show_warning_and_wait "Cannot back up site for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
73)
|
||||
show_warning_and_wait "Cannot back up site because no configured site was found in metadata.json." 4
|
||||
;;
|
||||
74)
|
||||
show_warning_and_wait "Cannot back up site because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
75)
|
||||
show_warning_and_wait "Site backup failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
|
||||
;;
|
||||
76)
|
||||
show_warning_and_wait "The backup command finished, but the backup metadata could not be written to metadata.json." 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Site backup failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
"Delete site")
|
||||
site_delete_confirmation="$(
|
||||
show_manage_stack_site_delete_confirmation \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" || true
|
||||
)"
|
||||
case "${site_delete_confirmation}" in
|
||||
"Yes")
|
||||
show_warning_message "Deleting site for stack: ${stack_name}"
|
||||
if delete_configured_stack_site "${stack_dir}"; then
|
||||
show_warning_and_wait "Site deleted successfully with its database: ${existing_site_name}" 3
|
||||
continue
|
||||
else
|
||||
site_flow_status=$?
|
||||
fi
|
||||
case "${site_flow_status}" in
|
||||
51)
|
||||
show_warning_and_wait "Cannot delete site: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot delete site for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "Cannot delete site because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "The cleared site metadata could not be written to metadata.json." 4
|
||||
;;
|
||||
60)
|
||||
show_warning_and_wait "Site deletion could not remove all site or database data automatically. Manual cleanup is required." 5
|
||||
;;
|
||||
61)
|
||||
show_warning_and_wait "Cannot delete site because the configured site name was empty or unsafe for cleanup operations." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Site deletion failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown site delete confirmation action: ${site_delete_confirmation}" 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Back" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown site details action: ${existing_site_details_action}" 2
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Unknown site action: ${site_action}" 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
451
scripts/easy-docker/lib/app/wizard/flows/manage/stack.sh
Executable file
|
|
@ -0,0 +1,451 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_manage_selected_stack_flow() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir=""
|
||||
local stack_action=""
|
||||
local apps_action=""
|
||||
local updates_action=""
|
||||
local stack_metadata_path=""
|
||||
local stack_apps_path=""
|
||||
local stack_env_path=""
|
||||
local custom_apps_update_status=0
|
||||
local compose_start_status=0
|
||||
local stack_runtime_status=""
|
||||
local stack_frappe_branch=""
|
||||
local stack_custom_image_ref=""
|
||||
local stack_custom_tag=""
|
||||
local custom_tag_prompt_status=0
|
||||
local custom_tag_update_status=0
|
||||
local missing_custom_image_action=""
|
||||
local delete_stack_confirmation_action=""
|
||||
local delete_stack_keyword=""
|
||||
|
||||
stack_dir="$(get_stack_dir_by_name "${stack_name}" || true)"
|
||||
if [ -z "${stack_dir}" ]; then
|
||||
show_warning_and_wait "Could not resolve stack directory for '${stack_name}'." 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
get_stack_compose_runtime_status_label stack_runtime_status "${stack_dir}"
|
||||
stack_frappe_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
|
||||
stack_custom_image_ref="$(get_stack_custom_image_ref "${stack_dir}" || true)"
|
||||
stack_action="$(show_manage_stack_actions_menu "${stack_name}" "${stack_dir}" "${stack_runtime_status}" || true)"
|
||||
case "${stack_action}" in
|
||||
"Apps")
|
||||
while true; do
|
||||
apps_action="$(show_manage_stack_apps_menu "${stack_name}" "${stack_dir}" "${stack_frappe_branch}" || true)"
|
||||
case "${apps_action}" in
|
||||
"Select apps and branches")
|
||||
if update_stack_custom_modular_apps "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
custom_apps_update_status=$?
|
||||
case "${custom_apps_update_status}" in
|
||||
2 | 130)
|
||||
continue
|
||||
;;
|
||||
3)
|
||||
stack_metadata_path="${stack_dir}/metadata.json"
|
||||
show_warning_and_wait "Cannot update app selection because metadata is missing: ${stack_metadata_path}" 3
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not update app selection (${custom_apps_update_status}) for stack: ${stack_name}" 3
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
show_warning_and_wait "App selection updated in ${stack_dir}/metadata.json and ${stack_apps_path}." 3
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown apps action: ${apps_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"Updates")
|
||||
while true; do
|
||||
stack_custom_image_ref="$(get_stack_custom_image_ref "${stack_dir}" || true)"
|
||||
updates_action="$(show_manage_stack_updates_menu "${stack_name}" "${stack_dir}" "${stack_frappe_branch}" "${stack_custom_image_ref}" || true)"
|
||||
case "${updates_action}" in
|
||||
"Update selected app branches")
|
||||
if update_stack_selected_app_branches "${stack_dir}"; then
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
show_warning_and_wait "Selected app branches updated in ${stack_dir}/metadata.json and ${stack_apps_path}. Set the next custom image tag, build the updated image, restart the stack, and migrate the site to apply the update." 6
|
||||
else
|
||||
custom_apps_update_status=$?
|
||||
case "${custom_apps_update_status}" in
|
||||
2 | 130)
|
||||
continue
|
||||
;;
|
||||
3)
|
||||
stack_metadata_path="${stack_dir}/metadata.json"
|
||||
show_warning_and_wait "Cannot update selected app branches because metadata is missing: ${stack_metadata_path}" 3
|
||||
continue
|
||||
;;
|
||||
4)
|
||||
show_warning_and_wait "No selected stack apps were found for branch updates. Select apps for the stack first." 4
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not update selected app branches (${custom_apps_update_status}) for stack: ${stack_name}" 3
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
"Set next custom image tag")
|
||||
stack_env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
if [ ! -f "${stack_env_path}" ]; then
|
||||
show_warning_and_wait "Cannot update CUSTOM_TAG because the stack env file is missing: ${stack_env_path}" 4
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "$(get_stack_custom_image_name "${stack_dir}" || true)" ]; then
|
||||
show_warning_and_wait "Cannot update CUSTOM_TAG because CUSTOM_IMAGE is missing in the stack env file." 4
|
||||
continue
|
||||
fi
|
||||
|
||||
if prompt_stack_custom_image_tag_with_cancel stack_custom_tag "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
custom_tag_prompt_status=$?
|
||||
case "${custom_tag_prompt_status}" in
|
||||
2 | 130)
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not collect the next custom image tag (${custom_tag_prompt_status})." 3
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if set_stack_custom_image_tag "${stack_dir}" "${stack_custom_tag}"; then
|
||||
stack_custom_image_ref="$(get_stack_custom_image_ref "${stack_dir}" || true)"
|
||||
show_warning_message "Custom image tag updated successfully: ${stack_custom_image_ref:-${stack_custom_tag}}"
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
continue
|
||||
fi
|
||||
else
|
||||
custom_tag_update_status=$?
|
||||
case "${custom_tag_update_status}" in
|
||||
31)
|
||||
show_warning_and_wait "Cannot update CUSTOM_TAG because the stack env file is missing: ${stack_env_path}" 4
|
||||
;;
|
||||
32)
|
||||
show_warning_and_wait "Cannot update CUSTOM_TAG because the value is not a valid Docker image tag." 4
|
||||
;;
|
||||
33)
|
||||
show_warning_and_wait "Cannot update CUSTOM_TAG because CUSTOM_IMAGE is missing in the stack env file." 4
|
||||
;;
|
||||
34)
|
||||
show_warning_and_wait "Could not write the updated CUSTOM_TAG to ${stack_env_path}." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not update CUSTOM_TAG (${custom_tag_update_status})." 4
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
"Build updated image")
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown update action: ${updates_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"Start stack in Docker Compose")
|
||||
while true; do
|
||||
show_warning_message "Starting stack with docker compose: ${stack_name}"
|
||||
if start_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Stack started successfully with docker compose: ${stack_name}" 3
|
||||
break
|
||||
else
|
||||
compose_start_status=$?
|
||||
fi
|
||||
case "${compose_start_status}" in
|
||||
31)
|
||||
show_warning_and_wait "Cannot start stack: metadata.json is missing in ${stack_dir}." 4
|
||||
break
|
||||
;;
|
||||
32)
|
||||
show_warning_and_wait "Cannot start stack: stack env file not found in ${stack_dir}." 4
|
||||
break
|
||||
;;
|
||||
33)
|
||||
show_warning_and_wait "Cannot start stack: topology is missing in metadata.json. Re-run the topology wizard for this stack." 4
|
||||
break
|
||||
;;
|
||||
34)
|
||||
show_warning_and_wait "Cannot start stack via docker compose for topology '${EASY_DOCKER_COMPOSE_ERROR_DETAIL}'. Use the topology-specific runbook path." 5
|
||||
break
|
||||
;;
|
||||
35)
|
||||
show_warning_and_wait "Cannot start stack: no compose files configured in metadata.json." 4
|
||||
break
|
||||
;;
|
||||
36)
|
||||
show_warning_and_wait "Cannot start stack: compose file is missing -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4
|
||||
break
|
||||
;;
|
||||
37)
|
||||
show_warning_and_wait "docker compose up failed. Check the output above for details." 4
|
||||
break
|
||||
;;
|
||||
38)
|
||||
missing_custom_image_action="$(
|
||||
show_missing_custom_image_start_menu "${stack_name}" "${stack_dir}" "${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" || true
|
||||
)"
|
||||
case "${missing_custom_image_action}" in
|
||||
"Build custom image now")
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
continue
|
||||
fi
|
||||
break
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown missing-image action: ${missing_custom_image_action}" 2
|
||||
break
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
39)
|
||||
show_warning_and_wait "Cannot inspect custom image before start. Check Docker and try again. Details: ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
break
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Cannot start stack with docker compose (${compose_start_status})." 4
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"Restart stack in Docker Compose")
|
||||
while true; do
|
||||
show_warning_message "Restarting stack with docker compose: ${stack_name}"
|
||||
if restart_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Stack restarted successfully with docker compose: ${stack_name}" 3
|
||||
break
|
||||
else
|
||||
compose_start_status=$?
|
||||
fi
|
||||
case "${compose_start_status}" in
|
||||
57)
|
||||
show_warning_and_wait "Cannot restart stack: metadata.json is missing in ${stack_dir}." 4
|
||||
break
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "Cannot restart stack: stack env file not found in ${stack_dir}." 4
|
||||
break
|
||||
;;
|
||||
59)
|
||||
show_warning_and_wait "Cannot restart stack: topology is missing in metadata.json. Re-run the topology wizard for this stack." 4
|
||||
break
|
||||
;;
|
||||
60)
|
||||
show_warning_and_wait "Cannot restart stack via docker compose for topology '${EASY_DOCKER_COMPOSE_ERROR_DETAIL}'. Use the topology-specific runbook path." 5
|
||||
break
|
||||
;;
|
||||
61)
|
||||
show_warning_and_wait "Cannot restart stack: no compose files configured in metadata.json." 4
|
||||
break
|
||||
;;
|
||||
62)
|
||||
show_warning_and_wait "Cannot restart stack: compose file is missing -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4
|
||||
break
|
||||
;;
|
||||
63)
|
||||
show_warning_and_wait "docker compose restart failed. Check the output above for details." 4
|
||||
break
|
||||
;;
|
||||
64)
|
||||
missing_custom_image_action="$(
|
||||
show_missing_custom_image_start_menu "${stack_name}" "${stack_dir}" "${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" || true
|
||||
)"
|
||||
case "${missing_custom_image_action}" in
|
||||
"Build custom image now")
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
continue
|
||||
fi
|
||||
break
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown missing-image action: ${missing_custom_image_action}" 2
|
||||
break
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
65)
|
||||
show_warning_and_wait "Cannot inspect custom image before restart. Check Docker and try again. Details: ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
break
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Cannot restart stack with docker compose (${compose_start_status})." 4
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"Stop stack in Docker Compose")
|
||||
show_warning_message "Stopping stack with docker compose: ${stack_name}"
|
||||
if stop_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Stack stopped successfully with docker compose: ${stack_name}" 3
|
||||
continue
|
||||
else
|
||||
compose_start_status=$?
|
||||
fi
|
||||
case "${compose_start_status}" in
|
||||
41)
|
||||
show_warning_and_wait "Cannot stop stack: metadata.json is missing in ${stack_dir}." 4
|
||||
;;
|
||||
42)
|
||||
show_warning_and_wait "Cannot stop stack: stack env file not found in ${stack_dir}." 4
|
||||
;;
|
||||
43)
|
||||
show_warning_and_wait "Cannot stop stack: topology is missing in metadata.json. Re-run the topology wizard for this stack." 4
|
||||
;;
|
||||
44)
|
||||
show_warning_and_wait "Cannot stop stack via docker compose for topology '${EASY_DOCKER_COMPOSE_ERROR_DETAIL}'. Use the topology-specific runbook path." 5
|
||||
;;
|
||||
45)
|
||||
show_warning_and_wait "Cannot stop stack: no compose files configured in metadata.json." 4
|
||||
;;
|
||||
46)
|
||||
show_warning_and_wait "Cannot stop stack: compose file is missing -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4
|
||||
;;
|
||||
47)
|
||||
show_warning_and_wait "docker compose stop failed. Check the output above for details." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Cannot stop stack with docker compose (${compose_start_status})." 4
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Delete stack")
|
||||
delete_stack_confirmation_action="$(
|
||||
show_manage_stack_delete_confirmation "${stack_name}" "${stack_dir}" || true
|
||||
)"
|
||||
case "${delete_stack_confirmation_action}" in
|
||||
"Yes")
|
||||
if ! prompt_manage_stack_delete_keyword_with_cancel delete_stack_keyword "${stack_name}"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${delete_stack_keyword}" != "delete" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_message "Deleting stack with docker compose resources: ${stack_name}"
|
||||
if delete_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Stack deleted successfully with containers, networks, volumes, image, and stack directory: ${stack_name}" 5
|
||||
return "${FLOW_CONTINUE}"
|
||||
else
|
||||
compose_start_status=$?
|
||||
fi
|
||||
case "${compose_start_status}" in
|
||||
48)
|
||||
show_warning_and_wait "Cannot delete stack: metadata.json is missing in ${stack_dir}." 4
|
||||
;;
|
||||
49)
|
||||
show_warning_and_wait "Cannot delete stack: stack env file not found in ${stack_dir}." 4
|
||||
;;
|
||||
50)
|
||||
show_warning_and_wait "Cannot delete stack: topology is missing in metadata.json. Re-run the topology wizard for this stack." 4
|
||||
;;
|
||||
51)
|
||||
show_warning_and_wait "Cannot delete stack via docker compose for topology '${EASY_DOCKER_COMPOSE_ERROR_DETAIL}'. Use the topology-specific runbook path." 5
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot delete stack: no compose files configured in metadata.json." 4
|
||||
;;
|
||||
53)
|
||||
show_warning_and_wait "Cannot delete stack: compose file is missing -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "docker compose down failed. Check the output above for details." 4
|
||||
;;
|
||||
55)
|
||||
show_warning_and_wait "Stack resources were removed, but the configured custom image could not be deleted -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
;;
|
||||
56)
|
||||
show_warning_and_wait "Docker resources were removed, but the stack directory could not be deleted -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Cannot delete stack with docker compose (${compose_start_status})." 4
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown delete-stack action: ${delete_stack_confirmation_action}" 2
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Site")
|
||||
if handle_manage_stack_site_flow "${stack_name}" "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
compose_start_status=$?
|
||||
case "${compose_start_status}" in
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
"Back" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown stack action: ${stack_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
131
scripts/easy-docker/lib/app/wizard/flows/navigation.sh
Executable file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_abort_wizard_flow() {
|
||||
local stack_dir="${1}"
|
||||
local abort_action=""
|
||||
local rollback_status=0
|
||||
|
||||
abort_action="$(show_abort_wizard_prompt "${stack_dir}" || true)"
|
||||
case "${abort_action}" in
|
||||
"Rollback files and return to main menu")
|
||||
if rollback_stack_directory "${stack_dir}"; then
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
fi
|
||||
|
||||
rollback_status=$?
|
||||
if [ "${rollback_status}" -eq 2 ]; then
|
||||
show_warning_and_wait "Refused rollback for unsafe path: ${stack_dir}" 2
|
||||
else
|
||||
show_warning_and_wait "Could not rollback stack files: ${stack_dir}" 2
|
||||
fi
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
"Keep files and return to main menu")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown abort action: ${abort_action}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
handle_stack_topology_flow() {
|
||||
local stack_dir="${1}"
|
||||
local topology_action=""
|
||||
local abort_status=0
|
||||
local single_host_status=0
|
||||
local split_services_status=0
|
||||
local manage_status=0
|
||||
local stack_name=""
|
||||
|
||||
while true; do
|
||||
topology_action="$(show_stack_topology_menu "${stack_dir}" || true)"
|
||||
case "${topology_action}" in
|
||||
"Single-host" | "Single-host (recommended)")
|
||||
if handle_single_host_stack_flow "${stack_dir}"; then
|
||||
single_host_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
single_host_status=$?
|
||||
fi
|
||||
|
||||
case "${single_host_status}" in
|
||||
"${FLOW_OPEN_MANAGE_STACK}")
|
||||
stack_name="${stack_dir##*/}"
|
||||
if handle_manage_selected_stack_flow "${stack_name}"; then
|
||||
manage_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
manage_status=$?
|
||||
fi
|
||||
|
||||
case "${manage_status}" in
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Split services")
|
||||
if handle_split_services_stack_flow "${stack_dir}"; then
|
||||
split_services_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
split_services_status=$?
|
||||
fi
|
||||
|
||||
case "${split_services_status}" in
|
||||
"${FLOW_OPEN_MANAGE_STACK}")
|
||||
stack_name="${stack_dir##*/}"
|
||||
if handle_manage_selected_stack_flow "${stack_name}"; then
|
||||
manage_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
manage_status=$?
|
||||
fi
|
||||
|
||||
case "${manage_status}" in
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Abort wizard to main menu")
|
||||
handle_abort_wizard_flow "${stack_dir}"
|
||||
abort_status=$?
|
||||
case "${abort_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown topology selection: ${topology_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
312
scripts/easy-docker/lib/app/wizard/flows/setup.sh
Executable file
|
|
@ -0,0 +1,312 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_create_new_stack_flow() {
|
||||
local setup_type="${1:-production}"
|
||||
local stack_name=""
|
||||
local frappe_branch=""
|
||||
local stack_dir=""
|
||||
local create_stack_status=0
|
||||
local stack_input_status=0
|
||||
local branch_select_status=0
|
||||
local topology_status=0
|
||||
|
||||
case "${setup_type}" in
|
||||
production | development) ;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown setup type: ${setup_type}" 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
|
||||
while true; do
|
||||
stack_name=""
|
||||
if prompt_stack_name_with_cancel stack_name; then
|
||||
:
|
||||
else
|
||||
stack_input_status=$?
|
||||
if [ "${stack_input_status}" -eq "${FLOW_ABORT_INPUT}" ]; then
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Input canceled."
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
if [ -z "${stack_name}" ]; then
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
if ! is_valid_stack_name "${stack_name}"; then
|
||||
show_warning_and_wait "Invalid stack name. Use letters, numbers, dot, underscore, or hyphen." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
frappe_branch=""
|
||||
if prompt_frappe_branch_with_cancel frappe_branch "${stack_name}"; then
|
||||
:
|
||||
else
|
||||
branch_select_status=$?
|
||||
if [ "${branch_select_status}" -eq "${FLOW_ABORT_INPUT}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Could not select Frappe branch profile." 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
stack_dir=""
|
||||
if create_stack_directory_with_metadata stack_dir "${stack_name}" "${setup_type}" "${frappe_branch}"; then
|
||||
handle_stack_topology_flow "${stack_dir}"
|
||||
topology_status=$?
|
||||
case "${topology_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
create_stack_status=$?
|
||||
if [ "${create_stack_status}" -eq 2 ]; then
|
||||
show_warning_and_wait "Stack already exists: ${stack_name}" 2
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Could not create stack directory for: ${stack_name}" 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
handle_manage_existing_stacks_flow() {
|
||||
local setup_type="${1:-production}"
|
||||
local manage_action=""
|
||||
local selected_stack_status=0
|
||||
local stack_names_raw=""
|
||||
local -a stack_names=()
|
||||
|
||||
while true; do
|
||||
stack_names_raw="$(list_existing_stack_names "${setup_type}")"
|
||||
if [ -z "${stack_names_raw}" ]; then
|
||||
manage_action="$(show_manage_stacks_placeholder "${setup_type}" || true)"
|
||||
else
|
||||
mapfile -t stack_names <<<"${stack_names_raw}"
|
||||
manage_action="$(show_manage_stacks_menu "${setup_type}" "${stack_names[@]}" || true)"
|
||||
fi
|
||||
|
||||
case "${manage_action}" in
|
||||
"Back" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
if [ -n "${stack_names_raw}" ] && stack_name_in_array "${manage_action}" "${stack_names[@]}"; then
|
||||
if handle_manage_selected_stack_flow "${manage_action}"; then
|
||||
selected_stack_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
selected_stack_status=$?
|
||||
fi
|
||||
|
||||
case "${selected_stack_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
else
|
||||
show_warning_and_wait "Unknown manage-stacks action: ${manage_action}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
handle_setup_flow() {
|
||||
local setup_type="${1}"
|
||||
local setup_action=""
|
||||
|
||||
case "${setup_type}" in
|
||||
production | development) ;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown setup type: ${setup_type}" 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
|
||||
while true; do
|
||||
case "${setup_type}" in
|
||||
production)
|
||||
setup_action="$(show_production_setup_menu || true)"
|
||||
;;
|
||||
development)
|
||||
setup_action="$(show_development_setup_menu || true)"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${setup_action}" in
|
||||
"Create new stack")
|
||||
if handle_create_new_stack_flow "${setup_type}"; then
|
||||
:
|
||||
else
|
||||
case "$?" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
"Manage existing stacks")
|
||||
if handle_manage_existing_stacks_flow "${setup_type}"; then
|
||||
:
|
||||
else
|
||||
case "$?" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
"Back" | "")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown ${setup_type} action: ${setup_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
handle_production_setup_flow() {
|
||||
handle_setup_flow "production"
|
||||
}
|
||||
|
||||
handle_development_setup_flow() {
|
||||
handle_setup_flow "development"
|
||||
}
|
||||
|
||||
handle_environment_check_flow() {
|
||||
local environment_action=""
|
||||
|
||||
environment_action="$(show_environment_status || true)"
|
||||
case "${environment_action}" in
|
||||
"Back to main menu" | "")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown environment action: ${environment_action}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_easy_docker_app() {
|
||||
local action=""
|
||||
local handler_status=0
|
||||
|
||||
enter_alt_screen
|
||||
render_main_screen 1
|
||||
|
||||
while true; do
|
||||
action="$(show_main_menu || true)"
|
||||
|
||||
if [ -z "${action}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${action}" in
|
||||
"Production Stack")
|
||||
if handle_production_setup_flow; then
|
||||
handler_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
handler_status=$?
|
||||
fi
|
||||
case "${handler_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
render_main_screen 1
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return 0
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Development Stack")
|
||||
if handle_development_setup_flow; then
|
||||
handler_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
handler_status=$?
|
||||
fi
|
||||
case "${handler_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
render_main_screen 1
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return 0
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Tools")
|
||||
if handle_tools_flow; then
|
||||
handler_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
handler_status=$?
|
||||
fi
|
||||
case "${handler_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
render_main_screen 1
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return 0
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Environment check")
|
||||
if handle_environment_check_flow; then
|
||||
handler_status="${FLOW_CONTINUE}"
|
||||
else
|
||||
handler_status=$?
|
||||
fi
|
||||
case "${handler_status}" in
|
||||
"${FLOW_BACK_TO_MAIN}")
|
||||
render_main_screen 1
|
||||
;;
|
||||
"${FLOW_EXIT_APP}")
|
||||
return 0
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
;;
|
||||
"Exit")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown action: ${action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
81
scripts/easy-docker/lib/app/wizard/flows/single_host.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_single_host_stack_flow() {
|
||||
local stack_dir="${1}"
|
||||
local proxy_mode=""
|
||||
local database_choice=""
|
||||
local redis_choice=""
|
||||
local stack_env_path=""
|
||||
local stack_apps_path=""
|
||||
local generated_compose_path=""
|
||||
local save_selection_status=0
|
||||
local render_compose_status=0
|
||||
|
||||
proxy_mode="$(show_single_host_proxy_menu "${stack_dir}" || true)"
|
||||
case "${proxy_mode}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_single_host_proxy_mode_id "${proxy_mode}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown proxy mode: ${proxy_mode}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
database_choice="$(show_single_host_database_menu "${stack_dir}" || true)"
|
||||
case "${database_choice}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_single_host_database_id "${database_choice}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown database choice: ${database_choice}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
redis_choice="$(show_single_host_redis_menu "${stack_dir}" || true)"
|
||||
case "${redis_choice}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_single_host_redis_id "${redis_choice}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown Redis choice: ${redis_choice}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if save_single_host_selection "${stack_dir}" "${proxy_mode}" "${database_choice}" "${redis_choice}"; then
|
||||
:
|
||||
else
|
||||
save_selection_status=$?
|
||||
if [ "${save_selection_status}" -eq 2 ] || [ "${save_selection_status}" -eq 130 ]; then
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Could not save single-host selection for stack: ${stack_dir}" 2
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
if render_stack_compose_from_metadata "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
render_compose_status=$?
|
||||
stack_env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
show_warning_and_wait "Selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}, but compose rendering failed (${render_compose_status}) for ${generated_compose_path}." 3
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
stack_env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
show_warning_and_wait "Single-host selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}. Rendered compose: ${generated_compose_path}." 3
|
||||
return "${FLOW_OPEN_MANAGE_STACK}"
|
||||
}
|
||||
130
scripts/easy-docker/lib/app/wizard/flows/split_services.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_split_services_stack_flow() {
|
||||
local stack_dir="${1}"
|
||||
local data_mode=""
|
||||
local database_choice=""
|
||||
local redis_choice=""
|
||||
local proxy_mode=""
|
||||
local summary_action=""
|
||||
local stack_env_path=""
|
||||
local stack_apps_path=""
|
||||
local generated_compose_path=""
|
||||
local save_selection_status=0
|
||||
local render_compose_status=0
|
||||
|
||||
data_mode="$(show_split_services_data_mode_menu "${stack_dir}" || true)"
|
||||
case "${data_mode}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_split_services_data_mode_id "${data_mode}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown data services mode: ${data_mode}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
database_choice="$(show_split_services_database_menu "${stack_dir}" || true)"
|
||||
case "${database_choice}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_single_host_database_id "${database_choice}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown database choice: ${database_choice}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
redis_choice="$(show_split_services_redis_mode_menu "${stack_dir}" || true)"
|
||||
case "${redis_choice}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_split_services_redis_id "${redis_choice}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown Redis choice: ${redis_choice}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
proxy_mode="$(show_split_services_proxy_mode_menu "${stack_dir}" || true)"
|
||||
case "${proxy_mode}" in
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
if ! get_single_host_proxy_mode_id "${proxy_mode}" >/dev/null; then
|
||||
show_warning_and_wait "Unknown reverse proxy mode: ${proxy_mode}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
summary_action="$(show_split_services_summary_menu "${stack_dir}" "${data_mode}" "${database_choice}" "${redis_choice}" "${proxy_mode}" || true)"
|
||||
case "${summary_action}" in
|
||||
"Yes, write stack files") ;;
|
||||
"Back to topology selection" | "")
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
"Abort wizard to main menu")
|
||||
handle_abort_wizard_flow "${stack_dir}"
|
||||
return $?
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown split-services summary action: ${summary_action}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if save_split_services_selection "${stack_dir}" "${proxy_mode}" "${data_mode}" "${database_choice}" "${redis_choice}"; then
|
||||
:
|
||||
else
|
||||
save_selection_status=$?
|
||||
if [ "${save_selection_status}" -eq 2 ] || [ "${save_selection_status}" -eq 130 ]; then
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
case "${save_selection_status}" in
|
||||
31)
|
||||
show_warning_and_wait "Could not write the split-services env file for stack: ${stack_dir}" 3
|
||||
;;
|
||||
32)
|
||||
show_warning_and_wait "Could not write the split-services wizard metadata in ${stack_dir}/metadata.json." 3
|
||||
;;
|
||||
33)
|
||||
show_warning_and_wait "Split-services app selection is empty. Select at least one app before writing stack files." 3
|
||||
;;
|
||||
34)
|
||||
show_warning_and_wait "Could not write the selected app metadata in ${stack_dir}/metadata.json." 3
|
||||
;;
|
||||
35)
|
||||
show_warning_and_wait "Could not generate ${stack_dir}/apps.json from the selected split-services apps." 3
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Could not save split-services selection for stack: ${stack_dir} (${save_selection_status})." 3
|
||||
;;
|
||||
esac
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
if render_stack_compose_from_metadata "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
render_compose_status=$?
|
||||
stack_env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
show_warning_and_wait "Selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}, but compose rendering failed (${render_compose_status}) for ${generated_compose_path}." 3
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
stack_env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
show_warning_and_wait "Split-services selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}. Rendered compose: ${generated_compose_path}." 3
|
||||
return "${FLOW_OPEN_MANAGE_STACK}"
|
||||
}
|
||||
199
scripts/easy-docker/lib/app/wizard/flows/tools.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
prompt_tools_apps_catalog_value_with_back() {
|
||||
local result_var="${1}"
|
||||
local field_label="${2}"
|
||||
local help_text="${3}"
|
||||
local placeholder="${4:-}"
|
||||
local input_value=""
|
||||
local input_status=0
|
||||
|
||||
input_value="$(prompt_tools_apps_catalog_input "${field_label}" "${help_text}" "${placeholder}")"
|
||||
input_status=$?
|
||||
if [ "${input_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
input_value="$(printf '%s' "${input_value}" | tr -d '\r\n')"
|
||||
case "${input_value}" in
|
||||
/back | /BACK | /Back)
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf -v "${result_var}" "%s" "${input_value}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
}
|
||||
|
||||
prompt_tools_apps_default_branch_from_csv_with_back() {
|
||||
local result_var="${1}"
|
||||
local branches_csv="${2}"
|
||||
local selection=""
|
||||
local selection_status=0
|
||||
local branch=""
|
||||
local -a branch_options=()
|
||||
|
||||
IFS=',' read -r -a branch_options <<<"${branches_csv}"
|
||||
if [ "${#branch_options[@]}" -eq 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
selection="$(show_tools_apps_default_branch_menu "${branch_options[@]}")"
|
||||
selection_status=$?
|
||||
if [ "${selection_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
case "${selection}" in
|
||||
"" | "Back")
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
esac
|
||||
|
||||
branch="${selection}"
|
||||
if ! is_valid_predefined_app_branch "${branch}"; then
|
||||
return 1
|
||||
fi
|
||||
if ! csv_contains_branch "${branches_csv}" "${branch}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${branch}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
}
|
||||
|
||||
run_add_app_catalog_entry_wizard() {
|
||||
local app_id=""
|
||||
local app_label=""
|
||||
local app_repo=""
|
||||
local app_branches_csv=""
|
||||
local normalized_branches_csv=""
|
||||
local app_default_branch=""
|
||||
local input_status=0
|
||||
|
||||
if ! get_predefined_apps_catalog_entries >/dev/null 2>&1; then
|
||||
show_warning_and_wait "Could not load scripts/easy-docker/config/apps.tsv. Check format before adding new entries." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if prompt_tools_apps_catalog_value_with_back \
|
||||
app_label \
|
||||
"App Label" \
|
||||
"Display name used in the app selection list." \
|
||||
"My Custom App"; then
|
||||
:
|
||||
else
|
||||
input_status=$?
|
||||
return "${input_status}"
|
||||
fi
|
||||
|
||||
trim_predefined_catalog_field app_label "${app_label}"
|
||||
if [ -z "${app_label}" ]; then
|
||||
show_warning_and_wait "App label is required." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if predefined_app_catalog_has_label "${app_label}"; then
|
||||
show_warning_and_wait "App label already exists in apps.tsv: ${app_label}" 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! generate_predefined_app_id_from_label app_id "${app_label}"; then
|
||||
show_warning_and_wait "Could not generate a valid app id from label. Use letters/numbers and simple separators." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if predefined_app_catalog_has_id "${app_id}"; then
|
||||
show_warning_and_wait "Generated app id already exists (${app_id}). Choose a different label." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
if prompt_tools_apps_catalog_value_with_back \
|
||||
app_repo \
|
||||
"Repository URL" \
|
||||
"Git repository URL for this app." \
|
||||
"https://github.com/acme/my-custom-app"; then
|
||||
:
|
||||
else
|
||||
input_status=$?
|
||||
return "${input_status}"
|
||||
fi
|
||||
|
||||
if ! is_valid_predefined_app_repo "${app_repo}"; then
|
||||
show_warning_and_wait "Invalid repository URL. Use https/http/ssh/git formats." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
if prompt_tools_apps_catalog_value_with_back \
|
||||
app_branches_csv \
|
||||
"Branches (CSV)" \
|
||||
"Comma-separated branches for selection. Example: version-15,version-16,develop" \
|
||||
"version-15,version-16,develop"; then
|
||||
:
|
||||
else
|
||||
input_status=$?
|
||||
return "${input_status}"
|
||||
fi
|
||||
|
||||
if ! normalize_predefined_branches_csv normalized_branches_csv "${app_branches_csv}"; then
|
||||
show_warning_and_wait "Invalid branch list. Use a comma-separated list with valid branch names." 2
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
if prompt_tools_apps_default_branch_from_csv_with_back app_default_branch "${normalized_branches_csv}"; then
|
||||
:
|
||||
else
|
||||
input_status=$?
|
||||
if [ "${input_status}" -eq "${FLOW_ABORT_INPUT}" ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
show_warning_and_wait "Could not select default branch from branch list." 2
|
||||
return "${input_status}"
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
if ! append_predefined_app_catalog_entry "${app_id}" "${app_label}" "${app_repo}" "${app_default_branch}" "${normalized_branches_csv}"; then
|
||||
show_warning_and_wait "Could not append app entry to scripts/easy-docker/config/apps.tsv." 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
show_warning_and_wait "App added to apps.tsv: ${app_label} (${app_id})" 2
|
||||
return 0
|
||||
}
|
||||
|
||||
handle_tools_flow() {
|
||||
local tools_action=""
|
||||
|
||||
while true; do
|
||||
tools_action="$(show_tools_menu || true)"
|
||||
|
||||
case "${tools_action}" in
|
||||
"Add Apps for App Selection")
|
||||
run_add_app_catalog_entry_wizard || true
|
||||
;;
|
||||
"Back to main menu" | "")
|
||||
return "${FLOW_BACK_TO_MAIN}"
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown tools action: ${tools_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
267
scripts/easy-docker/lib/app/wizard/single_host.sh
Executable file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_single_host_proxy_mode_id() {
|
||||
local proxy_mode="${1}"
|
||||
|
||||
case "${proxy_mode}" in
|
||||
"Traefik (HTTP, built-in proxy)")
|
||||
printf 'traefik-http\n'
|
||||
;;
|
||||
"Traefik (HTTPS + Let's Encrypt)")
|
||||
printf 'traefik-https\n'
|
||||
;;
|
||||
"nginx-proxy (HTTP)")
|
||||
printf 'nginxproxy-http\n'
|
||||
;;
|
||||
"nginx-proxy + acme-companion (HTTPS)")
|
||||
printf 'nginxproxy-https\n'
|
||||
;;
|
||||
"Caddy (external reverse proxy)")
|
||||
printf 'caddy-external\n'
|
||||
;;
|
||||
"No reverse proxy (direct :8080)")
|
||||
printf 'no-proxy\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_single_host_proxy_overrides() {
|
||||
local proxy_mode="${1}"
|
||||
|
||||
case "${proxy_mode}" in
|
||||
"Traefik (HTTP, built-in proxy)")
|
||||
printf 'overrides/compose.proxy.yaml\n'
|
||||
;;
|
||||
"Traefik (HTTPS + Let's Encrypt)")
|
||||
printf 'overrides/compose.https.yaml\n'
|
||||
;;
|
||||
"nginx-proxy (HTTP)")
|
||||
printf 'overrides/compose.nginxproxy.yaml\n'
|
||||
;;
|
||||
"nginx-proxy + acme-companion (HTTPS)")
|
||||
printf 'overrides/compose.nginxproxy.yaml\noverrides/compose.nginxproxy-ssl.yaml\n'
|
||||
;;
|
||||
"Caddy (external reverse proxy)" | "No reverse proxy (direct :8080)")
|
||||
printf 'overrides/compose.noproxy.yaml\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_single_host_database_id() {
|
||||
local database_choice="${1}"
|
||||
|
||||
case "${database_choice}" in
|
||||
"MariaDB (recommended)")
|
||||
printf 'mariadb\n'
|
||||
;;
|
||||
"PostgreSQL")
|
||||
printf 'postgres\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_single_host_database_override() {
|
||||
local database_choice="${1}"
|
||||
|
||||
case "${database_choice}" in
|
||||
"MariaDB (recommended)")
|
||||
printf 'overrides/compose.mariadb.yaml\n'
|
||||
;;
|
||||
"PostgreSQL")
|
||||
printf 'overrides/compose.postgres.yaml\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_single_host_redis_id() {
|
||||
local redis_choice="${1}"
|
||||
|
||||
case "${redis_choice}" in
|
||||
"Include Redis (recommended)")
|
||||
printf 'enabled\n'
|
||||
;;
|
||||
"Skip Redis (experienced users only)")
|
||||
printf 'disabled\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_single_host_redis_override() {
|
||||
local redis_choice="${1}"
|
||||
|
||||
case "${redis_choice}" in
|
||||
"Include Redis (recommended)")
|
||||
printf 'overrides/compose.redis.yaml\n'
|
||||
;;
|
||||
"Skip Redis (experienced users only)")
|
||||
printf ''
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
persist_single_host_env_file() {
|
||||
local stack_dir="${1}"
|
||||
local env_lines="${2}"
|
||||
local env_path=""
|
||||
local env_tmp_path=""
|
||||
local generated_at=""
|
||||
|
||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
||||
env_tmp_path="${env_path}.tmp"
|
||||
generated_at="$(get_current_utc_timestamp)"
|
||||
|
||||
if ! {
|
||||
printf '# Generated by easy-docker wizard at %s\n' "${generated_at}"
|
||||
printf '# Adjust values as needed for this stack.\n'
|
||||
if [ -n "${env_lines}" ]; then
|
||||
printf '\n%s\n' "${env_lines}"
|
||||
else
|
||||
printf '\n# No additional environment variables configured by the wizard.\n'
|
||||
fi
|
||||
} >"${env_tmp_path}"; then
|
||||
rm -f -- "${env_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -- "${env_tmp_path}" "${env_path}"; then
|
||||
rm -f -- "${env_tmp_path}" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
persist_single_host_selection_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local proxy_mode_id="${2}"
|
||||
local database_id="${3}"
|
||||
local redis_id="${4}"
|
||||
local compose_files_lines="${5}"
|
||||
local env_lines="${6}"
|
||||
local updated_at=""
|
||||
local compose_files_json=""
|
||||
local env_json_object=""
|
||||
local wizard_json_object=""
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
compose_files_json="$(build_compose_files_json_array "${compose_files_lines}")"
|
||||
env_json_object="$(build_env_json_object "${env_lines}")"
|
||||
|
||||
if ! wizard_json_object="$(
|
||||
cat <<EOF
|
||||
{
|
||||
"topology": "single-host",
|
||||
"selection": {
|
||||
"proxy_mode_id": "${proxy_mode_id}",
|
||||
"database_id": "${database_id}",
|
||||
"redis_id": "${redis_id}"
|
||||
},
|
||||
"env": ${env_json_object},
|
||||
"compose_files": [
|
||||
${compose_files_json}
|
||||
],
|
||||
"updated_at": "${updated_at}"
|
||||
}
|
||||
EOF
|
||||
)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_metadata_wizard_object "${stack_dir}" "${wizard_json_object}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
save_single_host_selection() {
|
||||
local stack_dir="${1}"
|
||||
local proxy_mode="${2}"
|
||||
local database_choice="${3}"
|
||||
local redis_choice="${4}"
|
||||
local proxy_mode_id=""
|
||||
local database_id=""
|
||||
local redis_id=""
|
||||
local database_override=""
|
||||
local redis_override=""
|
||||
local proxy_overrides=""
|
||||
local compose_files_lines=""
|
||||
local env_lines=""
|
||||
local apps_metadata_json_object=""
|
||||
local wizard_metadata_status=0
|
||||
local apps_metadata_status=0
|
||||
local collect_env_status=0
|
||||
|
||||
proxy_mode_id="$(get_single_host_proxy_mode_id "${proxy_mode}")" || return 1
|
||||
database_id="$(get_single_host_database_id "${database_choice}")" || return 1
|
||||
redis_id="$(get_single_host_redis_id "${redis_choice}")" || return 1
|
||||
|
||||
database_override="$(get_single_host_database_override "${database_choice}")" || return 1
|
||||
redis_override="$(get_single_host_redis_override "${redis_choice}")" || return 1
|
||||
proxy_overrides="$(get_single_host_proxy_overrides "${proxy_mode}")" || return 1
|
||||
|
||||
compose_files_lines="$(printf 'compose.yaml\n%s' "${database_override}")"
|
||||
if [ -n "${redis_override}" ]; then
|
||||
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${redis_override}")"
|
||||
fi
|
||||
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${proxy_overrides}")"
|
||||
|
||||
if collect_single_host_env_lines env_lines apps_metadata_json_object "${stack_dir}" "${proxy_mode_id}" "${database_id}" "${redis_id}"; then
|
||||
:
|
||||
else
|
||||
collect_env_status=$?
|
||||
return "${collect_env_status}"
|
||||
fi
|
||||
|
||||
if ! persist_single_host_env_file "${stack_dir}" "${env_lines}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if persist_single_host_selection_metadata \
|
||||
"${stack_dir}" \
|
||||
"${proxy_mode_id}" \
|
||||
"${database_id}" \
|
||||
"${redis_id}" \
|
||||
"${compose_files_lines}" \
|
||||
"${env_lines}"; then
|
||||
:
|
||||
else
|
||||
wizard_metadata_status=$?
|
||||
return "${wizard_metadata_status}"
|
||||
fi
|
||||
|
||||
if [ -z "${apps_metadata_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if persist_stack_metadata_apps_object "${stack_dir}" "${apps_metadata_json_object}"; then
|
||||
:
|
||||
else
|
||||
apps_metadata_status=$?
|
||||
return "${apps_metadata_status}"
|
||||
fi
|
||||
|
||||
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
164
scripts/easy-docker/lib/app/wizard/split_services.sh
Executable file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_split_services_data_mode_id() {
|
||||
local data_mode="${1}"
|
||||
|
||||
case "${data_mode}" in
|
||||
"Managed Data Services")
|
||||
printf 'managed\n'
|
||||
;;
|
||||
"External Data Services")
|
||||
printf 'external\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_split_services_redis_id() {
|
||||
local redis_choice="${1}"
|
||||
|
||||
case "${redis_choice}" in
|
||||
"Managed Redis Services")
|
||||
printf 'managed\n'
|
||||
;;
|
||||
"External Redis Services")
|
||||
printf 'external\n'
|
||||
;;
|
||||
"No Redis Services")
|
||||
printf 'disabled\n'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
persist_split_services_selection_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local proxy_mode_id="${2}"
|
||||
local data_mode_id="${3}"
|
||||
local database_id="${4}"
|
||||
local redis_id="${5}"
|
||||
local compose_files_lines="${6}"
|
||||
local env_lines="${7}"
|
||||
local updated_at=""
|
||||
local compose_files_json=""
|
||||
local env_json_object=""
|
||||
local wizard_json_object=""
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
compose_files_json="$(build_compose_files_json_array "${compose_files_lines}")"
|
||||
env_json_object="$(build_env_json_object "${env_lines}")"
|
||||
|
||||
if ! wizard_json_object="$(
|
||||
cat <<EOF
|
||||
{
|
||||
"topology": "split-services",
|
||||
"selection": {
|
||||
"proxy_mode_id": "${proxy_mode_id}",
|
||||
"data_mode_id": "${data_mode_id}",
|
||||
"database_id": "${database_id}",
|
||||
"redis_id": "${redis_id}"
|
||||
},
|
||||
"env": ${env_json_object},
|
||||
"compose_files": [
|
||||
${compose_files_json}
|
||||
],
|
||||
"updated_at": "${updated_at}"
|
||||
}
|
||||
EOF
|
||||
)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! persist_stack_metadata_wizard_object "${stack_dir}" "${wizard_json_object}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
save_split_services_selection() {
|
||||
local stack_dir="${1}"
|
||||
local proxy_mode="${2}"
|
||||
local data_mode="${3}"
|
||||
local database_choice="${4}"
|
||||
local redis_choice="${5}"
|
||||
local proxy_mode_id=""
|
||||
local data_mode_id=""
|
||||
local database_id=""
|
||||
local redis_id=""
|
||||
local database_override=""
|
||||
local redis_override=""
|
||||
local proxy_overrides=""
|
||||
local compose_files_lines=""
|
||||
local env_lines=""
|
||||
local apps_metadata_json_object=""
|
||||
local collect_env_status=0
|
||||
|
||||
proxy_mode_id="$(get_single_host_proxy_mode_id "${proxy_mode}")" || return 1
|
||||
data_mode_id="$(get_split_services_data_mode_id "${data_mode}")" || return 1
|
||||
database_id="$(get_single_host_database_id "${database_choice}")" || return 1
|
||||
redis_id="$(get_split_services_redis_id "${redis_choice}")" || return 1
|
||||
|
||||
if [ "${data_mode_id}" = "managed" ]; then
|
||||
database_override="$(get_single_host_database_override "${database_choice}")" || return 1
|
||||
fi
|
||||
|
||||
if [ "${redis_id}" = "managed" ]; then
|
||||
redis_override="$(get_single_host_redis_override "Include Redis (recommended)")" || return 1
|
||||
fi
|
||||
|
||||
proxy_overrides="$(get_single_host_proxy_overrides "${proxy_mode}")" || return 1
|
||||
|
||||
compose_files_lines="compose.yaml"
|
||||
if [ -n "${database_override}" ]; then
|
||||
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${database_override}")"
|
||||
fi
|
||||
if [ -n "${redis_override}" ]; then
|
||||
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${redis_override}")"
|
||||
fi
|
||||
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${proxy_overrides}")"
|
||||
|
||||
if collect_split_services_env_lines env_lines apps_metadata_json_object "${stack_dir}" "${proxy_mode_id}" "${data_mode_id}" "${database_id}" "${redis_id}"; then
|
||||
:
|
||||
else
|
||||
collect_env_status=$?
|
||||
return "${collect_env_status}"
|
||||
fi
|
||||
|
||||
if ! persist_single_host_env_file "${stack_dir}" "${env_lines}"; then
|
||||
return 31
|
||||
fi
|
||||
|
||||
if persist_split_services_selection_metadata \
|
||||
"${stack_dir}" \
|
||||
"${proxy_mode_id}" \
|
||||
"${data_mode_id}" \
|
||||
"${database_id}" \
|
||||
"${redis_id}" \
|
||||
"${compose_files_lines}" \
|
||||
"${env_lines}"; then
|
||||
:
|
||||
else
|
||||
return 32
|
||||
fi
|
||||
|
||||
if [ -z "${apps_metadata_json_object}" ]; then
|
||||
return 33
|
||||
fi
|
||||
|
||||
if persist_stack_metadata_apps_object "${stack_dir}" "${apps_metadata_json_object}"; then
|
||||
:
|
||||
else
|
||||
return 34
|
||||
fi
|
||||
|
||||
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
return 35
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
79
scripts/easy-docker/lib/checks/docker.sh
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
docker_compose_available() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
docker_daemon_running() {
|
||||
docker info >/dev/null 2>&1
|
||||
}
|
||||
|
||||
docker_supports_command() {
|
||||
docker "$@" --help >/dev/null 2>&1
|
||||
}
|
||||
|
||||
get_missing_docker_commands() {
|
||||
local missing=()
|
||||
local subcommand=""
|
||||
|
||||
for subcommand in ps exec inspect cp build; do
|
||||
if ! docker_supports_command "${subcommand}"; then
|
||||
missing+=("docker ${subcommand}")
|
||||
fi
|
||||
done
|
||||
|
||||
for subcommand in config up down logs exec pull ps; do
|
||||
if ! docker_supports_command compose "${subcommand}"; then
|
||||
missing+=("docker compose ${subcommand}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#missing[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "${missing[@]}"
|
||||
return 1
|
||||
}
|
||||
|
||||
format_missing_commands_list() {
|
||||
local missing_commands="${1}"
|
||||
local missing_list=""
|
||||
|
||||
missing_list="$(printf '%s' "${missing_commands}" | tr '\n' ',')"
|
||||
missing_list="${missing_list%,}"
|
||||
missing_list="${missing_list#,}"
|
||||
printf '%s\n' "${missing_list}"
|
||||
}
|
||||
|
||||
ensure_docker() {
|
||||
local missing_commands=""
|
||||
local missing_list=""
|
||||
|
||||
if ! command_exists docker; then
|
||||
echo "docker is not installed."
|
||||
print_docker_install_guidance
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! docker_compose_available; then
|
||||
echo "docker compose (Compose v2 command) is not available."
|
||||
print_docker_compose_install_guidance
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! docker_daemon_running; then
|
||||
echo "docker daemon is not running."
|
||||
print_docker_daemon_start_guidance
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! missing_commands="$(get_missing_docker_commands)"; then
|
||||
missing_list="$(format_missing_commands_list "${missing_commands}")"
|
||||
echo "Missing required docker commands: ${missing_list}"
|
||||
print_docker_command_support_guidance
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
6
scripts/easy-docker/lib/checks/jq.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
jq_check_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=scripts/easy-docker/lib/install/jq/load.sh
|
||||
source "${jq_check_dir}/../install/jq/load.sh"
|
||||
39
scripts/easy-docker/lib/core/commands.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
command_exists() {
|
||||
command -v "${1}" >/dev/null 2>&1 || command -v "${1}.exe" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_with_privileges() {
|
||||
if command_exists sudo; then
|
||||
sudo "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
"$@"
|
||||
}
|
||||
|
||||
copy_binary() {
|
||||
local source_path="${1}"
|
||||
local target_path="${2}"
|
||||
|
||||
if command_exists install; then
|
||||
install -m 0755 "${source_path}" "${target_path}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
cp "${source_path}" "${target_path}" && chmod +x "${target_path}"
|
||||
}
|
||||
|
||||
copy_binary_with_privileges() {
|
||||
local source_path="${1}"
|
||||
local target_path="${2}"
|
||||
|
||||
if command_exists install; then
|
||||
run_with_privileges install -m 0755 "${source_path}" "${target_path}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
run_with_privileges cp "${source_path}" "${target_path}" &&
|
||||
run_with_privileges chmod +x "${target_path}"
|
||||
}
|
||||
26
scripts/easy-docker/lib/core/json.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_easy_docker_jq_command() {
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
printf 'jq\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v jq.exe >/dev/null 2>&1; then
|
||||
printf 'jq.exe\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
easy_docker_require_jq() {
|
||||
get_easy_docker_jq_command >/dev/null 2>&1
|
||||
}
|
||||
|
||||
easy_docker_run_jq() {
|
||||
local jq_command=""
|
||||
|
||||
jq_command="$(get_easy_docker_jq_command)" || return 127
|
||||
"${jq_command}" "$@"
|
||||
}
|
||||
39
scripts/easy-docker/lib/core/messages.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
print_manual_gum_install_guidance() {
|
||||
echo "Install gum manually: https://github.com/charmbracelet/gum#installation"
|
||||
echo "If installed into ~/.local/bin, add it to PATH first."
|
||||
}
|
||||
|
||||
print_docker_install_guidance() {
|
||||
echo "Install Docker first: https://docs.docker.com/get-started/get-docker/"
|
||||
}
|
||||
|
||||
print_docker_compose_install_guidance() {
|
||||
echo "This script requires Docker Compose v2 via the 'docker compose' command."
|
||||
echo "Docker Desktop includes it by default."
|
||||
echo "On Linux Engine-only setups, install the Docker Compose CLI plugin package (commonly 'docker-compose-plugin')."
|
||||
echo "Setup docs:"
|
||||
echo "https://docs.docker.com/compose/install/"
|
||||
echo "Note: this script uses 'docker compose' (Compose v2), not the old standalone 'docker-compose'."
|
||||
}
|
||||
|
||||
print_docker_daemon_start_guidance() {
|
||||
echo "Start the Docker daemon/service and retry."
|
||||
echo "If you use Docker Desktop, ensure it is running."
|
||||
}
|
||||
|
||||
print_docker_command_support_guidance() {
|
||||
echo "Update Docker to a recent version and ensure Compose v2 is available as 'docker compose'."
|
||||
echo "Standard 'docker' and 'docker compose' commands are required."
|
||||
}
|
||||
|
||||
print_jq_install_guidance() {
|
||||
print_manual_jq_install_guidance
|
||||
}
|
||||
|
||||
print_manual_jq_install_guidance() {
|
||||
echo "Install jq first: https://jqlang.org/download/"
|
||||
echo "On Windows, you can also use: winget install jqlang.jq"
|
||||
echo "Ensure the 'jq' or 'jq.exe' command is available on PATH before running easy-docker."
|
||||
}
|
||||