Files
tankstopp-app/scripts/docker/validate.sh
T
2025-07-07 01:44:12 +02:00

474 lines
13 KiB
Bash
Executable File

#!/bin/bash
# TankStopp Dockerfile Validation Script
# This script validates Dockerfile syntax and best practices without requiring Docker daemon
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
DOCKERFILE_PATH="Dockerfile"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
VALIDATION_ERRORS=0
VALIDATION_WARNINGS=0
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
((VALIDATION_WARNINGS++))
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
((VALIDATION_ERRORS++))
}
check_dockerfile_exists() {
log_info "Checking Dockerfile existence..."
if [ ! -f "$PROJECT_ROOT/$DOCKERFILE_PATH" ]; then
log_error "Dockerfile not found at $PROJECT_ROOT/$DOCKERFILE_PATH"
return 1
fi
log_success "Dockerfile found"
return 0
}
validate_dockerfile_syntax() {
log_info "Validating Dockerfile syntax..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check for FROM instruction
if ! grep -q "^FROM " "$dockerfile"; then
log_error "No FROM instruction found"
else
log_success "FROM instruction found"
fi
# Check for multi-stage build
local from_count=$(grep -c "^FROM " "$dockerfile")
if [ "$from_count" -gt 1 ]; then
log_success "Multi-stage build detected ($from_count stages)"
else
log_warning "Single-stage build detected. Consider multi-stage for optimization"
fi
# Check for WORKDIR
if grep -q "^WORKDIR " "$dockerfile"; then
log_success "WORKDIR instruction found"
else
log_warning "No WORKDIR instruction found"
fi
# Check for USER instruction
if grep -q "^USER " "$dockerfile"; then
log_success "USER instruction found (security best practice)"
else
log_warning "No USER instruction found. Running as root is not recommended"
fi
# Check for EXPOSE instruction
if grep -q "^EXPOSE " "$dockerfile"; then
log_success "EXPOSE instruction found"
else
log_warning "No EXPOSE instruction found"
fi
# Check for HEALTHCHECK
if grep -q "^HEALTHCHECK " "$dockerfile"; then
log_success "HEALTHCHECK instruction found"
else
log_warning "No HEALTHCHECK instruction found"
fi
# Check for VOLUME
if grep -q "^VOLUME " "$dockerfile"; then
log_success "VOLUME instruction found"
else
log_warning "No VOLUME instruction found"
fi
}
validate_base_images() {
log_info "Validating base images..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Extract base images
local base_images=$(grep "^FROM " "$dockerfile" | awk '{print $2}')
for image in $base_images; do
if [[ "$image" == *":latest" ]]; then
log_warning "Base image '$image' uses 'latest' tag. Consider pinning to specific version"
elif [[ "$image" =~ :[0-9]+\.[0-9]+ ]]; then
log_success "Base image '$image' uses versioned tag"
else
log_info "Base image: $image"
fi
# Check for official images
if [[ "$image" =~ ^(alpine|golang|node|nginx|redis|postgres|mysql):.* ]]; then
log_success "Using official base image: $image"
fi
done
}
validate_security_practices() {
log_info "Validating security practices..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check if running as root
if grep -q "USER root" "$dockerfile"; then
log_error "Explicitly running as root user"
fi
# Check for package manager cache cleanup
if grep -q "rm -rf /var/cache/apk/\*\|apt-get clean\|yum clean" "$dockerfile"; then
log_success "Package manager cache cleanup found"
else
log_warning "Consider cleaning package manager cache to reduce image size"
fi
# Check for unnecessary packages
if grep -q "curl\|wget" "$dockerfile"; then
local curl_line=$(grep -n "curl\|wget" "$dockerfile" | head -1)
log_info "Network tools found: $curl_line"
fi
# Check for secrets in Dockerfile
if grep -qi "password\|secret\|key\|token" "$dockerfile"; then
log_warning "Potential secrets found in Dockerfile. Ensure no hardcoded credentials"
fi
}
validate_build_optimization() {
log_info "Validating build optimization..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check for layer optimization
local run_count=$(grep -c "^RUN " "$dockerfile")
if [ "$run_count" -gt 10 ]; then
log_warning "Many RUN instructions ($run_count). Consider combining for fewer layers"
else
log_success "Reasonable number of RUN instructions ($run_count)"
fi
# Check for COPY vs ADD
if grep -q "^ADD " "$dockerfile"; then
log_warning "ADD instruction found. Consider using COPY unless URL/tar extraction is needed"
fi
# Check for .dockerignore
if [ -f "$PROJECT_ROOT/.dockerignore" ]; then
log_success ".dockerignore file found"
else
log_warning ".dockerignore file not found. Consider adding to reduce build context"
fi
# Check for build args
if grep -q "^ARG " "$dockerfile"; then
log_success "Build arguments found for flexibility"
fi
}
validate_required_files() {
log_info "Validating required files..."
# Check for Go module files
if [ -f "$PROJECT_ROOT/go.mod" ]; then
log_success "go.mod found"
else
log_error "go.mod not found"
fi
if [ -f "$PROJECT_ROOT/go.sum" ]; then
log_success "go.sum found"
else
log_warning "go.sum not found"
fi
# Check for main.go or cmd directory
if [ -f "$PROJECT_ROOT/main.go" ] || [ -d "$PROJECT_ROOT/cmd" ]; then
log_success "Main Go file or cmd directory found"
else
log_error "No main.go or cmd directory found"
fi
# Check for static files
if [ -d "$PROJECT_ROOT/static" ]; then
log_success "Static directory found"
else
log_warning "Static directory not found"
fi
# Check for configuration files
if ls "$PROJECT_ROOT"/config*.yaml >/dev/null 2>&1; then
log_success "Configuration files found"
else
log_warning "No configuration files found"
fi
}
validate_environment_variables() {
log_info "Validating environment variables..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check for ENV instructions
local env_count=$(grep -c "^ENV " "$dockerfile")
if [ "$env_count" -gt 0 ]; then
log_success "Environment variables defined ($env_count)"
# Show environment variables
grep "^ENV " "$dockerfile" | while read line; do
log_info " $line"
done
else
log_info "No ENV instructions found"
fi
# Check for common application variables
if grep -q "TANKSTOPP_" "$dockerfile"; then
log_success "Application-specific environment variables found"
fi
}
validate_ports_and_volumes() {
log_info "Validating ports and volumes..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check exposed ports
if grep -q "^EXPOSE " "$dockerfile"; then
local ports=$(grep "^EXPOSE " "$dockerfile" | awk '{print $2}')
log_success "Exposed ports: $ports"
fi
# Check volumes
if grep -q "^VOLUME " "$dockerfile"; then
local volumes=$(grep "^VOLUME " "$dockerfile" | sed 's/VOLUME //')
log_success "Volumes defined: $volumes"
fi
}
validate_labels_and_metadata() {
log_info "Validating labels and metadata..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check for labels
if grep -q "^LABEL " "$dockerfile"; then
log_success "Labels found"
grep "^LABEL " "$dockerfile" | while read line; do
log_info " $line"
done
else
log_warning "No labels found. Consider adding metadata labels"
fi
}
validate_entrypoint_and_cmd() {
log_info "Validating entrypoint and command..."
local dockerfile="$PROJECT_ROOT/$DOCKERFILE_PATH"
# Check for ENTRYPOINT
if grep -q "^ENTRYPOINT " "$dockerfile"; then
log_success "ENTRYPOINT instruction found"
fi
# Check for CMD
if grep -q "^CMD " "$dockerfile"; then
log_success "CMD instruction found"
local cmd=$(grep "^CMD " "$dockerfile" | tail -1)
log_info " $cmd"
else
log_warning "No CMD instruction found"
fi
}
check_docker_compose_files() {
log_info "Checking Docker Compose files..."
if [ -f "$PROJECT_ROOT/docker-compose.yml" ]; then
log_success "docker-compose.yml found"
else
log_warning "docker-compose.yml not found"
fi
if [ -f "$PROJECT_ROOT/docker-compose.prod.yml" ]; then
log_success "docker-compose.prod.yml found"
fi
if [ -f "$PROJECT_ROOT/docker-compose.override.yml" ]; then
log_info "docker-compose.override.yml found"
fi
}
validate_build_scripts() {
log_info "Checking build scripts..."
if [ -f "$PROJECT_ROOT/scripts/docker/build.sh" ]; then
log_success "Docker build script found"
if [ -x "$PROJECT_ROOT/scripts/docker/build.sh" ]; then
log_success "Build script is executable"
else
log_warning "Build script is not executable"
fi
else
log_warning "Docker build script not found"
fi
if [ -f "$PROJECT_ROOT/scripts/docker/deploy.sh" ]; then
log_success "Docker deploy script found"
if [ -x "$PROJECT_ROOT/scripts/docker/deploy.sh" ]; then
log_success "Deploy script is executable"
else
log_warning "Deploy script is not executable"
fi
else
log_warning "Docker deploy script not found"
fi
}
generate_recommendations() {
log_info "Generating recommendations..."
echo ""
echo "=== RECOMMENDATIONS ==="
if [ "$VALIDATION_ERRORS" -eq 0 ] && [ "$VALIDATION_WARNINGS" -eq 0 ]; then
log_success "Dockerfile validation passed with no issues!"
else
echo ""
echo "Consider the following improvements:"
echo ""
if grep -q "USER root" "$PROJECT_ROOT/$DOCKERFILE_PATH" 2>/dev/null; then
echo "• Create and use a non-root user for security"
fi
if ! grep -q "HEALTHCHECK" "$PROJECT_ROOT/$DOCKERFILE_PATH" 2>/dev/null; then
echo "• Add HEALTHCHECK instruction for container monitoring"
fi
if ! grep -q "LABEL" "$PROJECT_ROOT/$DOCKERFILE_PATH" 2>/dev/null; then
echo "• Add metadata labels (version, maintainer, description)"
fi
if [ ! -f "$PROJECT_ROOT/.dockerignore" ]; then
echo "• Create .dockerignore to optimize build context"
fi
if ! grep -q "VOLUME" "$PROJECT_ROOT/$DOCKERFILE_PATH" 2>/dev/null; then
echo "• Consider adding VOLUME for data persistence"
fi
echo "• Pin base image versions for reproducible builds"
echo "• Minimize the number of layers by combining RUN commands"
echo "• Remove package manager caches to reduce image size"
echo "• Use multi-stage builds to reduce final image size"
fi
}
show_summary() {
echo ""
echo "=== VALIDATION SUMMARY ==="
echo "Dockerfile: $PROJECT_ROOT/$DOCKERFILE_PATH"
echo "Errors: $VALIDATION_ERRORS"
echo "Warnings: $VALIDATION_WARNINGS"
if [ "$VALIDATION_ERRORS" -eq 0 ]; then
log_success "Dockerfile validation completed successfully!"
echo "Your Dockerfile is ready for building."
else
log_error "Dockerfile validation failed with $VALIDATION_ERRORS errors"
echo "Please fix the errors before building the Docker image."
exit 1
fi
}
main() {
echo "TankStopp Dockerfile Validation"
echo "==============================="
echo ""
cd "$PROJECT_ROOT"
# Run all validations
check_dockerfile_exists || exit 1
validate_dockerfile_syntax
validate_base_images
validate_security_practices
validate_build_optimization
validate_required_files
validate_environment_variables
validate_ports_and_volumes
validate_labels_and_metadata
validate_entrypoint_and_cmd
check_docker_compose_files
validate_build_scripts
# Generate recommendations and summary
generate_recommendations
show_summary
}
# Show help if requested
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
cat << EOF
TankStopp Dockerfile Validation Script
Usage: $0 [OPTIONS]
This script validates your Dockerfile against best practices and security guidelines.
It checks syntax, security practices, optimization, and required files.
OPTIONS:
-h, --help Show this help message
CHECKS PERFORMED:
• Dockerfile syntax validation
• Base image version pinning
• Security best practices
• Build optimization
• Required files presence
• Environment variables
• Port and volume configuration
• Labels and metadata
• Entrypoint and command setup
• Docker Compose files
• Build scripts
EXAMPLES:
# Run validation
$0
# Show help
$0 --help
EOF
exit 0
fi
# Run main function
main "$@"