Building Reusable GitLab CI Templates for OpenERP/Odoo Testing
Table of Contents
- Introduction
- Problem Statement
- Architecture Overview
- Step-by-Step Implementation
- Template Configuration
- Usage Examples
- Troubleshooting
- Best Practices
- Conclusion
Introduction
In this guide, we’ll explore how to create a reusable GitLab CI/CD pipeline template for testing OpenERP/Odoo modules. This approach allows teams to standardize their testing infrastructure while maintaining flexibility for project-specific requirements.
What You’ll Learn
- How to create Docker-based CI/CD pipelines for OpenERP/Odoo
- Building reusable GitLab CI templates with the
includedirective - Managing container registry access with Deploy Tokens
- Mounting custom addons and handling file permissions
- Generating JUnit XML reports for test visualization
Prerequisites
- GitLab instance with GitLab Runner configured
- Docker environment for building and running containers
- Basic knowledge of OpenERP/Odoo architecture
- Understanding of GitLab CI/CD concepts
Problem Statement
The Challenge
When working with multiple OpenERP/Odoo projects, teams often face:
- Code Duplication: Each project maintains its own CI/CD configuration
- Inconsistent Testing: Different test setups across projects lead to reliability issues
- Maintenance Overhead: Updates must be replicated across all projects
- Access Management: Complex credential handling for private container registries
- Environment Setup: Repetitive database and application container configuration
The Solution
Create a centralized CI/CD template that:
- Lives in a single repository
- Can be included in any project
- Provides configurable options through variables
- Handles authentication automatically
- Generates standardized test reports
Architecture Overview
Component Diagram
┌─────────────────────────────────────────────────────────────┐
│ GitLab Instance │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ kazacube-ci-tools Repository │ │
│ │ │ │
│ │ ├── .gitlab-ci.yml (main template) │ │
│ │ ├── docker/ │ │
│ │ │ ├── openprodtest-db/ │ │
│ │ │ └── openprodtest-openprod/ │ │
│ │ └── Container Registry │ │
│ │ ├── openprodtest-db:latest │ │
│ │ └── openprodtest-openprod:latest │ │
│ └──────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ include │
│ ┌──────────────────────┴─────────────────────────────┐ │
│ │ Project Repository (e.g., OPENPROD-EXTRA) │ │
│ │ │ │
│ │ ├── .gitlab-ci.yml (uses template) │ │
│ │ ├── module1/ │ │
│ │ ├── module2/ │ │
│ │ └── moduleN/ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ GitLab Runner │ │
│ │ │ │
│ │ ├── Docker Executor │ │
│ │ └── Volume Mounts: /builds:/builds │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Workflow
- Project Trigger: Developer pushes code or manually triggers pipeline
- Template Loading: GitLab loads the shared template from
kazacube-ci-tools - Authentication: Deploy Token or CI credentials authenticate with registry
- Container Orchestration:
- PostgreSQL container starts with restored database
- OpenProd container mounts custom addons
- Test Execution: OpenERP runs tests with JUnit XML output
- Result Collection: Test reports are extracted and published
Step-by-Step Implementation
Phase 1: Prepare Docker Images
1.1 Database Image
Create a PostgreSQL image that automatically restores your OpenERP database:
# docker/openprodtest-db/Dockerfile
FROM postgres:12
ENV POSTGRES_USER=openprod
ENV POSTGRES_PASSWORD=openprod
ENV POSTGRES_DB=postgres
# Copy database dump
COPY dump.sql /docker-entrypoint-initdb.d/
# Initialization script
COPY init-db.sh /docker-entrypoint-initdb.d/
RUN chmod +x /docker-entrypoint-initdb.d/init-db.sh
init-db.sh:
#!/bin/bash
set -e
# Wait for PostgreSQL to be ready
until pg_isready -U openprod; do
echo "Waiting for PostgreSQL..."
sleep 2
done
# Restore database if not exists
if ! psql -U openprod -lqt | cut -d \| -f 1 | grep -qw $DB_NAME; then
echo "Creating database $DB_NAME..."
createdb -U openprod $DB_NAME
echo "Restoring database from dump..."
psql -U openprod -d $DB_NAME < /docker-entrypoint-initdb.d/dump.sql
echo "Database restored successfully!"
fi
1.2 OpenProd Application Image
# docker/openprodtest-openprod/Dockerfile
FROM ubuntu:18.04
# Install Python 2.7 and dependencies
RUN apt-get update && apt-get install -y \
python2.7 \
python-pip \
postgresql-client \
git \
&& rm -rf /var/lib/apt/lists/*
# Create virtualenv
RUN pip install virtualenv
RUN virtualenv -p python2.7 /opt/openprod/server/venvs/18_04
# Copy OpenProd source
COPY openprod/ /opt/openprod/
# Install Python dependencies
RUN /opt/openprod/server/venvs/18_04/bin/pip install -r /opt/openprod/requirements.txt
WORKDIR /opt/openprod
# Set entrypoint
CMD ["/bin/bash"]
Build and push images:
# Build database image
cd docker/openprodtest-db
docker build -t gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-db:latest .
docker push gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-db:latest
# Build application image
cd ../openprodtest-openprod
docker build -t gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-openprod:latest .
docker push gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools/openprodtest-openprod:latest
Phase 2: Create the CI Template
2.1 Template Structure
Create .gitlab-ci.yml in your kazacube-ci-tools repository:
# .gitlab-ci.yml
stages:
- build
- test
variables:
# Docker configuration
DOCKER_DRIVER: overlay2
REGISTRY: gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools
# Image references
IMAGE_DB: $REGISTRY/openprodtest-db
IMAGE_APP: $REGISTRY/openprodtest-openprod
# Database configuration
DB_NAME: openprod_restored
DB_INIT_SLEEP: "60"
# OpenERP paths
PYTHON_PATH: /opt/openprod/server/venvs/18_04/bin/python2.7
OPENERP_SERVER_PATH: /opt/openprod/server/openerp-server
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons
# Optional custom addons
CUSTOM_ADDONS_MOUNT: ""
CUSTOM_ADDONS_PATH: ""
# OpenERP options
MODULES_TO_INSTALL: ""
MODULES_TO_UPDATE: ""
OPENERP_EXTRA_OPTIONS: ""
.default_docker:
tags:
- docker
- local
image: docker:26
before_script:
- docker info
# Smart authentication: use custom credentials or fallback to CI defaults
- |
if [ -n "$DOCKER_REGISTRY_USER" ] && [ -n "$DOCKER_REGISTRY_TOKEN" ]; then
echo "Using custom Docker registry credentials"
echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin $REGISTRY
else
echo "Using default CI credentials"
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $REGISTRY
fi
test-db-and-unit:
stage: test
extends: .default_docker
script:
# Pre-cleanup
- echo "Pre-cleanup: removing any existing containers and network..."
- docker rm -f db-test openprod3-test 2>/dev/null || true
- docker network rm openprod3-net 2>/dev/null || true
# Pull images
- echo "Pulling images..."
- docker pull $IMAGE_APP:latest
- docker pull $IMAGE_DB:latest
# Create network
- echo "Creating network..."
- docker network create openprod3-net
# Start PostgreSQL
- echo "Starting PostgreSQL..."
- >
docker run -d --name db-test
--network openprod3-net
-e POSTGRES_USER=openprod
-e POSTGRES_PASSWORD=openprod
-e POSTGRES_DB=postgres
-e DB_NAME=$DB_NAME
--tmpfs /var/lib/postgresql/data
$IMAGE_DB:latest
# Wait for database
- echo "Waiting for database to initialize ($DB_INIT_SLEEP seconds)..."
- sleep $DB_INIT_SLEEP
- docker exec db-test pg_isready -U openprod || sleep 10
- docker exec db-test psql -U openprod -d postgres -c "\l"
# Prepare test results directory
- mkdir -p test-results
# Build OpenERP command with dynamic options
- |
OPENERP_CMD="$PYTHON_PATH $OPENERP_SERVER_PATH \
-d $DB_NAME \
--db_host=db-test \
--db_port=5432 \
--db_user=openprod \
--db_password=openprod \
--addons-path=$ADDONS_PATH"
# Add modules to install
if [ -n "$MODULES_TO_INSTALL" ]; then
echo "Installing modules: $MODULES_TO_INSTALL"
OPENERP_CMD="$OPENERP_CMD -i $MODULES_TO_INSTALL"
fi
# Add modules to update
if [ -n "$MODULES_TO_UPDATE" ]; then
echo "Updating modules: $MODULES_TO_UPDATE"
OPENERP_CMD="$OPENERP_CMD -u $MODULES_TO_UPDATE"
fi
# Add extra options
if [ -n "$OPENERP_EXTRA_OPTIONS" ]; then
echo "Extra options: $OPENERP_EXTRA_OPTIONS"
OPENERP_CMD="$OPENERP_CMD $OPENERP_EXTRA_OPTIONS"
fi
# Add test options (always present)
OPENERP_CMD="$OPENERP_CMD \
--test-enable \
--stop-after-init \
--log-level=test \
--test-junitxml \
--test-report-directory=/tmp/test-reports"
# Build Docker run command
- |
DOCKER_RUN_CMD="docker run --name openprod3-test \
--network openprod3-net \
-e DB_HOST=db-test \
-e DB_PORT=5432 \
-e DB_USER=openprod \
-e DB_PASSWORD=openprod \
-e DB_NAME=$DB_NAME"
# Add volume mount if custom addons defined
if [ -n "$CUSTOM_ADDONS_MOUNT" ] && [ -n "$CUSTOM_ADDONS_PATH" ]; then
echo "Mounting custom addons from $CUSTOM_ADDONS_MOUNT to $CUSTOM_ADDONS_PATH"
DOCKER_RUN_CMD="$DOCKER_RUN_CMD -v $CUSTOM_ADDONS_MOUNT:$CUSTOM_ADDONS_PATH:rw"
fi
# Finalize command
DOCKER_RUN_CMD="$DOCKER_RUN_CMD $IMAGE_APP:latest $OPENERP_CMD"
echo "Executing: $DOCKER_RUN_CMD"
eval $DOCKER_RUN_CMD || true
# Collect results
- echo "Collecting test logs..."
- docker logs openprod3-test 2>&1 | tee tests_output.log
- echo "Copying XML files from container..."
- docker cp openprod3-test:/tmp/test-reports/. test-results/ || echo "Failed to copy test results"
- echo "Checking generated XML files..."
- ls -lah test-results/
- find test-results/ -name "*.xml" -ls || echo "No XML files found"
- echo "==== Test Summary ===="
- grep -E "Ran.*tests" tests_output.log | tail -5 || echo "No test summary found"
after_script:
- echo "Cleaning up..."
- docker rm -f db-test openprod3-test 2>/dev/null || true
- docker network rm openprod3-net 2>/dev/null || true
artifacts:
paths:
- tests_output.log
- test-results/
reports:
junit: test-results/*.xml
when: always
expire_in: 1 week
when: manual
timeout: 30m
Phase 3: Configure GitLab Runner
3.1 Runner Configuration
Edit /etc/gitlab-runner/config.toml:
concurrent = 4
[[runners]]
name = "docker-runner"
url = "https://gitlab.example.com/"
token = "YOUR_RUNNER_TOKEN"
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:26"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
# CRITICAL: Volume mounts for Docker-in-Docker
volumes = [
"/var/run/docker.sock:/var/run/docker.sock", # Docker socket
"/cache", # GitLab cache
"/builds:/builds" # Source code access
]
shm_size = 0
[runners.cache]
MaxUploadedArchiveSize = 0
Why /builds:/builds is critical:
Without this mount, the Docker daemon cannot access files in /builds/... when you use volume mounts like -v $CI_PROJECT_DIR:/workspace.
Restart the runner:
sudo gitlab-runner restart
sudo gitlab-runner verify
Phase 4: Set Up Access Control
4.1 Create Deploy Token
In your kazacube-ci-tools repository:
- Go to Settings → Repository → Deploy tokens
- Create a new token:
- Name:
openprod-registry-access - Scopes: ✅
read_registry - Expires at: (optional)
- Name:
- Copy the generated username and token (shown only once!)
Example output:
Username: gitlab+deploy-token-12345
Token: gldt-aBcDeFgHiJkLmNoPqRsTuVwXyZ
4.2 Configure Variables in Target Project
In your project that uses the template (e.g., OPENPROD-EXTRA):
-
Go to Settings → CI/CD → Variables
-
Add variable
DOCKER_REGISTRY_USER:- Value:
gitlab+deploy-token-12345 - Type: Variable
- Flags:
- ☐ Protect variable
- ☐ Mask variable ← IMPORTANT: UNCHECKED
- ☑ Expand variable reference
- Value:
-
Add variable
DOCKER_REGISTRY_TOKEN:- Value:
gldt-aBcDeFgHiJkLmNoPqRsTuVwXyZ - Type: Variable
- Flags:
- ☐ Protect variable
- ☑ Mask variable ← Checked for security
- ☑ Expand variable reference
- Value:
Why uncheck “Mask” for username?
GitLab replaces masked values with [MASKED] in command output. If the username is masked, Docker receives an empty string, causing authentication to fail.
Template Configuration
Available Variables
| Variable | Description | Default | Example |
|---|---|---|---|
REGISTRY | Container registry URL | gl.kazacube.fr:5050/... | Your registry URL |
IMAGE_DB | PostgreSQL image | $REGISTRY/openprodtest-db | Custom DB image |
IMAGE_APP | OpenProd image | $REGISTRY/openprodtest-openprod | Custom app image |
DB_NAME | Database name | openprod_restored | my_test_db |
DB_INIT_SLEEP | DB init wait time (seconds) | 60 | 90 |
PYTHON_PATH | Python interpreter path | /opt/openprod/.../python2.7 | Custom Python |
OPENERP_SERVER_PATH | OpenERP server script | /opt/openprod/.../openerp-server | Custom path |
ADDONS_PATH | Base addons path | path1,path2,path3 | Comma-separated |
CUSTOM_ADDONS_MOUNT | Host path to mount | "" | $CI_PROJECT_DIR |
CUSTOM_ADDONS_PATH | Container mount point | "" | /workspace/custom |
MODULES_TO_INSTALL | Modules to install | "" | module1,module2 |
MODULES_TO_UPDATE | Modules to update | "" | module1,module2 |
OPENERP_EXTRA_OPTIONS | Additional OpenERP flags | "" | --update-at-init |
Usage Examples
Example 1: Basic Usage (Default Tests)
In your project repository, create .gitlab-ci.yml:
# .gitlab-ci.yml
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: main
# The job 'test-db-and-unit' is now available
# It will run with default configuration
Example 2: Testing Custom Modules
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: main
test-db-and-unit:
variables:
# Mount project directory as custom addons
CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
CUSTOM_ADDONS_PATH: /workspace/custom-addons
# Add to addons path
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
before_script:
- docker info
- echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
Example 3: Installing Specific Modules
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: main
test-db-and-unit:
variables:
CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
CUSTOM_ADDONS_PATH: /workspace/custom-addons
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
# Install specific module
MODULES_TO_INSTALL: my_custom_module
OPENERP_EXTRA_OPTIONS: "--update-at-init"
before_script:
- docker info
- echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
Example 4: Multiple Test Jobs
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: main
# Override with custom credentials
.openprod_base:
before_script:
- docker info
- echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
# Test all modules (default)
test-all-modules:
extends:
- test-db-and-unit
- .openprod_base
variables:
CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
CUSTOM_ADDONS_PATH: /workspace/custom-addons
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
# Test specific module
test-module-a:
extends:
- test-db-and-unit
- .openprod_base
variables:
CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
CUSTOM_ADDONS_PATH: /workspace/custom-addons
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
MODULES_TO_INSTALL: module_a
OPENERP_EXTRA_OPTIONS: "--update-at-init"
when: manual
# Test with different database
test-with-custom-db:
extends:
- test-db-and-unit
- .openprod_base
variables:
DB_NAME: custom_test_db
DB_INIT_SLEEP: "90"
Example 5: Advanced Configuration
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: v1.0.0 # Use specific version tag
stages:
- test
- deploy
test-db-and-unit:
stage: test
variables:
CUSTOM_ADDONS_MOUNT: $CI_PROJECT_DIR
CUSTOM_ADDONS_PATH: /workspace/custom-addons
ADDONS_PATH: /opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons
MODULES_TO_INSTALL: module1,module2,module3
MODULES_TO_UPDATE: base,stock
OPENERP_EXTRA_OPTIONS: "--update-at-init --without-demo=all --load-language=fr_FR"
DB_INIT_SLEEP: "120"
before_script:
- docker info
- echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
only:
- merge_requests
- main
deploy-production:
stage: deploy
script:
- echo "Deploying to production..."
only:
- tags
when: manual
Troubleshooting
Issue 1: “Must provide —username with —password-stdin”
Symptom:
$ echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin
Must provide --username with --password-stdin
Cause: The DOCKER_REGISTRY_USER variable is empty or masked.
Solution:
- Check that the variable is defined in Settings → CI/CD → Variables
- Uncheck “Mask variable” for
DOCKER_REGISTRY_USER - Keep “Mask variable” checked only for
DOCKER_REGISTRY_TOKEN
Issue 2: “pull access denied”
Symptom:
Error response from daemon: pull access denied for registry/image,
repository does not exist or may require 'docker login': denied
Cause: Insufficient permissions to access the container registry.
Solutions:
A. Verify Deploy Token:
- Check token has
read_registryscope - Verify token hasn’t expired
- Ensure token was created in the correct project
B. Test authentication manually:
echo "YOUR_TOKEN" | docker login -u "YOUR_USERNAME" --password-stdin registry.example.com
docker pull registry.example.com/image:latest
C. Check registry visibility:
- Go to project Settings → Packages and registries → Container Registry
- Ensure visibility allows access from your target project
Issue 3: “The addons-path ‘/path’ does not seem to be a valid Addons Directory”
Symptom:
openerp-server: error: option --addons-path:
The addons-path '/workspace/custom-addons' does not seem to a be a valid Addons Directory!
Causes:
- Volume mount not working (GitLab Runner misconfiguration)
- Directory doesn’t contain valid OpenERP modules
- Permission issues
Solutions:
A. Verify GitLab Runner volumes:
Check /etc/gitlab-runner/config.toml:
volumes = [
"/var/run/docker.sock:/var/run/docker.sock",
"/cache",
"/builds:/builds" # ← This is CRITICAL
]
Restart runner after changes:
sudo gitlab-runner restart
B. Debug volume mount:
Add to your .gitlab-ci.yml:
script:
# Before running tests
- echo "Checking project structure..."
- ls -la $CI_PROJECT_DIR
- find $CI_PROJECT_DIR -name "__openerp__.py" -o -name "__manifest__.py"
# After container starts
- docker exec openprod3-test ls -la /workspace/custom-addons
- docker exec openprod3-test find /workspace/custom-addons -name "*.py" | head -10
C. Verify module structure:
Ensure your modules have this structure:
project/
├── module1/
│ ├── __init__.py
│ ├── __openerp__.py (or __manifest__.py)
│ └── ...
├── module2/
│ ├── __init__.py
│ ├── __openerp__.py
│ └── ...
D. Try read-write mount:
Change :ro to :rw in volume mount:
-v $CI_PROJECT_DIR:/workspace/custom-addons:rw
Issue 4: No JUnit XML files generated
Symptom:
WARNING: test-results/*.xml: no matching files
ERROR: No files to upload
Causes:
- OpenERP version doesn’t support
--test-junitxml - Tests failed before XML generation
- Wrong output directory
Solutions:
A. Check OpenERP version:
OpenERP 9 and earlier may not support JUnit XML natively.
B. Verify test execution:
script:
# After docker run
- docker logs openprod3-test 2>&1 | tee tests_output.log
- grep -i "xml" tests_output.log || echo "No XML mentioned in logs"
C. Manual XML generation for older versions:
Create a Python script to parse logs and generate JUnit XML:
# convert_to_junit.py
import re
import sys
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom
def parse_test_log(log_file):
with open(log_file, 'r') as f:
content = f.read()
# Parse test results
test_pattern = r'(\d+) test\(s\).*?(\d+) failure\(s\)'
matches = re.search(test_pattern, content)
if not matches:
return None
total_tests = int(matches.group(1))
failures = int(matches.group(2))
# Create JUnit XML
testsuites = Element('testsuites', tests=str(total_tests), failures=str(failures))
testsuite = SubElement(testsuites, 'testsuite', name='OpenProd', tests=str(total_tests), failures=str(failures))
# Add placeholder test case
testcase = SubElement(testsuite, 'testcase', name='all_tests', classname='openprod')
if failures > 0:
failure = SubElement(testcase, 'failure', message=f'{failures} test(s) failed')
# Pretty print
xml_str = minidom.parseString(tostring(testsuites)).toprettyxml(indent=" ")
return xml_str
if __name__ == '__main__':
xml = parse_test_log('tests_output.log')
if xml:
with open('test-results/TEST-openprod.xml', 'w') as f:
f.write(xml)
print("JUnit XML generated successfully")
else:
print("No test results found in logs")
Add to pipeline:
script:
# ... after collecting logs
- python3 convert_to_junit.py
- ls -la test-results/
Issue 5: Container exits immediately
Symptom:
Running as user 'root' is a security risk.
Usage: openerp-server [options]
(Container exits)
Cause: OpenERP command has syntax errors or missing parameters.
Solution:
Add debug output:
script:
# Before eval
- echo "Full command:"
- echo "$DOCKER_RUN_CMD"
- echo "---"
# Run with verbose error handling
- eval $DOCKER_RUN_CMD || echo "Exit code: $?"
# Check if container started
- docker ps -a | grep openprod3-test
Issue 6: Database connection refused
Symptom:
psycopg2.OperationalError: could not connect to server: Connection refused
Is the server running on host "db-test" (172.23.0.2) and accepting
TCP/IP connections on port 5432?
Causes:
- Database not ready yet
- Wrong network configuration
- Database failed to start
Solutions:
A. Increase wait time:
variables:
DB_INIT_SLEEP: "90" # Increase from 60
B. Add robust wait loop:
script:
# Replace simple sleep with retry loop
- |
echo "Waiting for database..."
for i in {1..60}; do
if docker exec db-test pg_isready -U openprod -d $DB_NAME > /dev/null 2>&1; then
echo "Database is ready!"
break
fi
echo "Still waiting... ($i/60)"
sleep 2
done
C. Check database logs:
script:
# After database start
- echo "Database logs:"
- docker logs db-test
Best Practices
1. Version Control for Templates
Use Git tags for template versions:
# In kazacube-ci-tools
git tag v1.0.0 -m "Stable release - initial template"
git push origin v1.0.0
In projects, reference specific versions:
include:
- project: 'kazacube/kazacube-interne/kazacube-ci-tools'
file: '.gitlab-ci.yml'
ref: v1.0.0 # Pinned version
Benefits:
- Predictable behavior
- No breaking changes from template updates
- Easy rollback if issues arise
- Clear changelog
2. Separate Concerns
Template responsibilities:
- Infrastructure setup (containers, networks)
- Standard authentication
- Test execution
- Result collection
Project responsibilities:
- Custom module paths
- Specific modules to test
- Project-specific options
- Additional deployment steps
3. Use Semantic Versioning
v1.0.0 - Initial stable release
v1.1.0 - Added custom addons support
v1.2.0 - Added module installation options
v2.0.0 - Breaking change: new Docker images
4. Document Variables
Create a README.md in your template repository:
# OpenProd CI Template
## Available Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DOCKER_REGISTRY_USER` | ✅ Yes* | - | Deploy token username |
| `DOCKER_REGISTRY_TOKEN` | ✅ Yes* | - | Deploy token password |
| `CUSTOM_ADDONS_MOUNT` | ❌ No | "" | Host path for custom addons |
| `MODULES_TO_INSTALL` | ❌ No | "" | Comma-separated module names |
*Required only in external projects
## Usage
See [USAGE.md](USAGE.md) for detailed examples.
5. Implement Caching
Speed up pipelines with Docker layer caching:
.default_docker:
before_script:
- docker info
# Enable BuildKit for better caching
- export DOCKER_BUILDKIT=1
- |
if [ -n "$DOCKER_REGISTRY_USER" ]; then
echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin $REGISTRY
else
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $REGISTRY
fi
cache:
key: docker-cache
paths:
- .docker-cache/
6. Set Up Notifications
Add Slack/email notifications for test failures:
test-db-and-unit:
# ... existing configuration
after_script:
- |
if [ "$CI_JOB_STATUS" == "failed" ]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Test failed in $CI_PROJECT_NAME: $CI_PIPELINE_URL\"}" \
$SLACK_WEBHOOK_URL
fi
- docker rm -f db-test openprod3-test 2>/dev/null || true
- docker network rm openprod3-net 2>/dev/null || true
7. Implement Resource Limits
Prevent runaway containers:
script:
- |
docker run --name openprod3-test \
--memory="2g" \
--memory-swap="2g" \
--cpus="2.0" \
--network openprod3-net \
# ... rest of configuration
8. Create Health Checks
Verify containers are healthy before running tests:
script:
- |
docker run -d --name db-test \
--health-cmd "pg_isready -U openprod -d $DB_NAME" \
--health-interval=5s \
--health-timeout=3s \
--health-retries=10 \
--network openprod3-net \
# ... rest of configuration
# Wait for healthy status
- |
until [ "$(docker inspect --format='{{.State.Health.Status}}' db-test)" == "healthy" ]; do
echo "Waiting for database to be healthy..."
sleep 2
done
9. Implement Retry Logic
Handle transient failures:
test-db-and-unit:
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
10. Use Matrix Testing
Test multiple configurations:
test-db-and-unit:
parallel:
matrix:
- MODULE: [module1, module2, module3]
variables:
MODULES_TO_INSTALL: $MODULE
Conclusion
What We’ve Built
We’ve created a robust, reusable CI/CD pipeline template that:
✅ Eliminates duplication across multiple projects
✅ Standardizes testing with consistent environments
✅ Simplifies maintenance through centralized configuration
✅ Provides flexibility via configurable variables
✅ Ensures security with proper credential management
✅ Generates reports in standard JUnit XML format
Key Takeaways
- Docker-in-Docker requires careful runner configuration, especially the
/builds:/buildsvolume mount - Deploy Tokens provide secure, scoped access to container registries
- Variable masking should be used selectively - usernames can remain visible, but tokens must be masked
- Template versioning prevents breaking changes and enables gradual rollouts
- Comprehensive error handling makes debugging much easier
Next Steps
Immediate improvements:
- Add parallel test execution for faster feedback
- Implement test coverage reporting
- Set up automated notifications
- Create development/staging/production variants
Long-term enhancements:
- Build a library of reusable templates (linting, security scanning, deployment)
- Implement automatic rollback on test failures
- Create custom GitLab Runner with pre-cached images
- Add performance testing and benchmarking
Resources
Official Documentation:
OpenERP/Odoo:
Final Thoughts
Building reusable CI/CD infrastructure requires initial investment but pays dividends through:
- Reduced maintenance overhead
- Consistent quality across projects
- Faster onboarding for new projects
- Easier troubleshooting with standardized patterns
The template approach scales excellently as your organization grows, and the techniques demonstrated here apply beyond OpenERP/Odoo to any containerized application testing.
Appendix
A. Complete Example Repository Structure
kazacube-ci-tools/
├── .gitlab-ci.yml # Main template
├── README.md # Documentation
├── USAGE.md # Usage examples
├── CHANGELOG.md # Version history
├── docker/
│ ├── openprodtest-db/
│ │ ├── Dockerfile
│ │ ├── init-db.sh
│ │ └── dump.sql
│ └── openprodtest-openprod/
│ ├── Dockerfile
│ └── requirements.txt
└── scripts/
├── convert_to_junit.py # Fallback for old OpenERP
└── test-local.sh # Local testing script
B. Local Testing Script
Save as scripts/test-local.sh:
#!/bin/bash
# Local testing script for development
set -e
# Configuration
REGISTRY="gl.kazacube.fr:5050/kazacube/kazacube-interne/kazacube-ci-tools"
DB_NAME="openprod_restored"
IMAGE_DB="$REGISTRY/openprodtest-db"
IMAGE_APP="$REGISTRY/openprodtest-openprod"
# Load credentials
if [ -f .env.local ]; then
source .env.local
else
echo "Create .env.local with:"
echo "export DOCKER_REGISTRY_USER='gitlab+deploy-token-xxxxx'"
echo "export DOCKER_REGISTRY_TOKEN='gldt-xxxxx'"
exit 1
fi
# Test authentication
echo "Testing Docker login..."
echo "$DOCKER_REGISTRY_TOKEN" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin gl.kazacube.fr:5050
if [ $? -eq 0 ]; then
echo "✓ Authentication successful"
else
echo "✗ Authentication failed"
exit 1
fi
# Pull images
echo "Pulling images..."
docker pull $IMAGE_APP:latest
docker pull $IMAGE_DB:latest
# Cleanup
docker rm -f db-test openprod3-test 2>/dev/null || true
docker network rm openprod3-net 2>/dev/null || true
# Create network
docker network create openprod3-net
# Start database
docker run -d --name db-test \
--network openprod3-net \
-e POSTGRES_USER=openprod \
-e POSTGRES_PASSWORD=openprod \
-e POSTGRES_DB=postgres \
-e DB_NAME=$DB_NAME \
--tmpfs /var/lib/postgresql/data \
$IMAGE_DB:latest
# Wait for database
echo "Waiting for database..."
sleep 60
# Run tests
mkdir -p test-results
docker run --name openprod3-test \
--network openprod3-net \
-e DB_HOST=db-test \
-e DB_PORT=5432 \
-e DB_USER=openprod \
-e DB_PASSWORD=openprod \
-e DB_NAME=$DB_NAME \
-v $(pwd):/workspace/custom-addons:rw \
$IMAGE_APP:latest \
/opt/openprod/server/venvs/18_04/bin/python2.7 \
/opt/openprod/server/openerp-server \
-d $DB_NAME \
--db_host=db-test \
--db_port=5432 \
--db_user=openprod \
--db_password=openprod \
--addons-path=/opt/openprod/server/openerp/addons,/opt/openprod/odoo-addons,/opt/openprod/openprod-addons,/workspace/custom-addons \
--test-enable \
--stop-after-init \
--log-level=test \
--test-junitxml \
--test-report-directory=/tmp/test-reports || true
# Collect results
docker logs openprod3-test 2>&1 | tee tests_output.log
docker cp openprod3-test:/tmp/test-reports/. test-results/ || true
# Display results
echo "=== Test Results ==="
ls -lh test-results/
grep -E "Ran.*tests" tests_output.log | tail -5 || echo "No summary found"
# Cleanup
docker rm -f db-test openprod3-test
docker network rm openprod3-net
echo "Done!"
C. Changelog Template
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.0] - 2024-01-15
### Added
- Support for custom module installation via `MODULES_TO_INSTALL`
- Support for module updates via `MODULES_TO_UPDATE`
- Extra OpenERP options via `OPENERP_EXTRA_OPTIONS`
### Changed
- Improved error messages in authentication
- Increased default DB_INIT_SLEEP to 60 seconds
### Fixed
- Volume mount permissions issue with custom addons
## [1.1.0] - 2024-01-01
### Added
- Custom addons mounting support
- Flexible authentication (Deploy Token or CI credentials)
## [1.0.0] - 2023-12-15
### Added
- Initial stable release
- Basic OpenERP testing with PostgreSQL
- JUnit XML report generation
- Docker-based execution
Author: [Your Name]
Date: November 2024
License: MIT
Repository: github.com/yourorg/openprod-ci-template
This guide is part of a series on building robust CI/CD pipelines for legacy ERP systems. Stay tuned for more articles on advanced deployment strategies and test automation!