Skip to content

Commit 6af7daf

Browse files
authored
Merge pull request #51 from geniusdynamics/dynamic
Dynamic Based Erpnext Image Builds.
2 parents b5d2ac4 + 042eff5 commit 6af7daf

26 files changed

+1694
-289
lines changed

UPGRADE.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Upgrade to V4
2+
3+
## Overview
4+
5+
In the new ns8-erpnext we build docker at runtime where you pass in the apps.json you need to build your app.
6+
7+
## Architecture
8+
9+
The module uses a runtime Docker image building approach where:
10+
11+
- Custom apps are defined via JSON configuration
12+
- Docker images are built on-demand based on the apps.json configuration
13+
- Podman is used for container management
14+
15+
## Configuration Settings UI
16+
17+
The Settings page (`ui/src/views/Settings.vue`) provides the following configuration options:
18+
19+
### 1. FQDN (Fully Qualified Domain Name)
20+
21+
- **Field**: Host/URL input
22+
- **Purpose**: Set the URL where ERPNext will be accessible
23+
- **Example**: `erpnext.example.org`
24+
- **Validation**: Required field
25+
26+
### 2. TLS/SSL Configuration
27+
28+
- **Let's Encrypt**: Toggle to enable automatic SSL certificate generation
29+
- **HTTP to HTTPS Redirect**: Toggle to force HTTPS redirects (enabled by default)
30+
31+
### 3. Frappe Version Selection
32+
33+
- **Options**:
34+
- `version-15` (default)
35+
- `version-16`
36+
- **Purpose**: Select the Frappe framework version to use
37+
38+
### 4. App Management
39+
40+
#### Adding Custom Apps
41+
42+
Apps can be added via the UI modal with the following fields:
43+
44+
- **App Name** (required): The application identifier
45+
- **Repository URL** (required): Git repository URL for the app
46+
- **Branch**: Git branch to use (defaults to "main")
47+
- **Labels**: Comma-separated labels for the app
48+
49+
#### App Management Features
50+
51+
- **Add App via Form**: Opens a modal to add new apps
52+
- **Edit App**: Modify existing app details
53+
- **Remove App**: Delete apps from the configuration
54+
- **Copy JSON**: Copy the current apps.json to clipboard
55+
- **JSON Editor**: Advanced users can directly edit the apps.json in the accordion section
56+
57+
#### Apps Display
58+
59+
Apps are displayed in a structured list showing:
60+
61+
- App Name
62+
- URL
63+
- Branch
64+
- Labels
65+
- Actions (Edit/Remove buttons)
66+
67+
### 5. ERPNext Modules Selection
68+
69+
- **Multi-select component** showing available modules
70+
- **Dynamic population**: Options are generated from the apps.json configuration
71+
- **Pre-selected values**: Previously selected modules are restored when loading configuration
72+
- **Filtering**: When apps are removed, their modules are automatically deselected
73+
74+
### 6. Podman Images (Advanced)
75+
76+
- **View**: Lists all built Podman images with details (Repository, Tag, ID, Created, Size)
77+
- **Refresh**: Button to refresh the images list
78+
- **Purpose**: Monitor built Docker images
79+
80+
## Data Flow
81+
82+
1. **Configuration Loading** (`getConfiguration`):
83+
- Fetches current configuration from backend
84+
- Decodes base64 appJson
85+
- Restores selected modules
86+
- Populates all form fields
87+
88+
2. **App JSON Processing**:
89+
- Stored as base64 encoded string in backend
90+
- Parsed into structured list for display
91+
- Used to generate multi-select options
92+
- Filters selected modules to only include valid apps
93+
94+
3. **Configuration Saving** (`configureModule`):
95+
- Validates host field
96+
- Encodes appJson to base64
97+
- Sends all configuration data to backend
98+
- Triggers module reconfiguration
99+
100+
## JSON Format
101+
102+
The `app_json` field expects an array of app objects:
103+
104+
```json
105+
[
106+
{
107+
"app_name": "my-custom-app",
108+
"url": "https://github.com/user/repo",
109+
"branch": "main",
110+
"labels": "production,custom"
111+
}
112+
]
113+
```
114+
115+
## Important Notes
116+
117+
1. **App Name Consistency**: The multi-select uses `app_name` or `name` field to match selected modules with available options
118+
2. **Base64 Encoding**: The appJson is base64 encoded when sent to the backend
119+
3. **Validation**: Apps must have at least an app_name and URL
120+
4. **Podman Integration**: The module interfaces with Podman for container management
121+
5. **Dynamic Options**: The ERPNext Modules multi-select options are dynamically generated from the apps.json
122+
123+
**NOTE**
124+
125+
- Always ensure apps.json is valid JSON before saving
126+
- Selected modules are filtered to only include apps that exist in the configuration
127+
- Podman images are specific to the configured apps and Frappe version
128+
- Make sure you install the apps that were previously installed to avoid installtion issues

build-images.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ repobase="${REPOBASE:-ghcr.io/geniusdynamics}"
1515
# Configure the image name
1616
reponame="erpnext"
1717

18-
app_version="15.92.1"
19-
2018
# Create a new empty container image
2119
container=$(buildah from scratch)
2220

@@ -47,7 +45,7 @@ buildah config --entrypoint=/ \
4745
--label="org.nethserver.authorizations=traefik@node:routeadm" \
4846
--label="org.nethserver.tcp-ports-demand=1" \
4947
--label="org.nethserver.rootfull=0" \
50-
--label="org.nethserver.images=docker.io/mariadb:10.11.5 docker.io/geniusdynamics/erpnext:${app_version} docker.io/redis:6.2-alpine" \
48+
--label="org.nethserver.images=docker.io/mariadb:10.11.5 docker.io/redis:6.2-alpine" \
5149
"${container}"
5250
# Commit the image
5351
buildah commit "${container}" "${repobase}/${reponame}"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2022 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
import json
9+
import sys
10+
import agent
11+
12+
# Try to parse the stdin as JSON.
13+
# If parsing fails, output everything to stderr
14+
data = json.load(sys.stdin)
15+
16+
# This is specific to you module, so you need to change it accordingly.
17+
18+
ERP_NEXT_MODULES = data.get("erpSelectedModules", [])
19+
APP_JSON = data.get("appJson")
20+
FRAPPE_VERSION = data.get("frappeVersion", "version-15")
21+
22+
agent.write_envfile(
23+
"erpnext-modules.env", {"ERP_NEXT_MODULES": ",".join(ERP_NEXT_MODULES) if ERP_NEXT_MODULES else "", "APPS_JSON": APP_JSON, "FRAPPE_VERSION": FRAPPE_VERSION}
24+
)
25+
26+
agent.dump_env()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
echo "Building Docker Image"
4+
../configure-module/30build_docker_image
5+
6+
echo "Docker Image Built"

imageroot/actions/configure-module/10configure_environment_vars

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import agent
1313
# If parsing fails, output everything to stderr
1414
data = json.load(sys.stdin)
1515

16-
#This is specific to you module, so you need to change it accordingly.
16+
# This is specific to you module, so you need to change it accordingly.
1717

1818
ERP_NEXT_MODULES = data.get("erpSelectedModules", [])
19+
APP_JSON = data.get("appJson")
20+
FRAPPE_VERSION = data.get("frappeVersion", "version-15")
1921

20-
agent.write_envfile("erpnext-modules.env", {
21-
"ERP_NEXT_MODULES": ERP_NEXT_MODULES
22-
})
22+
ERP_NEXT_MODULES_STR = ",".join(ERP_NEXT_MODULES) if ERP_NEXT_MODULES else ""
23+
24+
agent.write_envfile(
25+
"erpnext-modules.env", {"ERP_NEXT_MODULES": ERP_NEXT_MODULES_STR, "APPS_JSON": APP_JSON, "FRAPPE_VERSION": FRAPPE_VERSION}
26+
)
2327

2428
agent.dump_env()
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/bin/bash
2+
3+
set -e
4+
source erpnext-modules.env
5+
# Colors for output
6+
RED='\033[0;31m'
7+
GREEN='\033[0;32m'
8+
YELLOW='\033[1;33m'
9+
NC='\033[0m' # No Color
10+
11+
# Function to print colored output
12+
print_info() {
13+
echo -e "${GREEN}[INFO]${NC} $1"
14+
}
15+
16+
print_warning() {
17+
echo -e "${YELLOW}[WARNING]${NC} $1"
18+
}
19+
20+
print_error() {
21+
echo -e "${RED}[ERROR]${NC} $1"
22+
}
23+
24+
# Check if podman is installed
25+
if ! command -v podman &>/dev/null; then
26+
print_error "podman is required but not installed. Please install podman first."
27+
exit 1
28+
fi
29+
30+
# Check if jq is installed
31+
if ! command -v jq &>/dev/null; then
32+
print_error "jq is required but not installed. Please install jq first."
33+
exit 1
34+
fi
35+
36+
# Function to fetch latest ERPNext tag from GitHub based on FRAPPE_VERSION
37+
fetch_erpnext_tag() {
38+
local major_version="${FRAPPE_VERSION##version-}"
39+
local api_url="https://api.github.com/repos/frappe/erpnext/tags"
40+
41+
local tags_json
42+
if ! tags_json=$(curl -fsSL "$api_url" 2>/dev/null); then
43+
print_warning "Failed to fetch tags from GitHub, using default"
44+
return 1
45+
fi
46+
47+
local first_tag
48+
if ! first_tag=$(echo "$tags_json" | jq -r ".[] | select(.name | startswith(\"v${major_version}.\")) | .name" 2>/dev/null | head -1); then
49+
print_warning "Failed to parse tags, using default"
50+
return 1
51+
fi
52+
53+
if [ -z "$first_tag" ] || [ "$first_tag" = "null" ]; then
54+
print_warning "No v${major_version} tags found, using default"
55+
return 1
56+
fi
57+
58+
echo "$first_tag"
59+
return 0
60+
}
61+
62+
# Cache file for app_json hash
63+
CACHE_FILE=".app_json_cache"
64+
CURRENT_APPS_JSON="$APPS_JSON"
65+
66+
# Function to compute hash of the apps JSON
67+
compute_apps_hash() {
68+
echo -n "$1" | sha256sum | cut -d' ' -f1
69+
}
70+
71+
# Get current apps JSON hash
72+
CURRENT_HASH=$(compute_apps_hash "$CURRENT_APPS_JSON")
73+
74+
# Check if cache exists and compare
75+
if [ -f "$CACHE_FILE" ]; then
76+
CACHED_HASH=$(cat "$CACHE_FILE")
77+
if [ "$CURRENT_HASH" = "$CACHED_HASH" ]; then
78+
print_warning "App configuration unchanged. Skipping Docker image build."
79+
print_info "To force a rebuild, delete the cache file: rm $CACHE_FILE"
80+
exit 0
81+
else
82+
print_info "App configuration changed. Proceeding with Docker image build..."
83+
fi
84+
else
85+
print_info "No cache found. Building Docker image..."
86+
fi
87+
88+
# Fetch latest ERPNext tag from GitHub based on FRAPPE_VERSION
89+
ERPNEXT_TAG=$(fetch_erpnext_tag) || true
90+
91+
if [ -z "${ERPNEXT_TAG:-}" ]; then
92+
print_warning "Could not fetch ERPNext tag, using fallback"
93+
ERPNEXT_TAG="v${FRAPPE_VERSION##version-}.0.0"
94+
fi
95+
96+
print_info "Using ERPNext tag: $ERPNEXT_TAG"
97+
98+
ERPNEXT_IMAGE="frappe/erpnext:${ERPNEXT_TAG}"
99+
100+
print_info "ERPNEXT_IMAGE forcibly set to: $ERPNEXT_IMAGE"
101+
# Extract image name and tag from ERPNEXT_IMAGE
102+
BASE_IMAGE="$ERPNEXT_IMAGE"
103+
# Extract repository and tag
104+
IMAGE_REPO=$(echo "$ERPNEXT_IMAGE" | cut -d':' -f1)
105+
IMAGE_TAG=$(echo "$ERPNEXT_IMAGE" | cut -d':' -f2)
106+
107+
# Use the same tag for custom image
108+
CUSTOM_IMAGE_NAME="${IMAGE_REPO}"
109+
CUSTOM_IMAGE_TAG="$IMAGE_TAG"
110+
111+
print_info "Base Image: $BASE_IMAGE"
112+
print_info "Custom Image will be: $CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG"
113+
114+
print_info "Apps to be installed:"
115+
echo "$APPS_JSON"
116+
117+
FRAPPE_BRANCH="${FRAPPE_VERSION}"
118+
119+
print_info "Using Frappe branch: $FRAPPE_BRANCH"
120+
print_info "Building custom ERPNext image with Podman..."
121+
122+
# Build the image with Podman
123+
podman build --network=host \
124+
--jobs=4 \
125+
--build-arg APPS_JSON_BASE64="$APPS_JSON" \
126+
--build-arg FRAPPE_BRANCH="$FRAPPE_BRANCH" \
127+
-t "$CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG" \
128+
-f ../actions/configure-module/Dockerfile \
129+
.
130+
131+
if [ $? -eq 0 ]; then
132+
print_info "Build completed successfully!"
133+
print_info "Image tagged as: $CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG"
134+
135+
# Also tag as latest
136+
podman tag "$CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG" "$CUSTOM_IMAGE_NAME:latest"
137+
138+
# Tag with the same name as base image (for drop-in replacement)
139+
podman tag "$CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG" "$BASE_IMAGE"
140+
print_info "Also tagged as: $BASE_IMAGE (drop-in replacement)"
141+
142+
echo ""
143+
print_info "To push the image to a registry:"
144+
echo " podman push $CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG"
145+
echo " podman push $CUSTOM_IMAGE_NAME:latest"
146+
echo " podman push $BASE_IMAGE"
147+
148+
echo ""
149+
print_info "Image size:"
150+
podman images "$CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
151+
ENV_FILE="environment"
152+
153+
# Ensure env file exists
154+
[ -f "$ENV_FILE" ] || {
155+
print_error "Env file not found"
156+
exit 1
157+
}
158+
159+
CUSTOM_IMAGE_FULL="$CUSTOM_IMAGE_NAME:$CUSTOM_IMAGE_TAG"
160+
161+
# Update or add ERPNEXT_IMAGE
162+
if grep -q '^ERPNEXT_IMAGE=' "$ENV_FILE"; then
163+
sed -i "s|^ERPNEXT_IMAGE=.*|ERPNEXT_IMAGE=$CUSTOM_IMAGE_FULL|" "$ENV_FILE"
164+
else
165+
echo "ERPNEXT_IMAGE=$CUSTOM_IMAGE_FULL" >>"$ENV_FILE"
166+
fi
167+
168+
print_info "Updated ERPNEXT_IMAGE in $ENV_FILE to:"
169+
print_info "$CUSTOM_IMAGE_FULL" print_info "To use this image, it's already tagged as: $BASE_IMAGE"
170+
print_info "Your existing docker-compose or deployment will automatically use the custom image!"
171+
172+
# Update the cache with the new hash
173+
echo "$CURRENT_HASH" >"$CACHE_FILE"
174+
print_info "Cache updated."
175+
else
176+
print_error "Build failed!"
177+
exit 1
178+
fi

0 commit comments

Comments
 (0)