Add complete CI pipeline with testing stages
Some checks failed
CI Pipeline / test (push) Has been cancelled
CI Pipeline / security-scan (push) Has been cancelled
CI Pipeline / push (push) Has been cancelled
CI Pipeline / update-cd (push) Has been cancelled
CI Pipeline / lint (push) Has been cancelled
CI Pipeline / build (push) Has been cancelled

Pipeline stages:
1. lint - Dockerfile linting with hadolint
2. build - Build Docker image and save as artifact
3. test - Integration tests (schemas, overlays, LDAP operations)
4. security-scan - Trivy vulnerability scanning
5. push - Push to registry (only after tests pass)
6. update-cd - Placeholder for CD pipeline trigger

Tests verify:
- Container starts and LDAP responds
- All OUs created (People, Groups, Services, Policies)
- Schemas loaded (core, cosine, inetorgperson)
- Overlays configured (memberof, refint, unique, ppolicy)
- Service accounts exist
- User/group operations work
- memberOf overlay updates user attributes
- refint overlay cleans up group membership on user delete
- unique overlay rejects duplicate uids
This commit is contained in:
Patrick de Ruiter 2025-12-25 16:01:30 +01:00
parent 3508ac4f7f
commit f15108abb7
Signed by: pderuiter
GPG Key ID: 5EBA7F21CF583321
2 changed files with 580 additions and 31 deletions

View File

@ -1,4 +1,4 @@
name: Build and Push Docker Image
name: CI Pipeline
on:
push:
@ -15,8 +15,25 @@ env:
IMAGE_NAME: enterprise-openldap
jobs:
# Stage 1: Lint Dockerfile
lint:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Dockerfile with hadolint
run: |
docker run --rm -i hadolint/hadolint < Dockerfile || {
echo "::warning::Dockerfile linting found issues (non-blocking)"
}
# Stage 2: Build image
build:
runs-on: docker
needs: lint
outputs:
image_tag: ${{ steps.version.outputs.VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -25,47 +42,165 @@ jobs:
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
# Extract version from tag (v1.0.0 -> 1.0.0)
VERSION="${GITHUB_REF#refs/tags/v}"
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "TAGS=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_OUTPUT
elif [[ "$GITHUB_REF" == refs/heads/main ]]; then
# Use short SHA for main branch
SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7)
echo "VERSION=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "TAGS=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$SHORT_SHA" >> $GITHUB_OUTPUT
else
# Pull request - just use SHA
SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7)
echo "VERSION=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "TAGS=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-$SHORT_SHA" >> $GITHUB_OUTPUT
VERSION="$(echo "$GITHUB_SHA" | cut -c1-7)"
fi
- name: Log in to Docker Registry
if: github.event_name != 'pull_request'
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Build Docker image
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} .
docker build -t ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} .
docker tag ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} ${{ env.IMAGE_NAME }}:test
- name: Tag additional versions
if: github.event_name != 'pull_request'
- name: Save image for subsequent jobs
run: |
mkdir -p /tmp/images
docker save ${{ env.IMAGE_NAME }}:test -o /tmp/images/image.tar
- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: /tmp/images/image.tar
retention-days: 1
# Stage 3: Integration tests
test:
runs-on: docker
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: docker-image
path: /tmp/images
- name: Load Docker image
run: |
docker load -i /tmp/images/image.tar
- name: Run integration tests
run: |
chmod +x tests/test-container.sh
./tests/test-container.sh
env:
IMAGE_NAME: ${{ env.IMAGE_NAME }}:test
CONTAINER_NAME: openldap-ci-test
# Stage 4: Security scan
security-scan:
runs-on: docker
needs: build
steps:
- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: docker-image
path: /tmp/images
- name: Load Docker image
run: |
docker load -i /tmp/images/image.tar
- name: Scan image with Trivy
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL \
--exit-code 0 \
--no-progress \
${{ env.IMAGE_NAME }}:test
- name: Scan for critical vulnerabilities (blocking)
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity CRITICAL \
--exit-code 1 \
--no-progress \
--ignore-unfixed \
${{ env.IMAGE_NAME }}:test || {
echo "::error::Critical vulnerabilities found!"
exit 1
}
# Stage 5: Push to registry
push:
runs-on: docker
needs: [test, security-scan]
if: github.event_name != 'pull_request'
outputs:
version: ${{ steps.version.outputs.VERSION }}
full_image: ${{ steps.version.outputs.FULL_IMAGE }}
steps:
- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: docker-image
path: /tmp/images
- name: Load Docker image
run: |
docker load -i /tmp/images/image.tar
- name: Determine version and tags
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
# For releases, tag with version, major.minor, and latest
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
TAGS="${VERSION},${MAJOR}.${MINOR},latest"
else
VERSION="$(echo "$GITHUB_SHA" | cut -c1-7)"
TAGS="${VERSION},latest"
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "TAGS=$TAGS" >> $GITHUB_OUTPUT
echo "FULL_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" >> $GITHUB_OUTPUT
- name: Log in to Docker Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Tag and push images
run: |
IFS=',' read -ra TAGS <<< "${{ steps.version.outputs.TAGS }}"
for TAG in "${TAGS[@]}"; do
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} "$TAG"
done
- name: Push Docker image
if: github.event_name != 'pull_request'
run: |
IFS=',' read -ra TAGS <<< "${{ steps.version.outputs.TAGS }}"
for TAG in "${TAGS[@]}"; do
docker push "$TAG"
docker tag ${{ env.IMAGE_NAME }}:test ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG
echo "Pushed: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG"
done
- name: Logout from registry
if: always() && github.event_name != 'pull_request'
if: always()
run: docker logout ${{ env.REGISTRY }} || true
# Stage 6: Update CD pipeline (trigger deployment)
update-cd:
runs-on: docker
needs: push
if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Trigger CD pipeline
run: |
echo "=============================================="
echo " Ready to update CD pipeline"
echo "=============================================="
echo "New version: ${{ needs.push.outputs.version }}"
echo "Full image: ${{ needs.push.outputs.full_image }}"
echo ""
echo "TODO: Add step to update version in CD repository"
echo "This could be:"
echo " - Update docker-compose.yml in infra repo"
echo " - Update Helm values"
echo " - Trigger ArgoCD sync"
echo "=============================================="

414
tests/test-container.sh Executable file
View File

@ -0,0 +1,414 @@
#!/bin/sh
#
# OpenLDAP Container Integration Tests
# Runs a series of tests to verify the container works correctly
#
set -e
# Configuration
CONTAINER_NAME="${CONTAINER_NAME:-openldap-test}"
IMAGE_NAME="${IMAGE_NAME:-enterprise-openldap:test}"
LDAP_DOMAIN="${LDAP_DOMAIN:-test.local}"
LDAP_ORGANISATION="${LDAP_ORGANISATION:-Test Organisation}"
LDAP_ADMIN_PASSWORD="${LDAP_ADMIN_PASSWORD:-testpassword123}"
LDAP_BASE_DN="dc=test,dc=local"
MAX_WAIT="${MAX_WAIT:-60}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Counters
TESTS_PASSED=0
TESTS_FAILED=0
# Helper functions
log_info() {
echo "${YELLOW}[INFO]${NC} $1"
}
log_pass() {
echo "${GREEN}[PASS]${NC} $1"
TESTS_PASSED=$((TESTS_PASSED + 1))
}
log_fail() {
echo "${RED}[FAIL]${NC} $1"
TESTS_FAILED=$((TESTS_FAILED + 1))
}
cleanup() {
log_info "Cleaning up test container..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
}
# Ensure cleanup on exit
trap cleanup EXIT
# Start the container
start_container() {
log_info "Starting test container with image: $IMAGE_NAME"
docker run -d \
--name "$CONTAINER_NAME" \
-e LDAP_DOMAIN="$LDAP_DOMAIN" \
-e LDAP_ORGANISATION="$LDAP_ORGANISATION" \
-e LDAP_ADMIN_PASSWORD="$LDAP_ADMIN_PASSWORD" \
-e LDAP_TLS_ENABLED=false \
-e LDAP_CREATE_SERVICE_ACCOUNTS=true \
-e LDAP_LOG_LEVEL=256 \
"$IMAGE_NAME"
if [ $? -eq 0 ]; then
log_pass "Container started"
else
log_fail "Container failed to start"
return 1
fi
}
# Wait for LDAP to be ready
wait_for_ldap() {
log_info "Waiting for LDAP to be ready (max ${MAX_WAIT}s)..."
local count=0
while [ $count -lt $MAX_WAIT ]; do
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost -b "" -s base "objectClass=*" >/dev/null 2>&1; then
log_pass "LDAP is responding"
return 0
fi
count=$((count + 1))
sleep 1
done
log_fail "LDAP did not become ready within ${MAX_WAIT}s"
log_info "Container logs:"
docker logs "$CONTAINER_NAME" 2>&1 | tail -50
return 1
}
# Test: Verify base DN exists
test_base_dn() {
log_info "Testing base DN exists..."
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "$LDAP_BASE_DN" -s base "objectClass=*" >/dev/null 2>&1; then
log_pass "Base DN exists and admin can bind"
else
log_fail "Base DN test failed"
return 1
fi
}
# Test: Verify organizational units
test_organizational_units() {
log_info "Testing organizational units..."
local ous="People Groups Services Policies"
local failed=0
for ou in $ous; do
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "ou=$ou,$LDAP_BASE_DN" -s base "objectClass=organizationalUnit" 2>/dev/null | grep -q "dn: ou=$ou"; then
log_pass "OU=$ou exists"
else
log_fail "OU=$ou not found"
failed=1
fi
done
return $failed
}
# Test: Verify schemas are loaded
test_schemas() {
log_info "Testing schemas are loaded..."
local schemas="core cosine inetorgperson"
local failed=0
# Get loaded schemas
local loaded_schemas=$(docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "cn=schema,cn=config" -s one "objectClass=*" cn 2>/dev/null | grep "^cn:" | sed 's/cn: {[0-9]*}//' | tr -d ' ')
for schema in $schemas; do
if echo "$loaded_schemas" | grep -qi "$schema"; then
log_pass "Schema '$schema' is loaded"
else
log_fail "Schema '$schema' not found"
failed=1
fi
done
return $failed
}
# Test: Verify overlays are configured
test_overlays() {
log_info "Testing overlays are configured..."
local overlays="memberof refint unique ppolicy"
local failed=0
for overlay in $overlays; do
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "cn=config" "olcOverlay=$overlay" 2>/dev/null | grep -q "olcOverlay.*$overlay"; then
log_pass "Overlay '$overlay' is configured"
else
log_fail "Overlay '$overlay' not found"
failed=1
fi
done
return $failed
}
# Test: Verify service accounts exist
test_service_accounts() {
log_info "Testing service accounts..."
local services="keycloak nextcloud gitea postfix dovecot sssd"
local failed=0
for service in $services; do
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "cn=$service,ou=Services,$LDAP_BASE_DN" -s base "objectClass=*" 2>/dev/null | grep -q "dn: cn=$service"; then
log_pass "Service account '$service' exists"
else
log_fail "Service account '$service' not found"
failed=1
fi
done
return $failed
}
# Test: Add a user
test_add_user() {
log_info "Testing add user..."
local user_ldif=$(cat <<EOF
dn: uid=testuser,ou=People,$LDAP_BASE_DN
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: testuser
cn: Test User
sn: User
givenName: Test
mail: testuser@$LDAP_DOMAIN
uidNumber: 10001
gidNumber: 10000
homeDirectory: /home/testuser
loginShell: /bin/bash
userPassword: testuser123
EOF
)
if echo "$user_ldif" | docker exec -i "$CONTAINER_NAME" ldapadd -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
log_pass "User 'testuser' created"
else
log_fail "Failed to create user 'testuser'"
return 1
fi
}
# Test: Add a group with member
test_add_group() {
log_info "Testing add group with member..."
local group_ldif=$(cat <<EOF
dn: cn=testgroup,ou=Groups,$LDAP_BASE_DN
objectClass: groupOfMembers
objectClass: posixGroup
cn: testgroup
gidNumber: 10001
description: Test Group
member: uid=testuser,ou=People,$LDAP_BASE_DN
EOF
)
if echo "$group_ldif" | docker exec -i "$CONTAINER_NAME" ldapadd -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
log_pass "Group 'testgroup' created with member"
else
log_fail "Failed to create group 'testgroup'"
return 1
fi
}
# Test: Verify memberOf overlay works
test_memberof_overlay() {
log_info "Testing memberOf overlay..."
# Check if user has memberOf attribute
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "uid=testuser,ou=People,$LDAP_BASE_DN" memberOf 2>/dev/null | grep -q "memberOf: cn=testgroup"; then
log_pass "memberOf attribute correctly set on user"
else
log_fail "memberOf attribute not found on user"
return 1
fi
}
# Test: User authentication
test_user_authentication() {
log_info "Testing user authentication..."
if docker exec "$CONTAINER_NAME" ldapwhoami -x -H ldap://localhost \
-D "uid=testuser,ou=People,$LDAP_BASE_DN" -w "testuser123" 2>/dev/null | grep -q "testuser"; then
log_pass "User authentication successful"
else
log_fail "User authentication failed"
return 1
fi
}
# Test: Service account can search
test_service_account_search() {
log_info "Testing service account can search users..."
# Get keycloak password from the container
local keycloak_pass=$(docker exec "$CONTAINER_NAME" cat /var/lib/openldap/service-passwords.txt 2>/dev/null | grep keycloak | cut -d: -f2 | tr -d ' ')
if [ -z "$keycloak_pass" ]; then
log_fail "Could not retrieve keycloak service account password"
return 1
fi
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=keycloak,ou=Services,$LDAP_BASE_DN" -w "$keycloak_pass" \
-b "ou=People,$LDAP_BASE_DN" "(uid=testuser)" uid 2>/dev/null | grep -q "uid: testuser"; then
log_pass "Service account can search users"
else
log_fail "Service account search failed"
return 1
fi
}
# Test: Unique overlay (duplicate uid should fail)
test_unique_overlay() {
log_info "Testing unique overlay (duplicate uid should be rejected)..."
local dup_ldif=$(cat <<EOF
dn: uid=testuser2,ou=People,$LDAP_BASE_DN
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: testuser
cn: Duplicate User
sn: Duplicate
uidNumber: 10002
gidNumber: 10000
homeDirectory: /home/testuser2
EOF
)
if echo "$dup_ldif" | docker exec -i "$CONTAINER_NAME" ldapadd -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" 2>&1 | grep -qi "constraint violation\|already exists\|unique"; then
log_pass "Unique overlay rejected duplicate uid"
else
# Check if it was rejected by checking entry doesn't exist
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "uid=testuser2,ou=People,$LDAP_BASE_DN" 2>/dev/null | grep -q "numEntries: 0\|No such object"; then
log_pass "Unique overlay rejected duplicate uid"
else
log_fail "Unique overlay did not reject duplicate uid"
return 1
fi
fi
}
# Test: Refint overlay (delete user should remove from group)
test_refint_overlay() {
log_info "Testing refint overlay (user deletion updates group)..."
# Delete the test user
if docker exec "$CONTAINER_NAME" ldapdelete -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
"uid=testuser,ou=People,$LDAP_BASE_DN" >/dev/null 2>&1; then
# Check if user was removed from group
if docker exec "$CONTAINER_NAME" ldapsearch -x -H ldap://localhost \
-D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" \
-b "cn=testgroup,ou=Groups,$LDAP_BASE_DN" member 2>/dev/null | grep -q "member: uid=testuser"; then
log_fail "Refint overlay did not remove user from group"
return 1
else
log_pass "Refint overlay removed user from group"
fi
else
log_fail "Failed to delete test user"
return 1
fi
}
# Main test execution
main() {
echo "=============================================="
echo " OpenLDAP Container Integration Tests"
echo "=============================================="
echo ""
# Setup
cleanup
start_container || exit 1
wait_for_ldap || exit 1
echo ""
echo "--- Basic Structure Tests ---"
test_base_dn
test_organizational_units
echo ""
echo "--- Schema Tests ---"
test_schemas
echo ""
echo "--- Overlay Configuration Tests ---"
test_overlays
echo ""
echo "--- Service Account Tests ---"
test_service_accounts
echo ""
echo "--- Functional Tests ---"
test_add_user
test_add_group
test_memberof_overlay
test_user_authentication
test_service_account_search
test_unique_overlay
test_refint_overlay
echo ""
echo "=============================================="
echo " Test Results"
echo "=============================================="
echo " ${GREEN}Passed: $TESTS_PASSED${NC}"
echo " ${RED}Failed: $TESTS_FAILED${NC}"
echo "=============================================="
if [ $TESTS_FAILED -gt 0 ]; then
echo ""
log_info "Container logs (last 30 lines):"
docker logs "$CONTAINER_NAME" 2>&1 | tail -30
exit 1
fi
exit 0
}
main "$@"