mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
refactor(easy-docker): split wizard shell modules
This commit is contained in:
parent
1a839299ab
commit
da905fb1c4
32 changed files with 4847 additions and 3521 deletions
|
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python"
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python",
|
||||
"editor.detectIndentation": false,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
|
|
|
|||
128
scripts/easy-docker/docs/development-team-process.md
Normal file
128
scripts/easy-docker/docs/development-team-process.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Easy-Docker Development Team Process
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the working model for the easy-docker team.
|
||||
Focus is process, responsibilities, and execution flow for ongoing refactoring and feature work.
|
||||
|
||||
## Team Setup
|
||||
|
||||
- Lead Developer
|
||||
- Owns scope, priorities, and release decisions.
|
||||
- Resolves conflicts between technical proposals.
|
||||
- Approves final merge readiness.
|
||||
- Senior Developer A (Correctness)
|
||||
- Reviews control flow, edge cases, and failure behavior.
|
||||
- Validates data handling, state transitions, and rollback paths.
|
||||
- Checks defensive programming and explicit error handling.
|
||||
- Senior Developer B (Architecture)
|
||||
- Reviews modularity, coupling, and naming consistency.
|
||||
- Drives DRY/KISS refactors and shared helper extraction.
|
||||
- Validates maintainability and testability.
|
||||
- Implementation Developer
|
||||
- Delivers code changes according to approved scope.
|
||||
- Keeps behavior stable unless change is explicitly requested.
|
||||
- Adds/update docs for structure and flow changes.
|
||||
- QA/Verification Owner
|
||||
- Runs pre-commit and targeted checks.
|
||||
- Executes reproducible manual test matrix for wizard paths.
|
||||
- Reports pass/fail with concrete reproduction steps.
|
||||
|
||||
## Working Agreement
|
||||
|
||||
- No hidden behavior changes during refactors.
|
||||
- Source-of-truth decisions must be explicit and documented.
|
||||
- New code must prefer existing helpers over duplicated logic.
|
||||
- Every change batch must be reviewable by concern (flow, env, compose, ui).
|
||||
|
||||
## Daily Process (Tomorrow Plan)
|
||||
|
||||
1. Kickoff (15 min)
|
||||
- Confirm target scope for the day.
|
||||
- Confirm "no functional change" boundaries.
|
||||
- Assign owners for implementation and verification.
|
||||
2. Design sync (20 min)
|
||||
- Compare at least two technical options for non-trivial edits.
|
||||
- Select one approach with short tradeoff note.
|
||||
3. Implementation blocks
|
||||
- Work in small vertical batches (one concern per batch).
|
||||
- Keep public function contracts stable where possible.
|
||||
- Update docs in the same batch when structure changes.
|
||||
4. Review blocks
|
||||
- Senior A reviews correctness and failure paths.
|
||||
- Senior B reviews architecture and maintainability.
|
||||
- Lead resolves conflicts and accepts/rejects batch.
|
||||
5. Verification block
|
||||
- Run pre-commit for changed files.
|
||||
- Run targeted manual flow checks.
|
||||
- Record results in short checklist format.
|
||||
6. Handover
|
||||
- Write what is done, what is pending, and next first task.
|
||||
- List any blockers with owner and proposed resolution.
|
||||
|
||||
## Implementation Workflow
|
||||
|
||||
1. Define scope and constraints.
|
||||
2. Map affected files/functions.
|
||||
3. Propose options and select approach.
|
||||
4. Implement with small commits by concern.
|
||||
5. Validate with checks and manual path coverage.
|
||||
6. Document final state and next steps.
|
||||
|
||||
## Review Workflow
|
||||
|
||||
1. Findings-first review format.
|
||||
2. Severity order: BLOCKER, HIGH, MEDIUM, LOW.
|
||||
3. Each point must include file reference and reason.
|
||||
4. Lead decision:
|
||||
- Approved
|
||||
- Approved with conditions
|
||||
- Not approved
|
||||
|
||||
## Test and Verification Matrix (Minimum)
|
||||
|
||||
- Create new production stack and complete wizard.
|
||||
- Create new development stack and complete wizard.
|
||||
- Manage existing stack:
|
||||
- Apps -> Generate apps.json
|
||||
- Apps -> Select apps and branches
|
||||
- Docker -> Generate docker compose from env
|
||||
- Docker -> Start stack in Docker Compose (single-host topology)
|
||||
- Abort/Back paths:
|
||||
- Back navigation in each submenu
|
||||
- Abort wizard with rollback
|
||||
- Validation paths:
|
||||
- Domain validation error then correction
|
||||
- Branch selection from apps catalog (including back-navigation)
|
||||
|
||||
## Definition of Done (Team)
|
||||
|
||||
- Scope completed with no unplanned behavior change.
|
||||
- No avoidable duplication introduced.
|
||||
- Review completed by both senior roles.
|
||||
- Lead verdict documented.
|
||||
- Verification evidence recorded.
|
||||
- Handover notes prepared for next workday.
|
||||
|
||||
## Handover Template
|
||||
|
||||
Use this at day end:
|
||||
|
||||
```text
|
||||
Date:
|
||||
Completed:
|
||||
- ...
|
||||
|
||||
In Progress:
|
||||
- ...
|
||||
|
||||
Next First Task:
|
||||
- ...
|
||||
|
||||
Blockers:
|
||||
- <owner> - <issue> - <proposed action>
|
||||
|
||||
Verification:
|
||||
- pre-commit: <pass/fail + note>
|
||||
- manual matrix: <pass/fail + note>
|
||||
```
|
||||
174
scripts/easy-docker/docs/single-stack-readiness.md
Normal file
174
scripts/easy-docker/docs/single-stack-readiness.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Easy-Docker Single-Stack Readiness
|
||||
|
||||
## Purpose
|
||||
|
||||
This document freezes the required single-stack scope for `easy-docker`
|
||||
before work moves to `separate services`.
|
||||
|
||||
Current interpretation:
|
||||
|
||||
- `single-stack` means the implemented `single-host` topology.
|
||||
- The stack must be isolated from other stacks at the Docker Compose project level.
|
||||
- The supported happy path is one usable site per stack unless a later
|
||||
product decision explicitly broadens this.
|
||||
- The current site bootstrap path always installs the full app selection
|
||||
stored on the stack itself.
|
||||
- It is not yet supported to create multiple sites in one stack with
|
||||
different app selections per site.
|
||||
|
||||
## Current Supported Scope
|
||||
|
||||
The current codebase already supports these single-stack paths:
|
||||
|
||||
- Create production stack
|
||||
- Create development stack
|
||||
- Choose `single-host` topology
|
||||
- Select proxy, database, and redis mode
|
||||
- Select apps and branches
|
||||
- Persist `metadata.json`, stack `.env`, `apps.json`
|
||||
- Render `compose.generated.yaml`
|
||||
- Manage existing stacks
|
||||
- Regenerate `apps.json`
|
||||
- Re-select apps and branches
|
||||
- Build custom image
|
||||
- Start stack with Docker Compose
|
||||
- Stop stack with Docker Compose
|
||||
- Show stack runtime status
|
||||
- Abort wizard with rollback or keep-files behavior
|
||||
- Isolate stacks through stack-specific Compose project names
|
||||
|
||||
## Definition Of Done Before Separate Services
|
||||
|
||||
Single-stack is not considered complete when containers merely run.
|
||||
It is considered complete when the user can move from stack creation
|
||||
to a usable Frappe/ERPNext site and operate that stack safely.
|
||||
|
||||
Minimum user-facing path:
|
||||
|
||||
1. Create stack
|
||||
2. Configure single-host topology
|
||||
3. Build image if needed
|
||||
4. Start stack
|
||||
5. Create/bootstrap first site
|
||||
6. Install selected apps on that site
|
||||
7. Verify site access behind the chosen proxy mode
|
||||
8. Stop/restart/down the stack
|
||||
9. Re-open manage flow and inspect status/logs
|
||||
|
||||
## Required Remaining Changes
|
||||
|
||||
### High Priority
|
||||
|
||||
- Add a documented or automated site/bootstrap path
|
||||
- create first site
|
||||
- install selected apps
|
||||
- verify site routing/access
|
||||
- Freeze the supported site model
|
||||
- recommended: one site per stack as the supported happy path
|
||||
|
||||
### Medium Priority
|
||||
|
||||
- Add remaining lifecycle operations
|
||||
- `restart`
|
||||
- `down/remove`
|
||||
- `logs`
|
||||
- Add post-start recovery guidance
|
||||
- partial start
|
||||
- failed bootstrap
|
||||
- retry after custom image rebuild
|
||||
- Add one-time cleanup/runbook note for stacks created before
|
||||
per-stack Compose project isolation
|
||||
|
||||
### Hardening Priority
|
||||
|
||||
- Keep runtime status semantics explicit
|
||||
- `Not created`
|
||||
- `Created`
|
||||
- `Running`
|
||||
- `Partial`
|
||||
- `Stopped`
|
||||
- `Restarting`
|
||||
- optional uptime hint
|
||||
- Ensure manage actions only affect the selected stack
|
||||
- Preserve safe abort/rollback behavior
|
||||
|
||||
## Required Single-Stack Paths
|
||||
|
||||
### Setup Paths
|
||||
|
||||
- Environment check
|
||||
- Create production stack
|
||||
- Create development stack
|
||||
- Complete single-host wizard
|
||||
- Back/cancel at each prompt
|
||||
- Abort wizard with rollback
|
||||
- Abort wizard while keeping files
|
||||
|
||||
### Runtime Paths
|
||||
|
||||
- Generate compose from env
|
||||
- Build custom image
|
||||
- Start stack
|
||||
- Stop stack
|
||||
- Restart stack
|
||||
- Down/remove stack resources
|
||||
- Inspect runtime status
|
||||
- Inspect logs
|
||||
|
||||
### Site Paths
|
||||
|
||||
- Create first site
|
||||
- Install selected apps on the site
|
||||
- Current limitation: the site install set is the stack app set
|
||||
- one stack -> one supported site -> one shared app selection
|
||||
- Verify the site is reachable
|
||||
- Re-open and manage the stack after restart
|
||||
|
||||
### Recovery Paths
|
||||
|
||||
- Missing custom image -> build -> retry start
|
||||
- Invalid app branch -> mapped build failure
|
||||
- Partial start -> inspect status/logs -> retry
|
||||
- Failed bootstrap -> rerun or recover cleanly
|
||||
- Cleanup of pre-isolation shared Compose leftovers
|
||||
|
||||
## Verification Matrix
|
||||
|
||||
Before calling single-stack ready, the team should execute at least:
|
||||
|
||||
1. Environment/bootstrap gate
|
||||
2. New production single-stack creation
|
||||
3. New development single-stack creation
|
||||
4. Apps regeneration/update path
|
||||
5. Compose render path
|
||||
6. Custom image build success and failure paths
|
||||
7. Start path including missing-image build/retry
|
||||
8. Stop path
|
||||
9. Runtime isolation between two stacks
|
||||
10. Runtime status in not-created/created/running/partial/stopped states
|
||||
11. Abort/back/rollback paths
|
||||
12. Validation error and correction paths
|
||||
13. Site/bootstrap reality check after stack start
|
||||
|
||||
Required automated checks on every single-stack change:
|
||||
|
||||
- `bash -n` on touched shell files
|
||||
- `pre-commit run --files <changed easy-docker files>`
|
||||
- compose render/config validation for at least one production
|
||||
and one development stack
|
||||
|
||||
## Lead Verdict
|
||||
|
||||
`single-stack` is close on the Compose/runtime side but is not yet fully done.
|
||||
|
||||
The largest remaining gap before `separate services` is the missing
|
||||
site/bootstrap lifecycle. After that, the next most important gaps are
|
||||
`restart`, `down/remove`, `logs`, and reproducible manual verification.
|
||||
|
||||
Recommended order:
|
||||
|
||||
1. Freeze single-stack site model
|
||||
2. Add site/bootstrap path
|
||||
3. Add `restart`, `down/remove`, and `logs`
|
||||
4. Run the verification matrix
|
||||
5. Move to `separate services`
|
||||
76
scripts/easy-docker/docs/wizard-flow-clean.md
Normal file
76
scripts/easy-docker/docs/wizard-flow-clean.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Easy Docker Wizard Flow (Clean View)
|
||||
|
||||
This document shows the wizard paths in a clean, forward-only view.
|
||||
Back/Cancel/Exit loops are intentionally hidden to keep the flow readable.
|
||||
|
||||
## 1) Main Wizard Paths
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Main Menu]
|
||||
A --> B[Production Setup]
|
||||
A --> C[Development Setup]
|
||||
A --> D[Environment Check]
|
||||
A --> Z[Exit]
|
||||
|
||||
B --> E[Create new stack]
|
||||
B --> F[Manage existing stacks]
|
||||
C --> E2[Create new stack]
|
||||
C --> F2[Manage existing stacks]
|
||||
|
||||
E --> G[Create stack dir + metadata.json]
|
||||
E2 --> G
|
||||
G --> H[Topology selection]
|
||||
|
||||
H --> I[Single-host flow]
|
||||
H --> J[Split services flow]
|
||||
|
||||
I --> K[Persist files + render compose]
|
||||
K --> L[Done]
|
||||
|
||||
J --> J2[Current status: placeholder only]
|
||||
J2 --> L2[Pending implementation]
|
||||
|
||||
F --> M[Select existing stack]
|
||||
F2 --> M
|
||||
M --> N[Manage stack actions]
|
||||
N --> N1[Apps actions]
|
||||
N --> N2[Docker actions]
|
||||
N1 --> O[apps.json generated/updated]
|
||||
N2 --> P[compose.generated.yaml rendered]
|
||||
N2 --> Q[Start stack in Docker Compose]
|
||||
Q --> Q1{Topology}
|
||||
Q1 -->|single-host| Q2[docker compose up -d]
|
||||
Q1 -->|split-services / others| Q3[Show runbook warning]
|
||||
```
|
||||
|
||||
## 2) Single-host Detail Path
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S1[Single-host selected]
|
||||
S1 --> S2[Choose proxy mode]
|
||||
S2 --> S3[Choose database mode]
|
||||
S3 --> S4[Choose redis mode]
|
||||
S4 --> S5[Set CUSTOM_IMAGE + CUSTOM_TAG]
|
||||
S5 --> S6[Select apps: apps catalog]
|
||||
S6 --> S7[For each selected app: fetch branches + choose branch]
|
||||
S7 --> S8[Proxy-specific questions]
|
||||
S8 --> S9[Database-specific questions]
|
||||
S9 --> S10[Write .env]
|
||||
S10 --> S11[Write metadata.json]
|
||||
S11 --> S12[Generate apps.json]
|
||||
S12 --> S13[Render compose.generated.yaml]
|
||||
S13 --> S14[Success message]
|
||||
```
|
||||
|
||||
## 3) Notes
|
||||
|
||||
- This is a readability-focused flow map, not an exhaustive state machine.
|
||||
- Navigation loops (Back/Cancel/Exit) are intentionally omitted.
|
||||
- `Split services` remains not fully implemented in the wizard runtime.
|
||||
- `Start stack in Docker Compose` currently supports only `single-host` topology.
|
||||
- Site bootstrap is currently scoped to one supported site per stack.
|
||||
- The site bootstrap installs the full app selection stored on the stack.
|
||||
- Multiple sites in one stack with different per-site app selections are
|
||||
not supported yet and are planned for a later phase.
|
||||
136
scripts/easy-docker/docs/wizard-flow.md
Normal file
136
scripts/easy-docker/docs/wizard-flow.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Easy Docker Wizard Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Main Menu] -->|Production Stack| B[Setup Menu: Production]
|
||||
A -->|Development Stack| C[Setup Menu: Development]
|
||||
A -->|Environment check| D[Environment Status]
|
||||
A -->|Exit| Z1[Exit App]
|
||||
D -->|Back to main menu| A
|
||||
D -->|Exit and close easy-docker| Z1
|
||||
|
||||
B -->|Create new stack| E[Prompt: Stack name]
|
||||
B -->|Manage existing stacks| F[List existing production stacks]
|
||||
B -->|Back| A
|
||||
B -->|Exit| Z1
|
||||
|
||||
C -->|Create new stack| E2[Prompt: Stack name]
|
||||
C -->|Manage existing stacks| F2[List existing development stacks]
|
||||
C -->|Back| A
|
||||
C -->|Exit| Z1
|
||||
|
||||
E --> E3[Select Frappe branch profile from frappe.tsv]
|
||||
E2 --> E4[Select Frappe branch profile from frappe.tsv]
|
||||
E3 --> G[Create stack directory + metadata.json]
|
||||
E4 --> G2[Create stack directory + metadata.json]
|
||||
G --> H[Topology Menu]
|
||||
G2 --> H2[Topology Menu]
|
||||
|
||||
H -->|Single-host| I[Single-host selection]
|
||||
H -->|Split services| J[Split services example]
|
||||
H -->|Abort wizard to main menu| K[Abort prompt]
|
||||
H -->|Back/Cancel| B
|
||||
H2 -->|Single-host| I
|
||||
H2 -->|Split services| J
|
||||
H2 -->|Abort wizard to main menu| K
|
||||
H2 -->|Back/Cancel| C
|
||||
|
||||
J -->|Use this topology| J2[Info: placeholder path]
|
||||
J -->|Back| H
|
||||
J2 --> H
|
||||
|
||||
K -->|Rollback files and return to main menu| A
|
||||
K -->|Keep files and return to main menu| A
|
||||
K -->|Back to topology selection| H
|
||||
|
||||
I --> I1[Proxy mode]
|
||||
I1 --> I2[Database mode]
|
||||
I2 --> I3[Redis mode]
|
||||
I3 --> I6[Prompt CUSTOM_IMAGE + CUSTOM_TAG]
|
||||
I6 --> I7[App selection list]
|
||||
I7 -->|Enter| I8[Per selected app: choose branch from apps.tsv]
|
||||
I8 --> I9[Continue]
|
||||
|
||||
I9 --> P{Proxy specific questions}
|
||||
P -->|traefik-https| P1[SITE_DOMAINS + LETSENCRYPT_EMAIL + HTTP_PUBLISH_PORT? + HTTPS_PUBLISH_PORT?]
|
||||
P -->|nginxproxy-https| P2[SITE_DOMAINS + NGINX_PROXY_HOSTS + LETSENCRYPT_EMAIL + HTTP_PUBLISH_PORT? + HTTPS_PUBLISH_PORT?]
|
||||
P -->|nginxproxy-http| P3[SITE_DOMAINS + NGINX_PROXY_HOSTS + HTTP_PUBLISH_PORT?]
|
||||
P -->|traefik-http| P4[HTTP_PUBLISH_PORT?]
|
||||
P -->|caddy-external / no-proxy| P5[HTTP_PUBLISH_PORT? default 8080]
|
||||
|
||||
P1 --> DBQ
|
||||
P2 --> DBQ
|
||||
P3 --> DBQ
|
||||
P4 --> DBQ
|
||||
P5 --> DBQ
|
||||
|
||||
DBQ{Database specific question}
|
||||
DBQ -->|postgres| DB1[DB_PASSWORD required]
|
||||
DBQ -->|mariadb| DB2[DB_PASSWORD optional]
|
||||
|
||||
DB1 --> S[Write stack env file]
|
||||
DB2 --> S
|
||||
S --> T[Write metadata.json with top-level apps]
|
||||
T --> U[Generate apps.json from metadata.json apps]
|
||||
U --> V[Render compose.generated.yaml from metadata + env]
|
||||
V --> W[Success message]
|
||||
W --> B
|
||||
|
||||
F -->|Stack selected| M[Manage selected stack]
|
||||
F -->|Back| B
|
||||
F -->|Exit| Z1
|
||||
F -->|No stacks found| F0[Manage stacks placeholder]
|
||||
F0 -->|Back| B
|
||||
F0 -->|Exit| Z1
|
||||
|
||||
F2 -->|Stack selected| M
|
||||
F2 -->|Back| C
|
||||
F2 -->|Exit| Z1
|
||||
F2 -->|No stacks found| F20[Manage stacks placeholder]
|
||||
F20 -->|Back| C
|
||||
F20 -->|Exit| Z1
|
||||
|
||||
M --> M2[Stack actions: Apps / Docker / Back / Exit]
|
||||
M2 -->|Apps| M3[Apps submenu]
|
||||
M2 -->|Docker| M4[Docker submenu]
|
||||
M2 -->|Back| M0[Return to current stack list]
|
||||
M2 -->|Exit| Z1
|
||||
M0 --> F
|
||||
M0 --> F2
|
||||
|
||||
M3 -->|Generate apps.json| M31[Read metadata.json apps + regenerate apps.json]
|
||||
M3 -->|Select apps and branches| M32[Re-prompt app and branch selection]
|
||||
M32 --> M33[Update metadata.json apps]
|
||||
M33 --> M34[Regenerate apps.json from metadata]
|
||||
M34 --> M3
|
||||
M3 -->|Back| M2
|
||||
M3 -->|Exit| Z1
|
||||
M31 --> M3
|
||||
|
||||
M4 -->|Generate docker compose from env| M41[Render compose.generated.yaml]
|
||||
M4 -->|Start stack in Docker Compose| M42[Topology gate]
|
||||
M42 -->|single-host| M43[docker compose up -d]
|
||||
M42 -->|split-services / others| M44[Show topology-specific runbook message]
|
||||
M4 -->|Back| M2
|
||||
M4 -->|Exit| Z1
|
||||
M41 --> M4
|
||||
M43 --> M4
|
||||
M44 --> M4
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `SITE_DOMAINS` validation accepts only domain names in form `sub.domain.tld` or `sub.sub.domain.tld`.
|
||||
- Existing stack lists are filtered by `setup_type` (`production` vs `development`).
|
||||
- In `Manage existing stacks`, navigation options are only `Back` and `Exit`.
|
||||
- `Select apps and branches` writes app selection to top-level `apps` in `metadata.json`.
|
||||
- `Generate apps.json` uses only `metadata.json -> apps` as source of truth.
|
||||
- New stack wizard always uses custom image path (no separate official-vs-custom image step).
|
||||
- `Start stack in Docker Compose` is currently allowed only for `single-host` topology stacks.
|
||||
|
||||
## Module Layout
|
||||
|
||||
- `lib/app/wizard/common.sh` is now a loader for common modules under `lib/app/wizard/common/`.
|
||||
- `lib/app/wizard/env.sh` is now a loader for env modules under `lib/app/wizard/env/`.
|
||||
- `lib/app/wizard/flows.sh` is now a loader for flow modules under `lib/app/wizard/flows/`.
|
||||
- Public function names and flow behavior remain unchanged; only code organization was refactored.
|
||||
|
|
@ -1,934 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
trim_predefined_catalog_field() {
|
||||
local result_var="${1}"
|
||||
local value="${2}"
|
||||
load_easy_docker_wizard_app_modules() {
|
||||
local apps_dir=""
|
||||
apps_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/apps"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
printf -v "${result_var}" "%s" "${value}"
|
||||
# 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"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"predefined"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_predefined = 1
|
||||
next
|
||||
}
|
||||
in_predefined && /\]/ {
|
||||
in_predefined = 0
|
||||
next
|
||||
}
|
||||
in_predefined {
|
||||
if (match($0, /"([^"]+)"/, parts)) {
|
||||
if (csv == "") {
|
||||
csv = parts[1]
|
||||
} else {
|
||||
csv = csv "," parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (csv != "") {
|
||||
print csv
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_custom_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"custom"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_custom = 1
|
||||
next
|
||||
}
|
||||
in_custom && /\]/ {
|
||||
in_custom = 0
|
||||
repo = ""
|
||||
branch = ""
|
||||
next
|
||||
}
|
||||
in_custom {
|
||||
if (match($0, /"repo"[[:space:]]*:[[:space:]]*"([^"]+)"/, repo_parts)) {
|
||||
repo = repo_parts[1]
|
||||
}
|
||||
if (match($0, /"branch"[[:space:]]*:[[:space:]]*"([^"]+)"/, branch_parts)) {
|
||||
branch = branch_parts[1]
|
||||
}
|
||||
if (repo != "" && branch != "") {
|
||||
print repo "|" branch
|
||||
repo = ""
|
||||
branch = ""
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"predefined_branches"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_predefined_branches = 1
|
||||
next
|
||||
}
|
||||
in_predefined_branches && /}/ {
|
||||
in_predefined_branches = 0
|
||||
next
|
||||
}
|
||||
in_predefined_branches {
|
||||
if (match($0, /"([^"]+)"[[:space:]]*:[[:space:]]*"([^"]+)"/, parts)) {
|
||||
print parts[1] "|" parts[2]
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_for_id() {
|
||||
local metadata_path="${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 < <(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
persist_stack_metadata_apps_object() {
|
||||
local stack_dir="${1}"
|
||||
local apps_json_object="${2}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${apps_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! awk -v apps_object="${apps_json_object}" '
|
||||
BEGIN {
|
||||
in_top_level_apps = 0
|
||||
apps_depth = 0
|
||||
inserted = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_top_level_apps && $0 ~ /^ "apps"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"apps\": " apps_object ","
|
||||
in_top_level_apps = 1
|
||||
inserted = 1
|
||||
if ($0 ~ /{/) {
|
||||
apps_depth += gsub(/{/, "{", $0)
|
||||
apps_depth -= gsub(/}/, "}", $0)
|
||||
} else {
|
||||
apps_depth = 0
|
||||
}
|
||||
if (apps_depth <= 0) {
|
||||
in_top_level_apps = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (in_top_level_apps) {
|
||||
apps_depth += gsub(/{/, "{", $0)
|
||||
apps_depth -= gsub(/}/, "}", $0)
|
||||
if (apps_depth <= 0) {
|
||||
in_top_level_apps = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^ "wizard"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"apps\": " apps_object ","
|
||||
inserted = 1
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}/) {
|
||||
if (prev != "") {
|
||||
if (prev !~ /,[[:space:]]*$/) {
|
||||
prev = prev ","
|
||||
}
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
print " \"apps\": " apps_object
|
||||
inserted = 1
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
flush_prev()
|
||||
if (!inserted) {
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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_wizard_object() {
|
||||
local stack_dir="${1}"
|
||||
local wizard_json_object="${2}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${wizard_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! awk -v wizard_object="${wizard_json_object}" '
|
||||
BEGIN {
|
||||
in_top_level_wizard = 0
|
||||
wizard_depth = 0
|
||||
inserted = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_top_level_wizard && $0 ~ /^ "wizard"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"wizard\": " wizard_object
|
||||
in_top_level_wizard = 1
|
||||
inserted = 1
|
||||
if ($0 ~ /{/) {
|
||||
wizard_depth += gsub(/{/, "{", $0)
|
||||
wizard_depth -= gsub(/}/, "}", $0)
|
||||
} else {
|
||||
wizard_depth = 0
|
||||
}
|
||||
if (wizard_depth <= 0) {
|
||||
in_top_level_wizard = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (in_top_level_wizard) {
|
||||
wizard_depth += gsub(/{/, "{", $0)
|
||||
wizard_depth -= gsub(/}/, "}", $0)
|
||||
if (wizard_depth <= 0) {
|
||||
in_top_level_wizard = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}/) {
|
||||
if (prev != "") {
|
||||
if (prev !~ /,[[:space:]]*$/) {
|
||||
prev = prev ","
|
||||
}
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
print " \"wizard\": " wizard_object
|
||||
inserted = 1
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
flush_prev()
|
||||
if (!inserted) {
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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
|
||||
}
|
||||
load_easy_docker_wizard_app_modules
|
||||
|
|
|
|||
490
scripts/easy-docker/lib/app/wizard/common/apps/catalog.sh
Executable file
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
|
||||
}
|
||||
378
scripts/easy-docker/lib/app/wizard/common/apps/metadata.sh
Executable file
378
scripts/easy-docker/lib/app/wizard/common/apps/metadata.sh
Executable file
|
|
@ -0,0 +1,378 @@
|
|||
#!/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
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"predefined"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_predefined = 1
|
||||
next
|
||||
}
|
||||
in_predefined && /\]/ {
|
||||
in_predefined = 0
|
||||
next
|
||||
}
|
||||
in_predefined {
|
||||
if (match($0, /"([^"]+)"/, parts)) {
|
||||
if (csv == "") {
|
||||
csv = parts[1]
|
||||
} else {
|
||||
csv = csv "," parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (csv != "") {
|
||||
print csv
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_custom_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"custom"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_custom = 1
|
||||
next
|
||||
}
|
||||
in_custom && /\]/ {
|
||||
in_custom = 0
|
||||
repo = ""
|
||||
branch = ""
|
||||
next
|
||||
}
|
||||
in_custom {
|
||||
if (match($0, /"repo"[[:space:]]*:[[:space:]]*"([^"]+)"/, repo_parts)) {
|
||||
repo = repo_parts[1]
|
||||
}
|
||||
if (match($0, /"branch"[[:space:]]*:[[:space:]]*"([^"]+)"/, branch_parts)) {
|
||||
branch = branch_parts[1]
|
||||
}
|
||||
if (repo != "" && branch != "") {
|
||||
print repo "|" branch
|
||||
repo = ""
|
||||
branch = ""
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"apps"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_apps = 1
|
||||
}
|
||||
in_apps && /"predefined_branches"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_predefined_branches = 1
|
||||
next
|
||||
}
|
||||
in_predefined_branches && /}/ {
|
||||
in_predefined_branches = 0
|
||||
next
|
||||
}
|
||||
in_predefined_branches {
|
||||
if (match($0, /"([^"]+)"[[:space:]]*:[[:space:]]*"([^"]+)"/, parts)) {
|
||||
print parts[1] "|" parts[2]
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_apps_predefined_branch_for_id() {
|
||||
local metadata_path="${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 < <(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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=""
|
||||
|
||||
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 ! awk -v object_key="${object_key}" -v object_json="${object_json}" -v insert_before_key="${insert_before_key}" '
|
||||
BEGIN {
|
||||
target_regex = "^ \"" object_key "\"[[:space:]]*:"
|
||||
before_regex = ""
|
||||
if (insert_before_key != "") {
|
||||
before_regex = "^ \"" insert_before_key "\"[[:space:]]*:"
|
||||
}
|
||||
in_target = 0
|
||||
target_depth = 0
|
||||
inserted = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_target && $0 ~ target_regex) {
|
||||
flush_prev()
|
||||
if (object_key == "wizard") {
|
||||
print " \"" object_key "\": " object_json
|
||||
} else {
|
||||
print " \"" object_key "\": " object_json ","
|
||||
}
|
||||
in_target = 1
|
||||
inserted = 1
|
||||
if ($0 ~ /{/) {
|
||||
target_depth += gsub(/{/, "{", $0)
|
||||
target_depth -= gsub(/}/, "}", $0)
|
||||
} else {
|
||||
target_depth = 0
|
||||
}
|
||||
if (target_depth <= 0) {
|
||||
in_target = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (in_target) {
|
||||
target_depth += gsub(/{/, "{", $0)
|
||||
target_depth -= gsub(/}/, "}", $0)
|
||||
if (target_depth <= 0) {
|
||||
in_target = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && before_regex != "" && $0 ~ before_regex) {
|
||||
flush_prev()
|
||||
print " \"" object_key "\": " object_json ","
|
||||
inserted = 1
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}/) {
|
||||
if (prev != "") {
|
||||
if (prev !~ /,[[:space:]]*$/) {
|
||||
prev = prev ","
|
||||
}
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
print " \"" object_key "\": " object_json
|
||||
inserted = 1
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
flush_prev()
|
||||
if (!inserted) {
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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}"
|
||||
}
|
||||
191
scripts/easy-docker/lib/app/wizard/common/apps/persistence.sh
Executable file
191
scripts/easy-docker/lib/app/wizard/common/apps/persistence.sh
Executable file
|
|
@ -0,0 +1,191 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
persist_stack_metadata_apps_object() {
|
||||
local stack_dir="${1}"
|
||||
local apps_json_object="${2}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${apps_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! awk -v apps_object="${apps_json_object}" '
|
||||
BEGIN {
|
||||
in_top_level_apps = 0
|
||||
apps_depth = 0
|
||||
inserted = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_top_level_apps && $0 ~ /^ "apps"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"apps\": " apps_object ","
|
||||
in_top_level_apps = 1
|
||||
inserted = 1
|
||||
if ($0 ~ /{/) {
|
||||
apps_depth += gsub(/{/, "{", $0)
|
||||
apps_depth -= gsub(/}/, "}", $0)
|
||||
} else {
|
||||
apps_depth = 0
|
||||
}
|
||||
if (apps_depth <= 0) {
|
||||
in_top_level_apps = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (in_top_level_apps) {
|
||||
apps_depth += gsub(/{/, "{", $0)
|
||||
apps_depth -= gsub(/}/, "}", $0)
|
||||
if (apps_depth <= 0) {
|
||||
in_top_level_apps = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^ "wizard"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"apps\": " apps_object ","
|
||||
inserted = 1
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}/) {
|
||||
if (prev != "") {
|
||||
if (prev !~ /,[[:space:]]*$/) {
|
||||
prev = prev ","
|
||||
}
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
print " \"apps\": " apps_object
|
||||
inserted = 1
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
flush_prev()
|
||||
if (!inserted) {
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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_wizard_object() {
|
||||
local stack_dir="${1}"
|
||||
local wizard_json_object="${2}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${wizard_json_object}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! awk -v wizard_object="${wizard_json_object}" '
|
||||
BEGIN {
|
||||
in_top_level_wizard = 0
|
||||
wizard_depth = 0
|
||||
inserted = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_top_level_wizard && $0 ~ /^ "wizard"[[:space:]]*:/) {
|
||||
flush_prev()
|
||||
print " \"wizard\": " wizard_object
|
||||
in_top_level_wizard = 1
|
||||
inserted = 1
|
||||
if ($0 ~ /{/) {
|
||||
wizard_depth += gsub(/{/, "{", $0)
|
||||
wizard_depth -= gsub(/}/, "}", $0)
|
||||
} else {
|
||||
wizard_depth = 0
|
||||
}
|
||||
if (wizard_depth <= 0) {
|
||||
in_top_level_wizard = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (in_top_level_wizard) {
|
||||
wizard_depth += gsub(/{/, "{", $0)
|
||||
wizard_depth -= gsub(/}/, "}", $0)
|
||||
if (wizard_depth <= 0) {
|
||||
in_top_level_wizard = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}/) {
|
||||
if (prev != "") {
|
||||
if (prev !~ /,[[:space:]]*$/) {
|
||||
prev = prev ","
|
||||
}
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
print " \"wizard\": " wizard_object
|
||||
inserted = 1
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
flush_prev()
|
||||
if (!inserted) {
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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
|
||||
}
|
||||
193
scripts/easy-docker/lib/app/wizard/common/compose/runtime/lifecycle.sh
Executable file
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_single_host_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_single_host_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_single_host_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
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_single_host_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")
|
||||
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
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") ;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "N/A (${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
|
||||
}
|
||||
|
|
@ -1,566 +1,17 @@
|
|||
#!/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=()
|
||||
load_easy_docker_compose_lifecycle_modules() {
|
||||
local lifecycle_dir=""
|
||||
lifecycle_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/start"
|
||||
|
||||
# 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") ;;
|
||||
*)
|
||||
# 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
|
||||
# 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/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"
|
||||
}
|
||||
|
||||
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") ;;
|
||||
*)
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
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") ;;
|
||||
*)
|
||||
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
|
||||
}
|
||||
|
||||
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") ;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "N/A (${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
|
||||
}
|
||||
load_easy_docker_compose_lifecycle_modules
|
||||
|
|
|
|||
110
scripts/easy-docker/lib/app/wizard/common/compose/start/delete.sh
Executable file
110
scripts/easy-docker/lib/app/wizard/common/compose/start/delete.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
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") ;;
|
||||
*)
|
||||
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
|
||||
}
|
||||
131
scripts/easy-docker/lib/app/wizard/common/compose/start/start.sh
Executable file
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") ;;
|
||||
*)
|
||||
# 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
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") ;;
|
||||
*)
|
||||
printf -v "${result_var}" "%s" "N/A (${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
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") ;;
|
||||
*)
|
||||
# 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
89
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/errors.sh
Executable file
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
|
||||
}
|
||||
409
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/lifecycle.sh
Executable file
409
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/lifecycle.sh
Executable file
|
|
@ -0,0 +1,409 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
drop_stack_site_database() {
|
||||
local stack_dir="${1}"
|
||||
local db_name="${2}"
|
||||
local db_password=""
|
||||
local db_endpoint=""
|
||||
local db_host=""
|
||||
local db_port=""
|
||||
local drop_db_command=""
|
||||
|
||||
db_password="$(get_stack_database_root_password "${stack_dir}")"
|
||||
db_endpoint="$(get_stack_common_db_endpoint "${stack_dir}" || true)"
|
||||
db_host="${db_endpoint%%|*}"
|
||||
db_port="${db_endpoint#*|}"
|
||||
|
||||
if [ -z "${db_host}" ] || [ -z "${db_port}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
drop_db_command="$(
|
||||
printf "mysql --protocol=TCP -h %s -P %s -u root -p%s -e %s" \
|
||||
"$(shell_quote_site_command_arg "${db_host}")" \
|
||||
"$(shell_quote_site_command_arg "${db_port}")" \
|
||||
"$(printf '%s' "${db_password}" | sed "s/'/'\"'\"'/g")" \
|
||||
"$(shell_quote_site_command_arg "DROP DATABASE IF EXISTS \`${db_name}\`; DROP USER IF EXISTS '${db_name}'@'%'; DROP USER IF EXISTS '${db_name}'@'localhost'; FLUSH PRIVILEGES;")"
|
||||
)"
|
||||
|
||||
if ! run_stack_backend_bash_command "${stack_dir}" "${drop_db_command}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
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}" "${db_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=""
|
||||
|
||||
create_site_command="$(
|
||||
printf "bench new-site %s --mariadb-user-host-login-scope='%%' --admin-password %s --db-root-username root --db-root-password %s" \
|
||||
"$(shell_quote_site_command_arg "${site_name}")" \
|
||||
"$(shell_quote_site_command_arg "${admin_password}")" \
|
||||
"$(shell_quote_site_command_arg "$(get_stack_database_root_password "${stack_dir}")")"
|
||||
)"
|
||||
|
||||
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}" \
|
||||
"apps_installing" \
|
||||
"${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
|
||||
}
|
||||
|
||||
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 app_install_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}" "requested" "" "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}" "creating" "" "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
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "created" "" "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
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "ready" "${installed_app_lines}" "install-apps" "" "" "${created_at}" "${updated_at}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
157
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/runtime.sh
Executable file
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) ;;
|
||||
*)
|
||||
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
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
|
||||
}
|
||||
118
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/validation.sh
Executable file
118
scripts/easy-docker/lib/app/wizard/common/site/bootstrap/validation.sh
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/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
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
|
@ -1,362 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_metadata_site_string_field() {
|
||||
local metadata_path="${1}"
|
||||
local field_name="${2}"
|
||||
load_easy_docker_site_metadata_modules() {
|
||||
local metadata_dir=""
|
||||
metadata_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/metadata"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk -v field_name="${field_name}" '
|
||||
/"site"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
in_site {
|
||||
if (match($0, "\"" field_name "\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"", parts)) {
|
||||
print parts[1]
|
||||
exit
|
||||
}
|
||||
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
# 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"
|
||||
}
|
||||
|
||||
get_metadata_site_apps_installed_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"site"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
in_site && /"apps_installed"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_apps_installed = 1
|
||||
next
|
||||
}
|
||||
in_apps_installed && /\]/ {
|
||||
in_apps_installed = 0
|
||||
next
|
||||
}
|
||||
in_apps_installed {
|
||||
if (match($0, /"([^"]+)"/, parts)) {
|
||||
print parts[1]
|
||||
}
|
||||
}
|
||||
in_site {
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_stack_site_name() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "name"
|
||||
}
|
||||
|
||||
get_stack_site_state() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "state"
|
||||
}
|
||||
|
||||
get_stack_site_created_at() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "created_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_state=""
|
||||
|
||||
site_state="$(get_stack_site_state "${stack_dir}" || true)"
|
||||
case "${site_state}" in
|
||||
created | apps_installing | ready)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_stack_site_status_label() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_state=""
|
||||
local site_name=""
|
||||
local site_status_label=""
|
||||
|
||||
site_state="$(get_stack_site_state "${stack_dir}" || true)"
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
|
||||
case "${site_state}" in
|
||||
"")
|
||||
site_status_label="Not configured"
|
||||
;;
|
||||
requested)
|
||||
site_status_label="Requested"
|
||||
;;
|
||||
creating)
|
||||
site_status_label="Creating"
|
||||
;;
|
||||
created)
|
||||
site_status_label="Created"
|
||||
;;
|
||||
apps_installing)
|
||||
site_status_label="Installing apps"
|
||||
;;
|
||||
ready)
|
||||
site_status_label="Ready"
|
||||
;;
|
||||
failed)
|
||||
site_status_label="Failed"
|
||||
;;
|
||||
*)
|
||||
site_status_label="${site_state}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${site_name}" ]; then
|
||||
site_status_label="${site_status_label} (${site_name})"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${site_status_label}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_site_menu_entry() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name=""
|
||||
local site_status_label=""
|
||||
local menu_entry=""
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if [ -z "${site_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
get_stack_site_status_label site_status_label "${stack_dir}"
|
||||
menu_entry="$(printf "%s | %s" "${site_name}" "${site_status_label}")"
|
||||
printf -v "${result_var}" "%s" "${menu_entry}"
|
||||
return 0
|
||||
}
|
||||
|
||||
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 site_state="${4:-not_created}"
|
||||
local apps_installed_lines="${5:-}"
|
||||
local last_action="${6:-}"
|
||||
local last_error="${7:-}"
|
||||
local error_log_path="${8:-}"
|
||||
local created_at="${9:-}"
|
||||
local updated_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 "state": "%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 }' \
|
||||
"$(json_escape_string "${site_mode}")" \
|
||||
"$(json_escape_string "${site_name}")" \
|
||||
"$(json_escape_string "${site_state}")" \
|
||||
"${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}")"
|
||||
}
|
||||
|
||||
persist_stack_site_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local site_mode="${2:-single-site}"
|
||||
local site_name="${3:-}"
|
||||
local site_state="${4:-not_created}"
|
||||
local apps_installed_lines="${5:-}"
|
||||
local last_action="${6:-}"
|
||||
local last_error="${7:-}"
|
||||
local error_log_path="${8:-}"
|
||||
local created_at="${9:-}"
|
||||
local updated_at="${10:-}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
local site_json_object=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
|
||||
|
||||
if ! awk -v site_object="${site_json_object}" '
|
||||
BEGIN {
|
||||
in_site = 0
|
||||
inserted = 0
|
||||
site_depth = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_site && $0 ~ /^ "site"[[:space:]]*:[[:space:]]*{/) {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
|
||||
if (in_site) {
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
in_site = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}$/) {
|
||||
if (prev != "") {
|
||||
if (prev ~ /,$/) {
|
||||
print prev
|
||||
} else {
|
||||
print prev ","
|
||||
}
|
||||
prev = ""
|
||||
}
|
||||
print " \"site\": " site_object
|
||||
print "}"
|
||||
inserted = 1
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
if (!inserted) {
|
||||
if (prev != "") {
|
||||
if (prev ~ /,$/) {
|
||||
print prev
|
||||
} else {
|
||||
print prev ","
|
||||
}
|
||||
}
|
||||
print " \"site\": " site_object
|
||||
print "}"
|
||||
} else if (prev != "") {
|
||||
print prev
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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
|
||||
}
|
||||
|
||||
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}" "failed" "${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" "" "not_created" "" "delete-site" "" "" "" "${updated_at}"
|
||||
}
|
||||
load_easy_docker_site_metadata_modules
|
||||
|
|
|
|||
184
scripts/easy-docker/lib/app/wizard/common/site/metadata/read.sh
Executable file
184
scripts/easy-docker/lib/app/wizard/common/site/metadata/read.sh
Executable file
|
|
@ -0,0 +1,184 @@
|
|||
#!/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
|
||||
|
||||
awk -v field_name="${field_name}" '
|
||||
/"site"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
in_site {
|
||||
if (match($0, "\"" field_name "\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"", parts)) {
|
||||
print parts[1]
|
||||
exit
|
||||
}
|
||||
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_metadata_site_apps_installed_lines() {
|
||||
local metadata_path="${1}"
|
||||
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
/"site"[[:space:]]*:[[:space:]]*{/ {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
in_site && /"apps_installed"[[:space:]]*:[[:space:]]*\[/ {
|
||||
in_apps_installed = 1
|
||||
next
|
||||
}
|
||||
in_apps_installed && /\]/ {
|
||||
in_apps_installed = 0
|
||||
next
|
||||
}
|
||||
in_apps_installed {
|
||||
if (match($0, /"([^"]+)"/, parts)) {
|
||||
print parts[1]
|
||||
}
|
||||
}
|
||||
in_site {
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "${metadata_path}"
|
||||
}
|
||||
|
||||
get_stack_site_name() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "name"
|
||||
}
|
||||
|
||||
get_stack_site_state() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "state"
|
||||
}
|
||||
|
||||
get_stack_site_created_at() {
|
||||
local stack_dir="${1}"
|
||||
|
||||
get_metadata_site_string_field "${stack_dir}/metadata.json" "created_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_state=""
|
||||
|
||||
site_state="$(get_stack_site_state "${stack_dir}" || true)"
|
||||
case "${site_state}" in
|
||||
created | apps_installing | ready)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_stack_site_status_label() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_state=""
|
||||
local site_name=""
|
||||
local site_status_label=""
|
||||
|
||||
site_state="$(get_stack_site_state "${stack_dir}" || true)"
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
|
||||
case "${site_state}" in
|
||||
"")
|
||||
site_status_label="Not configured"
|
||||
;;
|
||||
requested)
|
||||
site_status_label="Requested"
|
||||
;;
|
||||
creating)
|
||||
site_status_label="Creating"
|
||||
;;
|
||||
created)
|
||||
site_status_label="Created"
|
||||
;;
|
||||
apps_installing)
|
||||
site_status_label="Installing apps"
|
||||
;;
|
||||
ready)
|
||||
site_status_label="Ready"
|
||||
;;
|
||||
failed)
|
||||
site_status_label="Failed"
|
||||
;;
|
||||
*)
|
||||
site_status_label="${site_state}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${site_name}" ]; then
|
||||
site_status_label="${site_status_label} (${site_name})"
|
||||
fi
|
||||
|
||||
printf -v "${result_var}" "%s" "${site_status_label}"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_site_menu_entry() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name=""
|
||||
local site_status_label=""
|
||||
local menu_entry=""
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if [ -z "${site_name}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
get_stack_site_status_label site_status_label "${stack_dir}"
|
||||
menu_entry="$(printf "%s | %s" "${site_name}" "${site_status_label}")"
|
||||
printf -v "${result_var}" "%s" "${menu_entry}"
|
||||
return 0
|
||||
}
|
||||
179
scripts/easy-docker/lib/app/wizard/common/site/metadata/write.sh
Executable file
179
scripts/easy-docker/lib/app/wizard/common/site/metadata/write.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/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 site_state="${4:-not_created}"
|
||||
local apps_installed_lines="${5:-}"
|
||||
local last_action="${6:-}"
|
||||
local last_error="${7:-}"
|
||||
local error_log_path="${8:-}"
|
||||
local created_at="${9:-}"
|
||||
local updated_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 "state": "%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 }' \
|
||||
"$(json_escape_string "${site_mode}")" \
|
||||
"$(json_escape_string "${site_name}")" \
|
||||
"$(json_escape_string "${site_state}")" \
|
||||
"${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}")"
|
||||
}
|
||||
|
||||
persist_stack_site_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local site_mode="${2:-single-site}"
|
||||
local site_name="${3:-}"
|
||||
local site_state="${4:-not_created}"
|
||||
local apps_installed_lines="${5:-}"
|
||||
local last_action="${6:-}"
|
||||
local last_error="${7:-}"
|
||||
local error_log_path="${8:-}"
|
||||
local created_at="${9:-}"
|
||||
local updated_at="${10:-}"
|
||||
local metadata_path=""
|
||||
local metadata_tmp_path=""
|
||||
local site_json_object=""
|
||||
|
||||
metadata_path="${stack_dir}/metadata.json"
|
||||
metadata_tmp_path="${metadata_path}.tmp"
|
||||
if [ ! -f "${metadata_path}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
|
||||
|
||||
if ! awk -v site_object="${site_json_object}" '
|
||||
BEGIN {
|
||||
in_site = 0
|
||||
inserted = 0
|
||||
site_depth = 0
|
||||
prev = ""
|
||||
}
|
||||
function flush_prev() {
|
||||
if (prev != "") {
|
||||
print prev
|
||||
prev = ""
|
||||
}
|
||||
}
|
||||
{
|
||||
if (!in_site && $0 ~ /^ "site"[[:space:]]*:[[:space:]]*{/) {
|
||||
in_site = 1
|
||||
site_depth = 1
|
||||
next
|
||||
}
|
||||
|
||||
if (in_site) {
|
||||
line = $0
|
||||
open_count = gsub(/{/, "{", line)
|
||||
close_count = gsub(/}/, "}", line)
|
||||
site_depth += open_count - close_count
|
||||
if (site_depth <= 0) {
|
||||
in_site = 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if (!inserted && $0 ~ /^}$/) {
|
||||
if (prev != "") {
|
||||
if (prev ~ /,$/) {
|
||||
print prev
|
||||
} else {
|
||||
print prev ","
|
||||
}
|
||||
prev = ""
|
||||
}
|
||||
print " \"site\": " site_object
|
||||
print "}"
|
||||
inserted = 1
|
||||
next
|
||||
}
|
||||
|
||||
flush_prev()
|
||||
prev = $0
|
||||
}
|
||||
END {
|
||||
if (!inserted) {
|
||||
if (prev != "") {
|
||||
if (prev ~ /,$/) {
|
||||
print prev
|
||||
} else {
|
||||
print prev ","
|
||||
}
|
||||
}
|
||||
print " \"site\": " site_object
|
||||
print "}"
|
||||
} else if (prev != "") {
|
||||
print prev
|
||||
}
|
||||
}
|
||||
' "${metadata_path}" >"${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
|
||||
}
|
||||
|
||||
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}" "failed" "${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" "" "not_created" "" "delete-site" "" "" "" "${updated_at}"
|
||||
}
|
||||
|
|
@ -1,662 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
run_build_stack_custom_image_with_feedback() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
local build_image_status=0
|
||||
load_easy_docker_manage_flow_modules() {
|
||||
local manage_dir=""
|
||||
manage_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/manage"
|
||||
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
|
||||
build_image_status=$?
|
||||
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
|
||||
;;
|
||||
18)
|
||||
show_warning_and_wait "Custom image build failed: base64 command is not available in this environment." 4
|
||||
;;
|
||||
19)
|
||||
show_warning_and_wait "Custom image build failed: apps.json could not be base64-encoded." 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
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Custom image build failed (${build_image_status})." 4
|
||||
;;
|
||||
esac
|
||||
|
||||
return "${build_image_status}"
|
||||
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/docker.sh
|
||||
source "${manage_dir}/docker.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"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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_details_action=""
|
||||
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 and selected stack apps were installed: ${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 MariaDB-backed single-host stacks." 4
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "The site state 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
|
||||
;;
|
||||
*)
|
||||
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)"
|
||||
if stack_backend_service_is_running "${stack_dir}" >/dev/null 2>&1; then
|
||||
get_stack_site_runtime_selected_apps_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}" || true
|
||||
)"
|
||||
case "${existing_site_details_action}" in
|
||||
"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
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
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 state 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
|
||||
}
|
||||
|
||||
handle_manage_selected_stack_flow() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir=""
|
||||
local stack_action=""
|
||||
local apps_action=""
|
||||
local docker_action=""
|
||||
local stack_metadata_path=""
|
||||
local stack_apps_path=""
|
||||
local custom_apps_update_status=0
|
||||
local persist_apps_status=0
|
||||
local render_compose_status=0
|
||||
local compose_start_status=0
|
||||
local generated_compose_path=""
|
||||
local stack_runtime_status=""
|
||||
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_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}" || true)"
|
||||
case "${apps_action}" in
|
||||
"Regenerate apps.json from metadata")
|
||||
stack_metadata_path="${stack_dir}/metadata.json"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
if [ ! -f "${stack_metadata_path}" ]; then
|
||||
show_warning_and_wait "Cannot generate apps.json because metadata is missing: ${stack_metadata_path}" 3
|
||||
continue
|
||||
fi
|
||||
|
||||
if persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
persist_apps_status=$?
|
||||
show_warning_and_wait "Could not generate ${stack_apps_path} (${persist_apps_status})." 3
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "apps.json generated successfully: ${stack_apps_path}" 3
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
fi
|
||||
|
||||
compose_start_status=$?
|
||||
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}"
|
||||
fi
|
||||
|
||||
compose_start_status=$?
|
||||
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
|
||||
;;
|
||||
"Docker")
|
||||
while true; do
|
||||
docker_action="$(show_manage_stack_docker_menu "${stack_name}" "${stack_dir}" || true)"
|
||||
case "${docker_action}" in
|
||||
"Build custom image")
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
"Generate docker compose from env")
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
if render_stack_compose_from_metadata "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
render_compose_status=$?
|
||||
show_warning_and_wait "Could not generate docker compose (${render_compose_status}) for ${generated_compose_path}." 3
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Docker compose generated successfully: ${generated_compose_path}" 3
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown docker action: ${docker_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"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
|
||||
}
|
||||
load_easy_docker_manage_flow_modules
|
||||
|
|
|
|||
64
scripts/easy-docker/lib/app/wizard/flows/manage/build.sh
Executable file
64
scripts/easy-docker/lib/app/wizard/flows/manage/build.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
|
||||
build_image_status=$?
|
||||
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
|
||||
;;
|
||||
18)
|
||||
show_warning_and_wait "Custom image build failed: base64 command is not available in this environment." 4
|
||||
;;
|
||||
19)
|
||||
show_warning_and_wait "Custom image build failed: apps.json could not be base64-encoded." 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
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Custom image build failed (${build_image_status})." 4
|
||||
;;
|
||||
esac
|
||||
|
||||
return "${build_image_status}"
|
||||
}
|
||||
64
scripts/easy-docker/lib/app/wizard/flows/manage/docker.sh
Executable file
64
scripts/easy-docker/lib/app/wizard/flows/manage/docker.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
|
||||
build_image_status=$?
|
||||
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
|
||||
;;
|
||||
18)
|
||||
show_warning_and_wait "Custom image build failed: base64 command is not available in this environment." 4
|
||||
;;
|
||||
19)
|
||||
show_warning_and_wait "Custom image build failed: apps.json could not be base64-encoded." 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
|
||||
;;
|
||||
*)
|
||||
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
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
|
||||
}
|
||||
185
scripts/easy-docker/lib/app/wizard/flows/manage/site.sh
Executable file
185
scripts/easy-docker/lib/app/wizard/flows/manage/site.sh
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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_details_action=""
|
||||
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 and selected stack apps were installed: ${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 MariaDB-backed single-host stacks." 4
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "The site state 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
|
||||
;;
|
||||
*)
|
||||
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)"
|
||||
if stack_backend_service_is_running "${stack_dir}" >/dev/null 2>&1; then
|
||||
get_stack_site_runtime_selected_apps_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}" || true
|
||||
)"
|
||||
case "${existing_site_details_action}" in
|
||||
"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
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
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 state 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
|
||||
}
|
||||
321
scripts/easy-docker/lib/app/wizard/flows/manage/stack.sh
Executable file
321
scripts/easy-docker/lib/app/wizard/flows/manage/stack.sh
Executable file
|
|
@ -0,0 +1,321 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
handle_manage_selected_stack_flow() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir=""
|
||||
local stack_action=""
|
||||
local apps_action=""
|
||||
local docker_action=""
|
||||
local stack_metadata_path=""
|
||||
local stack_apps_path=""
|
||||
local custom_apps_update_status=0
|
||||
local persist_apps_status=0
|
||||
local render_compose_status=0
|
||||
local compose_start_status=0
|
||||
local generated_compose_path=""
|
||||
local stack_runtime_status=""
|
||||
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_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}" || true)"
|
||||
case "${apps_action}" in
|
||||
"Regenerate apps.json from metadata")
|
||||
stack_metadata_path="${stack_dir}/metadata.json"
|
||||
stack_apps_path="${stack_dir}/apps.json"
|
||||
if [ ! -f "${stack_metadata_path}" ]; then
|
||||
show_warning_and_wait "Cannot generate apps.json because metadata is missing: ${stack_metadata_path}" 3
|
||||
continue
|
||||
fi
|
||||
|
||||
if persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
persist_apps_status=$?
|
||||
show_warning_and_wait "Could not generate ${stack_apps_path} (${persist_apps_status})." 3
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "apps.json generated successfully: ${stack_apps_path}" 3
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
fi
|
||||
|
||||
compose_start_status=$?
|
||||
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}"
|
||||
fi
|
||||
|
||||
compose_start_status=$?
|
||||
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
|
||||
;;
|
||||
"Docker")
|
||||
while true; do
|
||||
docker_action="$(show_manage_stack_docker_menu "${stack_name}" "${stack_dir}" || true)"
|
||||
case "${docker_action}" in
|
||||
"Build custom image")
|
||||
if run_build_stack_custom_image_with_feedback "${stack_name}" "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
"Generate docker compose from env")
|
||||
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
|
||||
if render_stack_compose_from_metadata "${stack_dir}"; then
|
||||
:
|
||||
else
|
||||
render_compose_status=$?
|
||||
show_warning_and_wait "Could not generate docker compose (${render_compose_status}) for ${generated_compose_path}." 3
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_and_wait "Docker compose generated successfully: ${generated_compose_path}" 3
|
||||
;;
|
||||
"Back" | "")
|
||||
break
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown docker action: ${docker_action}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
"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
|
||||
}
|
||||
Loading…
Reference in a new issue