Skip to content

Commit

Permalink
Merge pull request #29 from gbourniq/fix/fix_permission_denied_in_das…
Browse files Browse the repository at this point in the history
…hboard_container

fix: fix file permission
  • Loading branch information
gbourniq authored Dec 21, 2024
2 parents 220c921 + 1ac663b commit e83e4e4
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 112 deletions.
53 changes: 9 additions & 44 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,61 +138,26 @@ jobs:
- name: Run build
run: make build

integration-api:
name: API Integration Tests
integration-tests:
name: Integration Tests (${{ matrix.component }})
needs: [lint, test, build]
runs-on: ubuntu-latest
strategy:
matrix:
component: [api, dashboard]
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: ~/.cache/poetry
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry config virtualenvs.create false
poetry install
- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Start API service and run integration tests
run: |
docker compose up -d --pull never
pytest tests/integration -v -m api_integration
docker compose down
integration-dashboard:
name: Dashboard Integration Tests
needs: [lint, test, build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Start Dashboard service and run integration tests
run: |
docker build -t dash-app-tests . -f tests/integration/test_dashboard.Dockerfile
docker run --rm dash-app-tests
- name: Run integration tests
run: make test-${{ matrix.component }}

release:
name: Release
needs: [integration-api, integration-dashboard]
needs: [integration-tests]
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -226,7 +191,7 @@ jobs:
publish:
name: Publish Docker Images
needs: release
needs: [release]
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ build: up down
# Integration Testing

test-dashboard:
make up
$(DOCKER_COMPOSE) logs api
docker build -t dash-app-tests . -f tests/integration/test_dashboard.Dockerfile
docker run --rm dash-app-tests
docker run --network host --rm dash-app-tests
make down

test-api:
make up
pytest tests/integration -v -m api_integration
$(DOCKER_COMPOSE) logs api
docker build -t api-tests . -f tests/integration/test_api.Dockerfile
docker run --network host --rm api-tests
make down
6 changes: 3 additions & 3 deletions api.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ COPY portfolio_analytics/common portfolio_analytics/common
COPY portfolio_analytics/api portfolio_analytics/api

# Set ownership and switch to non-root user
RUN chown -R appuser:appuser /app
USER appuser
RUN chown -R 1000:1000 /app
USER 1000:1000

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
HEALTHCHECK --interval=10s --timeout=3s \
CMD curl -f http://localhost:8000/ || exit 1

# Command for production server
Expand Down
5 changes: 3 additions & 2 deletions base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ RUN apt-get update && apt-get install -y \
procps \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create user with explicit UID/GID
RUN groupadd -g 1000 appgroup && \
useradd -u 1000 -g appgroup -s /bin/bash -m appuser

WORKDIR /app

Expand Down
6 changes: 3 additions & 3 deletions dashboard.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ COPY portfolio_analytics/common portfolio_analytics/common
COPY portfolio_analytics/dashboard portfolio_analytics/dashboard

# Set ownership and switch to non-root user
RUN chown -R appuser:appuser /app
USER appuser
RUN chown -R 1000:1000 /app
USER 1000:1000

# Expose port
EXPOSE 8050

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
HEALTHCHECK --interval=10s --timeout=3s \
CMD curl -f http://localhost:8050/ || exit 1

# Command for production server
Expand Down
27 changes: 17 additions & 10 deletions data.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ FROM alpine:3.18

WORKDIR /data

# Create appuser with same UID/GID as in python-base
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Add user with same UID/GID as other containers
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# Copy your data directory
COPY data/ .
# Create directories first
RUN mkdir -p /data/market_data /data/cache /data/portfolios

# Make sure the files are owned by appuser
RUN chown -R appuser:appuser /data
# Copy data files
COPY --chown=1000:1000 data/ .

# Use appuser
USER appuser
# Set permissions for shared access and verify
RUN chown -R 1000:1000 /data && \
chmod -R 775 /data && \
chmod g+s /data /data/market_data /data/cache /data/portfolios && \
echo "Verifying permissions:" && \
ls -ln /data

CMD ["tail", "-f", "/dev/null"]
USER 1000:1000

# Add permission check to startup
CMD ls -ln /data && echo "Running as: $(id)" && tail -f /dev/null
6 changes: 3 additions & 3 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ echo "Deploying application version ${GIT_TAG:-latest}"
# Data service
docker run -d \
--name data \
--user appuser:appuser \
--user 1000:1000 \
-v shared-data:/data \
ghcr.io/gbourniq/portfolio_analytics/data:${GIT_TAG:-latest}

# Dashboard service
docker run -d \
--name api \
--user appuser:appuser \
--user 1000:1000 \
-v shared-data:/app/data \
-p 8000:8000 \
ghcr.io/gbourniq/portfolio_analytics/api:${GIT_TAG:-latest}

# API service
docker run -d \
--name dashboard \
--user appuser:appuser \
--user 1000:1000 \
-v shared-data:/app/data \
-p 8050:8050 \
ghcr.io/gbourniq/portfolio_analytics/dashboard:${GIT_TAG:-latest}
28 changes: 22 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
volumes:
shared-data:
driver: local
# driver_opts:
# type: none
# o: bind
# device: ${PWD}/data
# name: portfolio-analytics-data

services:
data:
Expand All @@ -8,8 +14,8 @@ services:
context: .
dockerfile: data.Dockerfile
volumes:
- shared-data:/data
user: "appuser:appuser"
- shared-data:/data:rw
user: "1000:1000"

api:
image: ghcr.io/gbourniq/portfolio_analytics/api:${GIT_TAG:-latest}
Expand All @@ -19,15 +25,20 @@ services:
ports:
- "8000:8000"
volumes:
- shared-data:/app/data
user: "appuser:appuser"
- shared-data:/app/data:rw
group_add:
- "1000"
user: "1000:1000"
deploy:
resources:
limits:
cpus: '0.2'
memory: 384M
reservations:
memory: 192M
depends_on:
data:
condition: service_started

dashboard:
image: ghcr.io/gbourniq/portfolio_analytics/dashboard:${GIT_TAG:-latest}
Expand All @@ -37,12 +48,17 @@ services:
ports:
- "8050:8050"
volumes:
- shared-data:/app/data
user: "appuser:appuser"
- shared-data:/app/data:rw
group_add:
- "1000"
user: "1000:1000"
deploy:
resources:
limits:
cpus: '0.8'
memory: 1024M
reservations:
memory: 512M
depends_on:
data:
condition: service_started
2 changes: 1 addition & 1 deletion portfolio_analytics/common/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def get_version():
str: The version string from pyproject.toml, or '0.0.0' if reading fails
"""
try:
pyproject_path = Path(__file__).parents[3] / "pyproject.toml"
pyproject_path = Path(__file__).parents[2] / "pyproject.toml"
with open(pyproject_path, "rb") as f:
pyproject_data = tomli.load(f)
return pyproject_data["tool"]["poetry"]["version"]
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/test_api.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.12-slim

WORKDIR /app

# Install Python packages
RUN pip install --no-cache-dir pytest requests

# Copy test files
COPY tests/integration/test_api.py ./test_api.py

# Set Python path
ENV PYTHONPATH=/app

# Run tests
CMD ["pytest", "test_api.py", "-v", "-m", "api_integration"]
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,15 @@ def test_fx_data_pipeline(self, api_client: requests.Session) -> None:
response = api_client.post(f"{BASE_URL}/market_data/fx", json=payload)

# Then
assert response.status_code == HTTPStatus.OK
data = response.json()

# Verify response structure
assert "output_path" in data
assert "file_stats" in data

# Verify data coverage
stats = data["file_stats"]
assert stats["row_count"] > 0
assert any("USD" in ticker for ticker in stats["currencies_covered"])
assert any("EUR" in ticker for ticker in stats["currencies_covered"])
try:
assert response.status_code == HTTPStatus.OK, \
f"API returned {response.status_code}: {response.text}"
except AssertionError as e:
print(f"\nRequest URL: {response.request.url}")
print(f"Request Headers: {response.request.headers}")
print(f"Request Body: {response.request.body}")
print(f"Response Headers: {response.headers}")
raise e


@pytest.mark.integration
Expand All @@ -86,9 +83,15 @@ def test_portfolio_workflow(
files = {"file": ("test_portfolio.csv", sample_portfolio, "text/csv")}
response = api_client.post(f"{BASE_URL}/portfolio", files=files)

assert response.status_code == HTTPStatus.CREATED
upload_data = response.json()
assert upload_data["filename"] == "test_portfolio.csv"
try:
assert response.status_code == HTTPStatus.CREATED, \
f"API returned {response.status_code}: {response.text}"
except AssertionError as e:
print(f"\nRequest URL: {response.request.url}")
print(f"Request Headers: {response.request.headers}")
print(f"Request Body: {response.request.body}")
print(f"Response Headers: {response.headers}")
raise e

# 2. Download all portfolios (including our upload)
response = api_client.get(
Expand Down
3 changes: 0 additions & 3 deletions tests/integration/test_dashboard.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ RUN pip install --no-cache-dir \
pandas

# Copy files to container
COPY portfolio_analytics/dashboard portfolio_analytics/dashboard
COPY portfolio_analytics/common portfolio_analytics/common
COPY data data
COPY pyproject.toml ./pyproject.toml
COPY tests/integration/test_dashboard.py ./test_dashboard.py

Expand Down
Loading

0 comments on commit e83e4e4

Please sign in to comment.