mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-22 15:55:09 +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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
trim_predefined_catalog_field() {
|
load_easy_docker_wizard_app_modules() {
|
||||||
local result_var="${1}"
|
local apps_dir=""
|
||||||
local value="${2}"
|
apps_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/apps"
|
||||||
|
|
||||||
value="${value#"${value%%[![:space:]]*}"}"
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps/catalog.sh
|
||||||
value="${value%"${value##*[![:space:]]}"}"
|
source "${apps_dir}/catalog.sh"
|
||||||
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/apps/metadata.sh
|
||||||
printf -v "${result_var}" "%s" "${value}"
|
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() {
|
load_easy_docker_wizard_app_modules
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
start_stack_with_compose_from_metadata() {
|
load_easy_docker_compose_lifecycle_modules() {
|
||||||
local stack_dir="${1}"
|
local lifecycle_dir=""
|
||||||
local metadata_path=""
|
lifecycle_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/start"
|
||||||
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.
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/start.sh
|
||||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
|
source "${lifecycle_dir}/start.sh"
|
||||||
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/stop.sh
|
||||||
metadata_path="${stack_dir}/metadata.json"
|
source "${lifecycle_dir}/stop.sh"
|
||||||
env_path="$(get_stack_env_path "${stack_dir}")"
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/delete.sh
|
||||||
compose_project_name="$(get_stack_compose_project_name "${stack_dir}")"
|
source "${lifecycle_dir}/delete.sh"
|
||||||
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start/status.sh
|
||||||
if [ ! -f "${metadata_path}" ]; then
|
source "${lifecycle_dir}/status.sh"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_stack_with_compose_from_metadata() {
|
load_easy_docker_compose_lifecycle_modules
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
get_metadata_site_string_field() {
|
load_easy_docker_site_metadata_modules() {
|
||||||
local metadata_path="${1}"
|
local metadata_dir=""
|
||||||
local field_name="${2}"
|
metadata_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/metadata"
|
||||||
|
|
||||||
if [ ! -f "${metadata_path}" ]; then
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/metadata/read.sh
|
||||||
return 1
|
source "${metadata_dir}/read.sh"
|
||||||
fi
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/site/metadata/write.sh
|
||||||
|
source "${metadata_dir}/write.sh"
|
||||||
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() {
|
load_easy_docker_site_metadata_modules
|
||||||
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}"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
run_build_stack_custom_image_with_feedback() {
|
load_easy_docker_manage_flow_modules() {
|
||||||
local stack_name="${1}"
|
local manage_dir=""
|
||||||
local stack_dir="${2}"
|
manage_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/manage"
|
||||||
local build_image_status=0
|
|
||||||
|
|
||||||
show_warning_message "Starting docker build for stack: ${stack_name}"
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/docker.sh
|
||||||
if build_stack_custom_image "${stack_dir}"; then
|
source "${manage_dir}/docker.sh"
|
||||||
show_warning_and_wait "Custom image build finished successfully for stack: ${stack_name}" 3
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/prompts.sh
|
||||||
return 0
|
source "${manage_dir}/prompts.sh"
|
||||||
fi
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/site.sh
|
||||||
|
source "${manage_dir}/site.sh"
|
||||||
build_image_status=$?
|
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/stack.sh
|
||||||
case "${build_image_status}" in
|
source "${manage_dir}/stack.sh"
|
||||||
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}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_manage_stack_site_name_with_cancel() {
|
load_easy_docker_manage_flow_modules
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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