diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..20b2466 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/data/ +/.idea/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100755 index 0000000..10e2c42 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,29 @@ +stages: + - test + - build + - deploy + +variables: + DOCKER_REGISTRY: "registry.beaconborn.ru:5005" + DOCKER_IMAGE: "$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME" + DOCKER_TAG: "latest" + + +test: + stage: test + image: python:3.11-alpine + script: + - pip install -r requirements.txt + - pytest --maxfail=1 --disable-warnings -q + only: + - main + + +build: + stage: build + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY + - docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG . + - docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG + only: + - main \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..5af34fb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-alpine AS base +WORKDIR /app + +FROM base AS builder + +COPY requirements.txt /app +RUN pip wheel --no-cache-dir -q \ + --no-deps --wheel-dir /app/wheels \ + -r requirements.txt + +FROM base AS runner + +COPY --from=builder /app/wheels /wheels +RUN pip install --no-cache-dir -q /wheels/* +COPY proxy/rss_proxy.py healthcheck.py /app/ + +ENV PYTHONUNBUFFERED=1 +EXPOSE 5050 +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD python3 test.py + +CMD ["gunicorn", "--workers", "4", "--bind", "0.0.0.0:5050", "rss_proxy:app"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..87a8b0c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + proxy: + build: . + container_name: proxy + ports: + - "5050:5050" + depends_on: + - redis + healthcheck: + test: [ "CMD", "python3", "/app/test.py" ] + interval: 30s + timeout: 10s + retries: 3 + + redis: + image: redis:latest + container_name: redis + restart: unless-stopped + command: [ "redis-server" ] + ports: + - "6379:6379" + volumes: + - ./data:/data + environment: + - TZ=Europe/Moscow + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 5s + retries: 3 diff --git a/proxy/__init__.py b/proxy/__init__.py new file mode 100755 index 0000000..41a567d --- /dev/null +++ b/proxy/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask +from .config import Config + +app = Flask(__name__) +app.config.from_object(Config) + +from proxy import rss_proxy, healthcheck diff --git a/proxy/config.py b/proxy/config.py new file mode 100755 index 0000000..3613970 --- /dev/null +++ b/proxy/config.py @@ -0,0 +1,3 @@ +class Config: + """Configuration class for Flask""" + PROXY_URL = None # Set proxy URL if needed diff --git a/proxy/healthcheck.py b/proxy/healthcheck.py new file mode 100755 index 0000000..05c8932 --- /dev/null +++ b/proxy/healthcheck.py @@ -0,0 +1,10 @@ +from flask import Response +from proxy import app + +@app.route("/health") +def healthcheck(): + """Health check route to monitor service status""" + try: + return Response("OK", status=200) + except Exception as e: + return f"Error: {e}", 500 diff --git a/proxy/rss_proxy.py b/proxy/rss_proxy.py new file mode 100755 index 0000000..863dea6 --- /dev/null +++ b/proxy/rss_proxy.py @@ -0,0 +1,27 @@ +import urllib.parse +from flask import request, Response +import requests +import os +from proxy import app + +PROXY_URL = os.getenv("PROXY_URL") + +@app.route("/proxy") +def proxy(): + """Proxy RSS feed with forced re-encoding to UTF-8""" + raw_query = request.query_string.decode() + if raw_query.startswith("url="): + url = urllib.parse.unquote(raw_query[4:]) + else: + return "Missing URL", 400 + + try: + proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else None + r = requests.get(url, timeout=10, proxies=proxies) + + r.encoding = "windows-1251" if "windows-1251" in r.headers.get("content-type", "").lower() else r.apparent_encoding + response_text = r.text.replace('', '') + + return Response(response_text, content_type="application/xml; charset=utf-8") + except Exception as e: + return f"Error: {e}", 500 diff --git a/proxy/test.py b/proxy/test.py new file mode 100755 index 0000000..7da6ce5 --- /dev/null +++ b/proxy/test.py @@ -0,0 +1,18 @@ +import requests +import sys + +def check_health(url="http://localhost:5050/health"): + try: + response = requests.get(url, timeout=10) + if response.status_code == 200: + print("Health check passed") + sys.exit(0) # Успешная проверка + else: + print(f"Health check failed: {response.status_code}") + sys.exit(1) # Ошибка + except requests.exceptions.RequestException as e: + print(f"Health check failed: {e}") + sys.exit(1) # Ошибка + +if __name__ == "__main__": + check_health() diff --git a/proxy/tests/test_rss_proxy.py b/proxy/tests/test_rss_proxy.py new file mode 100755 index 0000000..4235438 --- /dev/null +++ b/proxy/tests/test_rss_proxy.py @@ -0,0 +1,22 @@ +import unittest +from proxy import app + + +class FlaskTestCase(unittest.TestCase): + def setUp(self): + self.app = app.test_client() + self.app.testing = True + + def test_healthcheck(self): + response = self.app.get('/health') + self.assertEqual(response.status_code, 200) + + def test_proxy_missing_url(self): + response = self.app.get('/proxy') + self.assertEqual(response.status_code, 400) + + def test_proxy_with_url(self): + test_url = 'https://tapochek.net/rss/rssdg.xml' + response = self.app.get(f'/proxy?url={test_url}') + self.assertEqual(response.status_code, 200) + self.assertTrue(response.data) diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..b76320d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +requests==2.32.3 +Flask==3.1.0 +loguru==0.7.3 +redis==5.2.1 +gunicorn==23.0.0