diff --git a/src/main.py b/src/main.py index aadd3be..951d05a 100644 --- a/src/main.py +++ b/src/main.py @@ -80,7 +80,18 @@ def start(repo_url, report): repository_manager.dockerfile_path = str(pathlib.Path(docker_files[0]).absolute()) repository_manager.parse_docker_image() + docker_image = repository_manager.docker_image + + if not docker_image: + console.print("Unable to determine the base image from the Dockerfile.", style="red1") + docker_image = click.prompt("Enter a valid Docker image to use e.g. python:3.11", type=str) + repository_manager.parse_poetry_version() + poetry_version = repository_manager.poetry_version + + if not poetry_version: + console.print("Unable to determine the poetry version from the Dockerfile.", style="red1") + poetry_version = click.prompt("Enter a valid Poetry version to use e.g. 1.8.2", type=str) table = Table(title="Repository Information", box=box.MARKDOWN, show_lines=True) table.add_column("", style="bright_white") @@ -88,8 +99,8 @@ def start(repo_url, report): table.add_row("Repository URL", repository_manager.repo_url) table.add_row("Branch Name", repository_manager.get_branch()) table.add_row("Dockerfile Path", repository_manager.dockerfile_path) - table.add_row("Poetry Version", repository_manager.poetry_version) - table.add_row("Docker Image", repository_manager.docker_image) + table.add_row("Poetry Version", poetry_version) + table.add_row("Docker Image", docker_image) console.print(table) process = click.confirm("Do you want to continue with the above details?", default=True) @@ -102,8 +113,8 @@ def start(repo_url, report): # run the docker image docker = DockerManager( - repository_manager.docker_image, - repository_manager.poetry_version, + docker_image, + poetry_version, repository_manager.get_repo_dir, ) @@ -134,8 +145,8 @@ def start(repo_url, report): table.add_row("Repository URL", repository_manager.repo_url) table.add_row("Branch Name", repository_manager.get_branch()) table.add_row("Dockerfile Path", repository_manager.dockerfile_path) - table.add_row("Poetry Version", repository_manager.poetry_version) - table.add_row("Docker Image", repository_manager.docker_image) + table.add_row("Poetry Version", poetry_version) + table.add_row("Docker Image", docker_image) console.print(table) if report: diff --git a/src/managers/repository.py b/src/managers/repository.py index d42fe30..f9747ee 100644 --- a/src/managers/repository.py +++ b/src/managers/repository.py @@ -95,6 +95,8 @@ def parse_docker_image(self): pattern = r"^FROM.*?(python:?\d*\.*\d*\.*?).*" # result would be python:3.9 + python_image = None + for line in content: if line.startswith("#"): @@ -104,8 +106,11 @@ def parse_docker_image(self): if match: image = match.group(1) - self.docker_image = image + python_image = image break + + self.docker_image = python_image + def parse_poetry_version(self): with open(self.dockerfile_path) as f: @@ -115,6 +120,8 @@ def parse_poetry_version(self): pattern = r"^ARG.*?POETRY_VERSION=(.*)" # result would be 1.4.2 + poetry_version = None + for line in content: if line.startswith("#"): @@ -124,4 +131,6 @@ def parse_poetry_version(self): if match: version = match.group(1) - self.poetry_version = version + poetry_version = version + + self.poetry_version = poetry_version diff --git a/src/parsers/docker.py b/src/parsers/docker.py index c88d210..4e43870 100644 --- a/src/parsers/docker.py +++ b/src/parsers/docker.py @@ -47,10 +47,13 @@ def get_poetry_version(self): pattern = r"^ARG POETRY_VERSION=(.*?)" # result would be ARG POETRY_VERSION=1.4.2 + poetry_version = None + for line in content: if line.startswith("#"): continue match = re.search(pattern, line) if match: poetry_version = line.split("=")[1].strip() - return poetry_version + + return poetry_version diff --git a/tests/conftest.py b/tests/conftest.py index 9c16ed3..adfc9a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,14 @@ def typical_dockerfile_content(): with open(base_dir / "tests/test_files/Dockerfile", "r") as f: return f.read() + + +@pytest.fixture +def unexpected_dockerfile_content(): + """Return the content of a typical Dockerfile for testing""" + + with open(base_dir / "tests/test_files/DockerfileNoPoetry", "r") as f: + return f.read() @pytest.fixture @@ -26,6 +34,17 @@ def dockerfile_fixture(typical_dockerfile_content, tmp_path): return dockerfile +@pytest.fixture +def dockerfile_fixture_no_poetry(unexpected_dockerfile_content, tmp_path): + """Create a dockerfile with content for testing""" + + dockerfile = tmp_path / "Dockerfile" + lines = unexpected_dockerfile_content.split("\n") + dockerfile.write_text("\n".join(lines)) + + return dockerfile + + @pytest.fixture def poetry_lock_content(): """Return the content of a typical poetry.lock for testing""" diff --git a/tests/test_files/DockerfileNoPoetry b/tests/test_files/DockerfileNoPoetry new file mode 100644 index 0000000..b77fe46 --- /dev/null +++ b/tests/test_files/DockerfileNoPoetry @@ -0,0 +1,103 @@ +# +# A typical Dockerfile for a Django app using Poetry for dependency management +# This image is never run it only a template for testing purposes +# The only parts needed from this are the python image and poetry version +# + +FROM node:18-slim as frontend + +ENV NODE_OPTIONS=--openssl-legacy-provider + +ARG CI=true + +COPY package.json package-lock.json tsconfig.json webpack.config.js tailwind.config.js ./ +RUN npm ci --no-optional --no-audit --progress=false + + +COPY ./appname/ ./appname/ +RUN npm run build:prod + + +# This is the production image. It is optimized for runtime performance and +FROM python:3.11-slim-bullseye as production + +ARG POETRY_INSTALL_ARGS="--no-dev" + +ENV VIRTUAL_ENV=/venv + +RUN useradd newuser --create-home && mkdir /app $VIRTUAL_ENV && chown -R newuser /app $VIRTUAL_ENV + +WORKDIR /app + +# ENV PATH=$VIRTUAL_ENV/bin:$PATH \ +# POETRY_INSTALL_ARGS=${POETRY_INSTALL_ARGS} \ +# PYTHONUNBUFFERED=1 \ +# DJANGO_SETTINGS_MODULE=appname.settings.production \ +# PORT=8000 \ +# WEB_CONCURRENCY=3 \ +# GUNICORN_CMD_ARGS="-c gunicorn-conf.py --max-requests 1200 --max-requests-jitter 50 --access-logfile - --timeout 25" + +# ARG BUILD_ENV +# ENV BUILD_ENV=${BUILD_ENV} + +EXPOSE 8000 + +# RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \ +# build-essential \ +# libpq-dev \ +# curl \ +# git \ +# && apt-get autoremove && rm -rf /var/lib/apt/lists/* + + +# RUN apt-get update --yes --quiet && apt-get install -y binutils libproj-dev gdal-bin + +# RUN pip install --no-cache poetry==${POETRY_VERSION} + +USER newuser + +# Install your app's Python requirements. +# RUN python -m venv $VIRTUAL_ENV +# COPY --chown=sueryder pyproject.toml poetry.lock ./ +# RUN pip install --no-cache --upgrade pip && poetry install ${POETRY_INSTALL_ARGS} --no-root --extras gunicorn && rm -rf $HOME/.cache + +# COPY --chown=sueryder --from=frontend ./sueryder/static_compiled ./sueryder/static_compiled + +# Copy application code. +COPY --chown=newuser . . + +# Run poetry install again to install our project (so the the sueryder package is always importable) +# RUN poetry install ${POETRY_INSTALL_ARGS} + +# Collect static. This command will move static files from application +# directories and "static_compiled" folder to the main static directory that +# will be served by the WSGI server. +# RUN SECRET_KEY=none python manage.py collectstatic --noinput --clear + +# Load shortcuts +# COPY ./docker/bashrc.sh /home/sueryder/.bashrc + +# Run the WSGI server. It reads GUNICORN_CMD_ARGS, PORT and WEB_CONCURRENCY +# environment variable hence we don't specify a lot options below. +CMD gunicorn sueryder.wsgi:application + + + +FROM production as dev + +USER root + +RUN apt-get update --yes --quiet && apt-get install -y postgresql-client + +USER newuser + +ENV NODE_OPTIONS=--openssl-legacy-provider + +ARG NVM_VERSION=0.39.5 +COPY --chown=sueryder .nvmrc ./ +RUN curl https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash \ + && bash --login -c "nvm install --no-progress && nvm alias default $(nvm run --silent --version)" + +COPY --chown=newuser --from=frontend ./node_modules ./node_modules + +CMD tail -f /dev/null diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 51fe3af..822afbb 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -20,6 +20,15 @@ def test_match_docker_poetry_version(dockerfile_fixture): assert poetry_version == "1.3.2" +def test_match_docker_poetry_not_found(dockerfile_fixture_no_poetry): + """Test that the parser returns None if poetry is not found""" + + parser = DockerFileParser(dockerfile_fixture_no_poetry) + poetry_version = parser.get_poetry_version() + + assert poetry_version is None + + def test_docker_parser_file_error(): """Test that the parser can handle a file not found error"""