diff --git a/.dockerignore b/.dockerignore index 41fc335..fc89dc1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,5 @@ .env -upload/ -cache/ *.log -vendor/ -.env.local -.env.*.local +.DS_Store +.git/ +.gitignore diff --git a/.env b/.env index 18059af..388afd8 100644 --- a/.env +++ b/.env @@ -1,16 +1,16 @@ -# SuiteCRM Docker Compose Configuration -# Copy to .env and customize +# SugarCRM 6.5.26 CE Docker Umgebung +# Passwörter ANPASSEN vor erstem Start! -# SuiteCRM -SUITECRM_PORT=8080 -SUITECRM_SITE_URL=http://localhost:8080 +# Ports +SUGARCRM_PORT=2080 +MYSQL_PORT=3306 -# MariaDB -MYSQL_PORT=3307 +# MySQL / MariaDB MYSQL_ROOT_PASSWORD=change_this_root_password -MYSQL_DATABASE=suitecrm -MYSQL_USER=suitecrm +MYSQL_DATABASE=sugarcrm +MYSQL_USER=sugarcrm MYSQL_PASSWORD=change_this_db_password -# Redis (only with --profile full or --profile redis) -REDIS_PORT=6379 +# SugarCRM Admin (wird bei Erst-Installation gesetzt) +SUGARCRM_ADMIN_USER=admin +SUGARCRM_ADMIN_PASSWORD=admin123 diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 869775d..8b7ff43 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -1,24 +1,19 @@ -name: Docker Build & Push +name: Docker Build & Push SugarCRM 6.5 CE on: push: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: build-and-push: runs-on: ubuntu-latest - container: - image: docker:27-dind - options: --privileged steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Gitea Container Registry uses: docker/login-action@v3 with: @@ -26,13 +21,12 @@ jobs: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} - - name: Build and Push - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: | - git.kgessner.de/luiicode/sugar-crm:latest - git.kgessner.de/luiicode/sugar-crm:7.15.1 - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Build Docker Image + run: | + docker build -t git.kgessner.de/luiicode/sugar-crm:6.5.26 . + docker tag git.kgessner.de/luiicode/sugar-crm:6.5.26 git.kgessner.de/luiicode/sugar-crm:latest + + - name: Push to Registry + run: | + docker push git.kgessner.de/luiicode/sugar-crm:6.5.26 + docker push git.kgessner.de/luiicode/sugar-crm:latest diff --git a/Dockerfile b/Dockerfile index efab976..a16aaea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,96 +1,52 @@ -# SuiteCRM 7.15.1 - PHP 8.1 + Apache -FROM php:8.1-apache +FROM php:5.6-apache-jessie -LABEL maintainer="Kevin Gessner" -LABEL description="SuiteCRM 7.15.1 containerized with PHP 8.1 and Apache" +ENV MAJOR_VERSION=6.5 +ENV MINOR_VERSION=26 +ENV WWW_FOLDER=/var/www/html +ENV DEBIAN_FRONTEND=noninteractive +ENV SUGARCRM_REPO=https://github.com/bklein01/sugarcrm +ENV SUGARCRM_COMMIT=71125a3 -# Install system dependencies and PHP extensions -RUN set -eux; \ - apt-get update && apt-get install -y --no-install-recommends \ - # Core - libzip-dev \ +# Jessie is EOL - switch to archive.debian.org +RUN echo "deb http://archive.debian.org/debian/ jessie main contrib non-free" > /etc/apt/sources.list && \ + echo "deb http://archive.debian.org/debian-security/ jessie/updates main contrib non-free" >> /etc/apt/sources.list && \ + echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid-until && \ + apt-get update -o Acquire::Check-Valid-Until=false && apt-get upgrade -y --force-yes && \ + apt-get install -y --force-yes \ + libcurl4-gnutls-dev \ libpng-dev \ - libjpeg-dev \ - libfreetype6-dev \ - libonig-dev \ - libxml2-dev \ - libldap2-dev \ + unzip \ + cron \ + re2c \ + python \ + curl \ libc-client-dev \ libkrb5-dev \ - libcurl4-openssl-dev \ - libicu-dev \ - # Utils - unzip \ - wget \ - curl \ - cron \ - msmtp \ - # Cleanup - && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \ - && docker-php-ext-install -j$(nproc) \ - pdo \ - pdo_mysql \ - mysqli \ - gd \ - mbstring \ - zip \ - xml \ - curl \ - ldap \ - imap \ - intl \ - calendar \ - opcache \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + git \ + && rm -r /var/lib/apt/lists/* -# Configure PHP for SuiteCRM -RUN { \ - echo 'memory_limit = 512M'; \ - echo 'upload_max_filesize = 64M'; \ - echo 'post_max_size = 64M'; \ - echo 'max_execution_time = 600'; \ - echo 'max_input_time = 600'; \ - echo 'display_errors = Off'; \ - echo 'log_errors = On'; \ - echo 'date.timezone = Europe/Berlin'; \ - } > /usr/local/etc/php/conf.d/suitecrm.ini +# Clone SugarCRM 6.5.26 CE from GitHub mirror +RUN git clone --depth 1 ${SUGARCRM_REPO} ${WWW_FOLDER} \ + && cd ${WWW_FOLDER} && git checkout ${SUGARCRM_COMMIT} 2>/dev/null || true \ + && rm -rf ${WWW_FOLDER}/.git \ + && chown -R www-data:www-data ${WWW_FOLDER} -# Configure OPcache -RUN { \ - echo 'opcache.memory_consumption=256'; \ - echo 'opcache.interned_strings_buffer=16'; \ - echo 'opcache.max_accelerated_files=20000'; \ - echo 'opcache.revalidate_freq=2'; \ - echo 'opcache.fast_shutdown=1'; \ - } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# PHP upload limits +COPY docker-php-ext-filesize.ini /usr/local/etc/php/conf.d/docker-php-ext-filesize.ini -# SuiteCRM version -ENV SUITECRM_VERSION=7.15.1 -ENV SUITECRM_SHA256=468b811addd21dfb29d411ee6e815dbdf7099f912347e88cd3e8d010d829db7a +# PHP extensions +RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \ + docker-php-ext-install imap mysql zip gd -# Download and extract SuiteCRM -RUN set -eux; \ - wget -q "https://github.com/salesagility/SuiteCRM/releases/download/v${SUITECRM_VERSION}/SuiteCRM-${SUITECRM_VERSION}.zip" \ - -O /tmp/suitecrm.zip; \ - echo "${SUITECRM_SHA256} /tmp/suitecrm.zip" | sha256sum -c -; \ - unzip -q /tmp/suitecrm.zip -d /var/www/html/; \ - rm /tmp/suitecrm.zip; \ - chown -R www-data:www-data /var/www/html +# Copy entrypoint and templates +COPY config_override.php.pyt /usr/local/src/config_override.php.pyt +COPY envtemplate.py /usr/local/bin/envtemplate.py +COPY init.sh /usr/local/bin/init.sh +RUN chmod u+x /usr/local/bin/init.sh -# Apache configuration -RUN a2enmod rewrite expires headers - -COPY apache-suitecrm.conf /etc/apache2/sites-available/000-default.conf -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -WORKDIR /var/www/html - -VOLUME ["/var/www/html/upload", "/var/www/html/custom", "/var/www/html/config_override.php"] +# Cron +COPY crons.conf /root/crons.conf +RUN crontab /root/crons.conf EXPOSE 80 - -ENTRYPOINT ["docker-entrypoint.sh"] -CMD ["apache2-foreground"] +ENTRYPOINT ["/usr/local/bin/init.sh"] diff --git a/README.md b/README.md index 54acd2f..fde8c18 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,176 @@ -# SuiteCRM Docker — Containerized CRM Environment +# SugarCRM 6.5.26 CE — Containerized Docker Environment -**SuiteCRM 7.15.1** — vollständig containerisiert mit Docker Compose. -Ein Befehl, alles läuft: SuiteCRM + MariaDB + (optional) Redis. +**100% containerisierte SugarCRM Community Edition 6.5.26** +Ein `docker compose up -d` — alles läuft: Apache/PHP 5.6 + MySQL 5.7 + REST API v4.1. -## Systemanforderungen - -- Docker 20.10+ und Docker Compose v2 -- 2 GB RAM empfohlen (MariaDB Puffer) - -## Schnellstart +## 🚀 Quickstart ```bash -# 1. Repository klonen +# 1. Clone git clone https://git.kgessner.de/LuiiCode/sugar-crm.git cd sugar-crm -# 2. Umgebungsvariablen anpassen -cp .env.example .env -nano .env # Passwörter ändern! +# 2. Konfigurieren (Passwörter ändern!) +nano .env # 3. Starten docker compose up -d -# 4. SuiteCRM Installation im Browser abschließen: -# http://localhost:8080 +# 4. Web-UI öffnen +# → http://localhost:2080 +# Login: admin / admin123 + +# 5. API testen +python3 test_api.py ``` -## Architektur +## 🏗️ Architektur ``` -┌──────────────────────────────────────┐ -│ Docker Compose │ -│ │ -│ ┌──────────┐ ┌──────────┐ │ -│ │ SuiteCRM │ │ MariaDB │ Redis? │ -│ │ :8080 │ │ :3307 │ :6379 │ -│ │ PHP 8.1 │ │ 10.11 │ (opt.) │ -│ │ Apache │ │ │ │ -│ └──────────┘ └──────────┘ │ -│ │ │ │ -│ Volumes: Volumes: │ -│ - upload - /var/lib/mysql │ -│ - custom │ -│ - config │ -└──────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ Docker Compose │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ SugarCRM 6.5 │ │ MySQL 5.7 │ │ +│ │ Apache/PHP5.6 │ │ :3306 │ │ +│ │ :2080 │◄─│ │ │ +│ │ │ │ │ │ +│ └───────────────┘ └───────────────┘ │ +│ │ +│ Data (volumes): │ +│ sugarcrm_custom → custom modules │ +│ sugarcrm_upload → uploads │ +│ mysql_data → DB Daten │ +└─────────────────────────────────────────┘ ``` -## Services +## 📦 Services -| Service | Image | Port | Profil | -|-----------|------------------|-------|---------------| -| suitecrm | Custom (PHP 8.1) | 8080 | standard | -| mariadb | mariadb:10.11 | 3307 | standard | -| redis | redis:7-alpine | 6379 | `redis`/`full`| +| Service | Image | Port | Beschreibung | +|---------|-------|------|-------------| +| `sugarcrm` | Custom (PHP 5.6) | `2080` → `80` | SugarCRM 6.5.26 CE Web + API | +| `db` | `mysql:5.7` | `3306` | MySQL Datenbank | -## Konfiguration +## ⚙️ Konfiguration ### Umgebungsvariablen (`.env`) -| Variable | Default | Beschreibung | -|-----------------------|-----------------------------|-------------------------| -| `SUITECRM_PORT` | 8080 | Webinterface-Port | -| `SUITECRM_SITE_URL` | http://localhost:8080 | Öffentliche URL | -| `MYSQL_PORT` | 3307 | DB-Port (Host) | -| `MYSQL_ROOT_PASSWORD` | change_this… | Root-Passwort | -| `MYSQL_DATABASE` | suitecrm | Datenbank-Name | -| `MYSQL_USER` | suitecrm | Datenbank-Nutzer | -| `MYSQL_PASSWORD` | change_this… | Nutzer-Passwort | +| Variable | Default | Beschreibung | +|----------|---------|-------------| +| `SUGARCRM_PORT` | `2080` | Web-UI Port | +| `MYSQL_PORT` | `3306` | MySQL Port | +| `MYSQL_ROOT_PASSWORD` | `change_this…` | **Sofort ändern!** | +| `MYSQL_DATABASE` | `sugarcrm` | Datenbank-Name | +| `MYSQL_USER` | `sugarcrm` | DB-Nutzer | +| `MYSQL_PASSWORD` | `change_this…` | **Sofort ändern!** | +| `SUGARCRM_ADMIN_USER` | `admin` | Admin-Login | +| `SUGARCRM_ADMIN_PASSWORD` | `admin123` | Admin-Passwort | -> ⚠️ **Sicherheit**: Immer `.env` Passwörter ändern vor erstem Start! +> ⚠️ **Sicherheit**: `.env` vor erstem Start anpassen! Bei Änderung nach erstem Start: `docker compose down -v && docker compose up -d` -## Kommandos +## 🔌 REST API v4.1 + +**Endpoint:** `POST http://localhost:2080/service/v4_1/rest.php` +**Content-Type:** `application/x-www-form-urlencoded` + +### Aufbau + +``` +method=login +input_type=JSON +response_type=JSON +rest_data={"user_auth":{"user_name":"admin","password":"0192023a7bbd73250516f069df18b500"}} +``` + +### API Client (Python) + +```python +import hashlib, json, urllib.request + +pwd_hash = hashlib.md5(b"admin123").hexdigest() +rest_data = json.dumps({ + "user_auth": {"user_name": "admin", "password": pwd_hash}, + "application_name": "My App" +}) + +body = urllib.parse.urlencode({ + "method": "login", + "input_type": "JSON", + "response_type": "JSON", + "rest_data": rest_data +}).encode() + +req = urllib.request.Request( + "http://localhost:2080/service/v4_1/rest.php", + data=body, + headers={"Content-Type": "application/x-www-form-urlencoded"} +) +resp = json.loads(urllib.request.urlopen(req).read()) +session_id = resp["id"] +print(f"Session: {session_id}") +``` + +### Verfügbare Methoden + +| Methode | Beschreibung | +|---------|-------------| +| `login` | Session starten | +| `logout` | Session beenden | +| `get_available_modules` | Alle Module (Accounts, Contacts, Leads...) | +| `get_entry_list` | Einträge auslesen (mit Filter/Sortierung) | +| `get_entry` | Einzelnen Eintrag per ID | +| `set_entry` | Eintrag erstellen/aktualisieren | +| `set_entries` | Mehrere Einträge erstellen | +| `get_entries_count` | Anzahl Einträge zählen | +| `search_by_module` | Globale Suche | +| `get_module_fields` | Felddefinitionen eines Moduls | +| `set_relationship` | Beziehungen verknüpfen | +| `get_relationships` | Beziehungen auslesen | + +## 🛠️ Kommandos ```bash -# Grundbefehle -docker compose up -d # Alle Services starten -docker compose up -d redis # + Redis-Cache starten -docker compose down # Stoppen +# Start/Stop +docker compose up -d # Starten +docker compose down # Stoppen (Daten bleiben) docker compose down -v # Stoppen + ALLE DATEN LÖSCHEN # Logs -docker compose logs -f suitecrm # SuiteCRM-Logs verfolgen -docker compose logs mariadb # DB-Logs +docker compose logs -f sugarcrm # SugarCRM-Logs +docker compose logs db # MySQL-Logs + +# Shell +docker compose exec sugarcrm bash # Container-Shell # Backup -docker compose exec mariadb mysqldump -u suitecrm -p suitecrm > backup.sql -tar -czf upload-backup.tar.gz -C /var/lib/docker/volumes/sugarcrmreponame_suitecrm_data/_data . +docker compose exec db mysqldump -u sugarcrm -p sugarcrm > backup.sql ``` -## SuiteCRM Installation (Erst-Start) +## 🔧 CI/CD -Nach `docker compose up -d` im Browser `http://localhost:8080` öffnen: +Bei jedem Push auf `main` baut Gitea Actions automatisch: +1. Docker-Image aus `Dockerfile` +2. Push in die Gitea Container Registry: `git.kgessner.de/luiicode/sugar-crm` -1. **License Agreement** → Akzeptieren -2. **System Check** → Alle Checks sollten grün sein -3. **Database Configuration**: - - Host: `mariadb` - - Database: `suitecrm` - - User: `suitecrm` - - Password: (aus `.env`) -4. **Site Configuration** → Admin-Nutzer anlegen -5. **Fertig!** SuiteCRM ist einsatzbereit. +Secrets erforderlich: `REGISTRY_USER`, `REGISTRY_TOKEN` (bereits gesetzt). -## Redis aktivieren +## ⚠️ Technische Pitfalls -```bash -# Mit Redis-Profil starten -docker compose --profile redis up -d +| Problem | Fix | +|---------|-----| +| SourceForge ZIPs korrupt | GitHub Mirror `bklein01/sugarcrm` verwendet | +| Debian Jessie EOL | APT sources auf `archive.debian.org` umgebogen | +| Admin Wizard blockiert API | `installer_locked=true` + adminwizard DB-Eintrag (init.sh) | +| config.php wird überschrieben | init.sh macht idempotenten Restart | +| Kein mysqladmin im Container | PHP-Socket-Check für DB-Wait-Loop | -# Oder Full-Stack (alles inkl. Redis) -docker compose --profile full up -d -``` +## 📋 Systemanforderungen -Redis-Konfiguration in SuiteCRM Admin → System → Redis: -- Host: `redis` -- Port: `6379` - -## Elasticsearch (optional) - -Für Volltextsuche kann Elasticsearch ergänzt werden. Dazu in `docker-compose.yml` einfügen: - -```yaml - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.24 - container_name: suitecrm-es - environment: - - discovery.type=single-node - - xpack.security.enabled=false - volumes: - - es_data:/usr/share/elasticsearch/data - networks: - - suitecrm-net -``` - -## Docker Image bauen & pushen - -```bash -# Lokal bauen -docker build -t suitecrm:7.15.1 . - -# In Gitea Registry pushen -docker tag suitecrm:7.15.1 git.kgessner.de/luiicode/sugar-crm:7.15.1 -docker login git.kgessner.de -docker push git.kgessner.de/luiicode/sugar-crm:7.15.1 -``` - -## CI/CD - -Bei jedem Push auf `main` baut Gitea Actions das Image automatisch und pusht es in die Gitea Container Registry. Workflow: `.gitea/workflows/docker-build.yml` +- Docker 20.10+ +- Docker Compose v2 +- ~500 MB RAM (Apache + MySQL) +- Ports `2080` und `3306` verfügbar --- -**Version**: SuiteCRM 7.15.1 | **PHP**: 8.1 | **MariaDB**: 10.11 +**SugarCRM 6.5.26 CE** | PHP 5.6 | MySQL 5.7 | REST v4.1 | AGPLv3 diff --git a/apache-suitecrm.conf b/apache-suitecrm.conf deleted file mode 100644 index 6caa327..0000000 --- a/apache-suitecrm.conf +++ /dev/null @@ -1,23 +0,0 @@ - - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - - Options Indexes FollowSymLinks - AllowOverride All - Require all granted - - - # Protect sensitive files - - Require all denied - - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - # Security headers - Header always set X-Content-Type-Options "nosniff" - Header always set X-Frame-Options "SAMEORIGIN" - Header always set X-XSS-Protection "1; mode=block" - diff --git a/config_override.php.pyt b/config_override.php.pyt new file mode 100644 index 0000000..45d669b --- /dev/null +++ b/config_override.php.pyt @@ -0,0 +1,14 @@ + diff --git a/crons.conf b/crons.conf new file mode 100644 index 0000000..bd9299b --- /dev/null +++ b/crons.conf @@ -0,0 +1 @@ +* * * * * cd /var/www/html; php -f cron.php > /dev/null 2>&1 diff --git a/docker-compose.yml b/docker-compose.yml index dd168e1..0873338 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,87 +1,69 @@ -# SuiteCRM Docker Compose Environment -# ==================================== -# Start: docker compose up -d -# Stop: docker compose down -# Data persists in Docker volumes unless you run: docker compose down -v +# SugarCRM 6.5.26 CE — Docker Compose Environment +# Start: docker compose up -d +# Stop: docker compose down +# Clean: docker compose down -v (Achtung: löscht ALLE Daten!) + +version: '3.8' services: - # --- SuiteCRM Application --- - suitecrm: + sugarcrm: build: context: . dockerfile: Dockerfile - image: suitecrm:7.15.1 - container_name: suitecrm-app + image: sugarce:6.5.26 + container_name: sugarce-app restart: unless-stopped ports: - - "${SUITECRM_PORT:-8080}:80" - environment: - - DATABASE_HOST=mariadb - - DATABASE_PORT=3306 - - DATABASE_NAME=${MYSQL_DATABASE:-suitecrm} - - DATABASE_USER=${MYSQL_USER:-suitecrm} - - DATABASE_PASSWORD=${MYSQL_PASSWORD:-suitecrm_secret} - - SUITECRM_SITE_URL=${SUITECRM_SITE_URL:-http://localhost:8080} - volumes: - - suitecrm_data:/var/www/html/upload - - suitecrm_custom:/var/www/html/custom - - suitecrm_config:/var/www/html/config_override.php + - "${SUGARCRM_PORT:-2080}:80" depends_on: - mariadb: + db: condition: service_healthy + environment: + DB_TYPE: mysql + DB_MANAGER: MysqlManager + DB_HOST_NAME: db + DB_TCP_PORT: "3306" + DB_USER_NAME: ${MYSQL_USER:-sugarcrm} + DB_PASSWORD: ${MYSQL_PASSWORD:-sugarcrm_secret} + DATABASE_NAME: ${MYSQL_DATABASE:-sugarcrm} + volumes: + - sugarcrm_custom:/var/www/html/custom + - sugarcrm_upload:/var/www/html/upload + - sugarcrm_config_override:/var/www/html/config_override.php networks: - - suitecrm-net + - sugarce-net - # --- MariaDB Database --- - mariadb: - image: mariadb:10.11 - container_name: suitecrm-db + db: + image: mysql:5.7 + container_name: sugarce-db restart: unless-stopped ports: - - "${MYSQL_PORT:-3307}:3306" + - "${MYSQL_PORT:-3306}:3306" environment: - - MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root_secret} - - MARIADB_DATABASE=${MYSQL_DATABASE:-suitecrm} - - MARIADB_USER=${MYSQL_USER:-suitecrm} - - MARIADB_PASSWORD=${MYSQL_PASSWORD:-suitecrm_secret} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_secret} + MYSQL_DATABASE: ${MYSQL_DATABASE:-sugarcrm} + MYSQL_USER: ${MYSQL_USER:-sugarcrm} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-sugarcrm_secret} volumes: - - mariadb_data:/var/lib/mysql + - mysql_data:/var/lib/mysql command: - - --character-set-server=utf8mb4 - - --collation-server=utf8mb4_unicode_ci + - --character-set-server=utf8 + - --collation-server=utf8_general_ci - --max-allowed-packet=64M - - --innodb-buffer-pool-size=256M healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 networks: - - suitecrm-net - - # --- Redis Cache (optional) --- - redis: - image: redis:7-alpine - container_name: suitecrm-redis - restart: unless-stopped - ports: - - "${REDIS_PORT:-6379}:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru - networks: - - suitecrm-net - profiles: - - full - - redis + - sugarce-net volumes: - suitecrm_data: - suitecrm_custom: - suitecrm_config: - mariadb_data: - redis_data: + sugarcrm_custom: + sugarcrm_upload: + sugarcrm_config_override: + mysql_data: networks: - suitecrm-net: + sugarce-net: driver: bridge diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100644 index c70a5a6..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -e - -# Fix permissions -chown -R www-data:www-data /var/www/html/cache /var/www/html/upload /var/www/html/custom 2>/dev/null || true -chmod -R 755 /var/www/html 2>/dev/null || true -chmod -R 775 /var/www/html/cache /var/www/html/upload /var/www/html/custom 2>/dev/null || true - -# Generate SuiteCRM autoloader if missing -if [ ! -f /var/www/html/vendor/autoload.php ] && [ -f /var/www/html/composer.json ]; then - echo "Installing Composer dependencies..." - cd /var/www/html && composer install --no-dev --optimize-autoloader 2>/dev/null || true -fi - -# Set recommended permissions -touch /var/www/html/config.php 2>/dev/null || true -chmod 640 /var/www/html/config.php 2>/dev/null || true -chown www-data:www-data /var/www/html/config.php 2>/dev/null || true - -echo "SuiteCRM ready. Access http://localhost:8080 to complete installation." - -exec "$@" diff --git a/docker-php-ext-filesize.ini b/docker-php-ext-filesize.ini new file mode 100644 index 0000000..f8e3132 --- /dev/null +++ b/docker-php-ext-filesize.ini @@ -0,0 +1,2 @@ +upload_max_filesize = 32M +post_max_size = 32M diff --git a/envtemplate.py b/envtemplate.py new file mode 100644 index 0000000..8ea09ac --- /dev/null +++ b/envtemplate.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +import os, sys, getopt +from string import Template + +def main(argv): + inputfile = '' + outputfile = '' + try: + opts, args = getopt.getopt(argv,"hi:o:",["ifile=","ofile="]) + except getopt.GetoptError: + print 'envtemplate.py -i -o ' + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print 'envtemplate.py -i -o ' + sys.exit() + elif opt in ("-i", "--ifile"): + inputfile = arg + elif opt in ("-o", "--ofile"): + outputfile = arg + + # Populate case-insensitive env var dictionary + values = dict() + for k in os.environ: + v = os.environ.get(k) + values[k] = v + values[k.lower()] = v + + # Read template and substitute + with open(inputfile) as f: + templatestr = f.read() + + out = Template(templatestr).substitute(values) + + print "Writing output to {}".format(outputfile) + + with open(outputfile, "w") as f: + f.write(out) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/init.sh b/init.sh new file mode 100644 index 0000000..0af2acc --- /dev/null +++ b/init.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# SugarCRM 6.5 CE Docker Entrypoint +# ================================== +# Pitfalls documented (see README.md Technical Notes) + +# Resolve DB connection from env/linked container +if [ -z "$DB_HOST_NAME" ]; then + export DB_HOST_NAME=${DB_PORT_3306_TCP_ADDR:-db} +fi +if [ -z "$DB_TCP_PORT" ]; then + export DB_TCP_PORT=${DB_PORT_3306_TCP_PORT:-3306} +fi +if [ -z "$DB_USER_NAME" ]; then + export DB_USER_NAME=${DB_ENV_MYSQL_USER:-sugarcrm} +fi +if [ -z "$DB_PASSWORD" ]; then + export DB_PASSWORD=${DB_ENV_MYSQL_PASSWORD:-sugarcrm_secret} +fi +if [ -z "$DATABASE_NAME" ]; then + export DATABASE_NAME=${DB_ENV_MYSQL_DATABASE:-sugarcrm} +fi + +/usr/local/bin/envtemplate.py -i /usr/local/src/config_override.php.pyt -o /var/www/html/config_override.php + +# Start Apache in background for silent install via HTTP +echo "Starting Apache (background)..." +apachectl -DFOREGROUND & +APACHE_PID=$! + +# Wait for Apache to be ready +echo "Waiting for Apache..." +for i in $(seq 1 30); do + if curl -s http://localhost/ >/dev/null 2>&1; then + echo "Apache is ready" + break + fi + sleep 1 +done + +# Run silent install if not already installed +if [ ! -f /var/www/html/config.php ]; then + echo "Running SugarCRM Silent Install..." + + cat > /var/www/html/config_si.php << SIEOF + 'DB_HOST', + 'setup_db_database_name' => 'DB_NAME', + 'setup_db_admin_user_name' => 'DB_USER', + 'setup_db_admin_password' => 'DB_PASS', + 'setup_db_type' => 'mysql', + 'setup_db_port_num' => 'DB_PORT', + 'setup_db_drop_tables' => false, + 'setup_db_create_database' => false, + 'setup_db_create_sugarsales_user' => false, + 'setup_license_key' => 'free', + 'setup_license_accept' => true, + 'setup_site_url' => 'http://localhost:2080', + 'setup_system_name' => 'SugarCRM 6.5 CE', + 'setup_site_admin_user_name' => 'admin', + 'setup_site_admin_password' => 'admin123', + 'setup_site_admin_password_retype' => 'admin123', + 'demoData' => 'no', + 'dbUSRData' => 'create', +); +SIEOF + + sed -i "s/DB_HOST/$DB_HOST_NAME/g" /var/www/html/config_si.php + sed -i "s/DB_NAME/$DATABASE_NAME/g" /var/www/html/config_si.php + sed -i "s/DB_USER/$DB_USER_NAME/g" /var/www/html/config_si.php + sed -i "s/DB_PASS/$DB_PASSWORD/g" /var/www/html/config_si.php + sed -i "s/DB_PORT/$DB_TCP_PORT/g" /var/www/html/config_si.php + + # Wait for MySQL (using PHP socket check - no mysqladmin in container) + echo "Waiting for MySQL..." + for i in $(seq 1 30); do + if php -r "\$c=@mysql_connect('$DB_HOST_NAME:$DB_TCP_PORT','$DB_USER_NAME','$DB_PASSWORD');if(\$c){mysql_close(\$c);exit(0);}exit(1);" 2>/dev/null; then + echo "MySQL is ready" + break + fi + sleep 2 + done + + # Run silent install via HTTP + echo "Executing silent install..." + curl -s "http://localhost/install.php?goto=SilentInstall&cli=true" > /dev/null 2>&1 + + if [ -f /var/www/html/config.php ]; then + echo "Silent install complete" + + # Fix: installer_locked=true to prevent AdminWizard blocking API + sed -i "s/'installer_locked' => false/'installer_locked' => true/" /var/www/html/config.php 2>/dev/null || true + + # Skip Admin Wizard in config.php + sed -i "/'site_url' =>/a\\ + 'adminwizard' => array('completed' => true)," /var/www/html/config.php 2>/dev/null || true + + # Skip Admin Wizard in database (pitfall: needed for API access after restart) + php -r " + \$c = @mysql_connect('$DB_HOST_NAME:$DB_TCP_PORT', '$DB_USER_NAME', '$DB_PASSWORD'); + if (\$c) { + mysql_select_db('$DATABASE_NAME', \$c); + mysql_query(\"INSERT INTO config (category, name, value) VALUES ('system', 'adminwizard', '{\\\"completed\\\":true}')\", \$c); + mysql_close(\$c); + } + " 2>/dev/null || true + + echo "Admin Wizard disabled" + else + echo "WARNING: config.php was not created! Check install." + fi +else + echo "SugarCRM already installed" + + # Ensure installer_locked is true (idempotent restart fix) + if grep -q "'installer_locked' => false" /var/www/html/config.php 2>/dev/null; then + sed -i "s/'installer_locked' => false/'installer_locked' => true/" /var/www/html/config.php + echo "Fixed installer_locked" + fi + + # Ensure Admin Wizard remains disabled + if ! grep -q adminwizard /var/www/html/config.php 2>/dev/null; then + sed -i "/sugar_config\['site_url'\]/a\\ +\$sugar_config['adminwizard'] = array('completed' => true);" /var/www/html/config.php 2>/dev/null || true + echo "Admin Wizard disabled" + fi +fi + +# Clean up +rm -f /var/www/html/config_si.php + +# Start cron +/usr/sbin/cron + +# Bring Apache to foreground +echo "Setup complete. SugarCRM 6.5.26 CE ready." +wait $APACHE_PID diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..c53f934 --- /dev/null +++ b/test_api.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +SugarCRM 6.5 CE REST v4.1 API Test +=================================== +Endpunkt: http://localhost:{PORT}/service/v4_1/rest.php +""" +import http.client +import json +import hashlib +import urllib.parse +import sys +import os +from datetime import datetime + +# Configurable via env vars +BASE_HOST = os.environ.get("SUGARCRM_HOST", "localhost") +BASE_PORT = os.environ.get("SUGARCRM_PORT", "2080") +ENDPOINT = "/service/v4_1/rest.php" +USER = os.environ.get("SUGARCRM_USER", "admin") +PASSWORD = os.environ.get("SUGARCRM_PASSWORD", "admin123") + +BASE_URL = f"{BASE_HOST}:{BASE_PORT}" + +def call_api(method, rest_data): + """Aufruf der SugarCRM REST v4.1 API""" + conn = http.client.HTTPConnection(BASE_URL, timeout=30) + + body = urllib.parse.urlencode({ + "method": method, + "input_type": "JSON", + "response_type": "JSON", + "rest_data": json.dumps(rest_data) + }) + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + conn.request("POST", ENDPOINT, body, headers) + resp = conn.getresponse() + + if resp.status == 302: + location = resp.getheader("Location", "") + print(f" ⚠️ Redirect to: {location}") + print(" (Admin Wizard may be active - restart container to fix)") + return None + + data = json.loads(resp.read().decode()) + conn.close() + return data + + +def main(): + print("=" * 60) + print("🍬 SugarCRM 6.5.26 CE REST API Test") + print(f" URL: http://{BASE_URL}") + print("=" * 60) + + # 1. Login + pwd_hash = hashlib.md5(PASSWORD.encode()).hexdigest() + print(f"\n1️⃣ LOGIN (user={USER}, md5={pwd_hash[:8]}...)") + + result = call_api("login", { + "user_auth": {"user_name": USER, "password": pwd_hash}, + "application_name": "API Test Script" + }) + + if not result: + print("❌ LOGIN FAILED (Redirect - Admin Wizard active?)") + print(" Run: docker compose restart sugarcrm") + sys.exit(1) + + if result.get("id"): + session = result["id"] + print(f" ✅ Session: {session[:20]}...") + else: + print(f" ❌ Login Error: {result.get('name', 'Unknown')} - {result.get('description', '')}") + sys.exit(1) + + # 2. Available modules + print(f"\n2️⃣ AVAILABLE MODULES") + result = call_api("get_available_modules", {"session": session}) + if result and "modules" in result: + modules = result["modules"] + print(f" ✅ {len(modules)} modules available") + print(f" First 10: {', '.join(m['module_key'] for m in modules[:10])}") + else: + print(f" ❌ Error: {result}") + + # 3. Accounts list + print(f"\n3️⃣ ACCOUNTS (get_entry_list)") + result = call_api("get_entry_list", { + "session": session, + "module_name": "Accounts", + "query": "", + "order_by": "", + "offset": 0, + "select_fields": ["name", "id", "date_entered"], + "link_name_to_fields_array": [], + "max_results": 5, + "deleted": 0 + }) + if result and "entry_list" in result: + count = result.get("result_count", 0) + print(f" ✅ {count} accounts found") + for entry in result["entry_list"][:5]: + vals = {n["name"]: n["value"] for n in entry["name_value_list"]} + print(f" - {vals.get('name', 'N/A')} (ID: {vals.get('id', 'N/A')[:10]}...)") + else: + print(f" ❌ Error: {result}") + + # 4. Create test account + print(f"\n4️⃣ CREATE ACCOUNT (set_entry)") + test_name = f"Test API Account {datetime.now().strftime('%H:%M:%S')}" + result = call_api("set_entry", { + "session": session, + "module_name": "Accounts", + "name_value_list": { + "name": {"name": "name", "value": test_name}, + "account_type": {"name": "account_type", "value": "Customer"}, + } + }) + if result and result.get("id"): + print(f" ✅ Account '{test_name}' created (ID: {result['id'][:10]}...)") + + # 5. Session count + print(f"\n5️⃣ MODULE COUNT (get_entries_count)") + result = call_api("get_entries_count", { + "session": session, + "module_name": "Accounts", + "query": "", + "deleted": 0 + }) + if result: + print(f" ✅ Total accounts: {result.get('result_count', 'unknown')}") + + print(f"\n{'=' * 60}") + print("✅ ALL API TESTS PASSED!") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + main()