Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
github
.idea
.vscode
Dockerfile
docker-compose.yml
LICENSE
.dockerignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
.vscode
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ RUN apk add --no-cache \
bash \
curl \
yq \
python3
python3 \
py3-pip

COPY requirements.txt /app/requirements.txt

# pip install
RUN pip3 install --break-system-packages -r /app/requirements.txt

# Create required directories and log files
RUN mkdir -p /etc/quantixy /tmp/quantixy_last_access /app /var/log/nginx && \
Expand All @@ -30,6 +36,8 @@ COPY log_monitor.py /app/log_monitor.py
# Copy inactivity monitor
COPY inactivity_monitor.py /app/inactivity_monitor.py

COPY utils.py /app/utils.py

# Copy entrypoint script and make it executable
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
Expand Down
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ services:
environment:
- TIMEOUT_MINUTES=1 # Time (in minutes) after which containers will shutdown (after inactivity)
- VERBOSE_LOGGING=false
- QUANTIXY__exemple.com__container: test
- QUANTIXY__exemple.com__port: 1234
- QUANTIXY__exemple.com__protocol: http
- QUANTIXY__exemple.com__websocket: true
#- LOADING_PAGE_PATH= # If you want custom loading
```

Expand Down Expand Up @@ -83,19 +87,36 @@ The main configuration is located in `nginx/conf.d/default.conf` and includes:

### Service Configuration

Services are configured through `services.yaml` (implementation pending). Example structure:
Services are configured through `services.yaml` (implementation pending) or environment variables. Example structure:

#### service.yaml:
```yaml
services:
- domain: example.com
container: my_example_app
port: 8000
websocket: false

- domain: ws.example.com
container: my_ws_app
port: 3000
websocket: true
example.com:
container: my_example_app
port: 8000
websocket: false
protocol: http

ws.example.com:
container: my_ws_app
port: 3000
websocket: true
protocol: http

domain.tld:
container: name
port: 1234
protocol: http # https
websocket: true # false

```

#### Environment Variables:
```yaml
QUANTIXY__domain.do__container: container_name
QUANTIXY__domain.do__port: 80 # container_port
QUANTIXY__domain.do__protocol: http # container_service: http or https
QUANTIXY__domain.do__websocket: true # container_use_websocket: true or false
```

## 🔗 Endpoints
Expand Down
16 changes: 13 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./services.yaml:/etc/quantixy/services.yaml
environment:
- TIMEOUT_MINUTES=10
- VERBOSE_LOGGING=false
#- LOADING_PAGE_PATH=
TIMEOUT_MINUTES: 10
VERBOSE_LOGGING: false
#LOADING_PAGE_PATH:
QUANTIXY__exemple.com__container: test
QUANTIXY__exemple.com__port: 1234
QUANTIXY__exemple.com__protocol: http
QUANTIXY__exemple.com__websocket: true
healthcheck:
test: curl --fail http://localhost:80/health || exit 1
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
74 changes: 56 additions & 18 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,39 @@ VERBOSE_LOGGING=${VERBOSE_LOGGING:-"false"}
export TIMEOUT_MINUTES
export VERBOSE_LOGGING

# Genrate services.yaml from environment variables
generate_yaml_from_env() {
local prefix="QUANTIXY__"
local yaml_content=""

for env_var in $(env | grep "^${prefix}"); do
# Extract the key and value
key=$(echo "$env_var" | cut -d= -f1)
value=$(echo "$env_var" | cut -d= -f2-)

# Remove the prefix and convert to YAML structure
key_without_prefix=${key#${prefix}}
domain=$(echo "$key_without_prefix" | cut -d'__' -f1)
subkey=$(echo "$key_without_prefix" | cut -d'__' -f2- | tr '[:upper:]' '[:lower:]')

# if yaml_content does not contain the domain, add it
if [[ ! "$yaml_content" =~ ^[[:space:]]*${domain}: ]]; then
yaml_content+="${domain}:\n"
fi

# Append to YAML content
yaml_content+=" ${subkey#__}: \"${value}\"\n"
done

# Output the generated YAML
echo -e "$yaml_content"
}

# Function to start a container
start_container() {
local domain=$1
local container_name=$(yq e ".${domain}.container" $SERVICES_CONFIG)
local container_port=$(yq e ".${domain}.port" $SERVICES_CONFIG)
local container_name=$(yq e ".${domain}.container" /tmp/merged_services.yaml)
local container_port=$(yq e ".${domain}.port" /tmp/merged_services.yaml)

if [ -z "$container_name" ] || [ "$container_name" == "null" ]; then
echo "❌ No container mapping found for domain $domain"
Expand Down Expand Up @@ -63,7 +91,7 @@ stop_container() {
# Create a timestamp file for each service to track last access
touch_last_access_file() {
local domain=$1
local container_name=$(yq e ".${domain}.container" $SERVICES_CONFIG)
local container_name=$(yq e ".${domain}.container" /tmp/merged_services.yaml)
if [ -n "$container_name" ] && [ "$container_name" != "null" ]; then
mkdir -p /tmp/quantixy_last_access
touch "/tmp/quantixy_last_access/${container_name}"
Expand Down Expand Up @@ -127,24 +155,18 @@ server {
}

EOF

# Generate server blocks for each domain in services.yaml
#echo "🔧 DEBUG: Reading domains from services.yaml..."
#echo "🔧 DEBUG: Services config content:"
#cat "$SERVICES_CONFIG"

# Use a temporary file to build the dynamic configs
temp_config="/tmp/dynamic_servers.conf"
>"$temp_config" # Clear temp file

# Get all domains and process them
yq e 'keys | .[]' $SERVICES_CONFIG | while IFS= read -r domain; do
yq e 'keys | .[]' /tmp/merged_services.yaml | while IFS= read -r domain; do
if [ -n "$domain" ]; then
#echo "🔧 DEBUG: Processing domain: '$domain'"
container_name=$(yq e ".[\"${domain}\"].container" $SERVICES_CONFIG)
port=$(yq e ".[\"${domain}\"].port" $SERVICES_CONFIG)
protocol=$(yq e ".[\"${domain}\"].protocol // \"http\"" $SERVICES_CONFIG)
websocket=$(yq e ".[\"${domain}\"].websocket // false" $SERVICES_CONFIG)
container_name=$(yq e ".[\"${domain}\"].container" /tmp/merged_services.yaml)
port=$(yq e ".[\"${domain}\"].port" /tmp/merged_services.yaml)
protocol=$(yq e ".[\"${domain}\"].protocol // \"http\"" /tmp/merged_services.yaml)
websocket=$(yq e ".[\"${domain}\"].websocket // false" /tmp/merged_services.yaml)

#echo "🔧 DEBUG: Domain '$domain' -> container: '$container_name', port: '$port'"

Expand Down Expand Up @@ -229,8 +251,24 @@ EOF
fi
}

echo "générating /tmp/generated_services.yaml from environment variables..."
# Generate YAML from environment variables
generated_yaml=$(generate_yaml_from_env)

if [ "$VERBOSE_LOGGING" = "true" ]; then
echo "🔧 DEBUG: Generated YAML content:"
echo "$generated_yaml"
fi

# Use yq to process the generated YAML
echo "$generated_yaml" > /tmp/generated_services.yaml

echo "merge /tmp/generated_services.yaml with $SERVICES_CONFIG..."
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' /tmp/generated_services.yaml $SERVICES_CONFIG > /tmp/merged_services.yaml


# Generate NGINX configuration from services.yaml
echo "Generating NGINX configuration from services.yaml..."
echo "Generating NGINX configuration from /tmp/merged_services.yaml..."
generate_nginx_config

# Ensure log file exists before starting NGINX
Expand Down Expand Up @@ -259,7 +297,7 @@ echo "Services configuration: $SERVICES_CONFIG"
echo "Timeout set to: $TIMEOUT_MINUTES minutes"

# Initialize last access times for all configured containers
yq e 'keys | .[]' $SERVICES_CONFIG | while read domain; do
yq e 'keys | .[]' /tmp/merged_services.yaml | while read domain; do
touch_last_access_file "$domain"
done

Expand All @@ -268,8 +306,8 @@ done
while true; do
sleep 60 # Check every minute
# Iterate over configured domains
yq e 'keys | .[]' $SERVICES_CONFIG | while read domain; do
container_name=$(yq e ".${domain}.container" $SERVICES_CONFIG)
yq e 'keys | .[]' /tmp/merged_services.yaml | while read domain; do
container_name=$(yq e ".${domain}.container" /tmp/merged_services.yaml)
if [ -z "$container_name" ] || [ "$container_name" == "null" ]; then
continue
fi
Expand Down
32 changes: 22 additions & 10 deletions inactivity_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import sys
from pathlib import Path

from utils import load_services_config

# Configure logging based on VERBOSE_LOGGING environment variable
VERBOSE_LOGGING = os.environ.get('VERBOSE_LOGGING', 'false').lower() in ('true', '1', 'yes')

Expand All @@ -28,29 +30,39 @@
)
logger = logging.getLogger('inactivity_monitor')

# create stdout logger
STDOUT_LOGGER = logging.StreamHandler(sys.stdout) if VERBOSE_LOGGING else None

# Configuration
TIMEOUT_MINUTES = int(os.environ.get('TIMEOUT_MINUTES', '10'))
LAST_ACCESS_DIR = Path('/tmp/quantixy_last_access')
CHECK_INTERVAL = 30 # Check half minute
SERVICES_CONFIG = '/etc/quantixy/services.yaml'

if VERBOSE_LOGGING:
logger.info(f"Inactivity monitor started with timeout: {TIMEOUT_MINUTES} minutes")
logger.info(f"Verbose logging enabled: {VERBOSE_LOGGING}")
else:
logger.warning(f"Inactivity monitor started (timeout: {TIMEOUT_MINUTES}m, verbose: {VERBOSE_LOGGING})")


def get_containers():
"""Get list of container names from services.yaml using yq"""
"""Get list of container names from services.yaml using native Python libraries"""
try:
result = subprocess.run(
['yq', 'e', ".* | select(has(\"container\")) | .container", SERVICES_CONFIG],
capture_output=True, text=True, check=True
)
containers = [c.strip() for c in result.stdout.splitlines() if c.strip()]
return containers
except subprocess.CalledProcessError as e:
logger.error(f"Failed to read containers: {e}")
services_data = load_services_config()

if not services_data:
logger.error("Services configuration is empty or invalid")
return []

# Extract containers from the YAML data
containers = [
service.get('container')
for service in services_data.values()
if isinstance(service, dict) and 'container' in service
]
return [c for c in containers if c] # Filter out None values
except Exception as e:
logger.error(f"Unexpected error: {e}")
return []

def check_container_running(container_name):
Expand Down
Loading