Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: allow docker image use for non-root users #122

Merged
merged 3 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 19 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@ ARG PKG_NAME
ENV APP_PATH="/app"
ENV POETRY_VENV="${APP_PATH}/.venv"
ENV POETRY_PATH="${POETRY_VENV}/bin/poetry"
ENV PHYLUM_VENV="/opt/venv"
ENV PHYLUM_VENV_PIP="${PHYLUM_VENV}/bin/pip"
ENV PIP_NO_COMPILE=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR ${APP_PATH}

RUN pip install --no-cache-dir --upgrade pip setuptools
RUN set -eux; \
python -m venv ${PHYLUM_VENV}; \
${PHYLUM_VENV_PIP} install --no-cache-dir --upgrade pip setuptools
RUN set -eux; \
python -m venv ${POETRY_VENV}; \
${POETRY_VENV}/bin/pip install --no-cache-dir --upgrade pip setuptools; \
Expand All @@ -81,19 +85,19 @@ RUN set -eux; \
COPY pyproject.toml poetry.lock ./
RUN ${POETRY_PATH} export --without-hashes --format requirements.txt --output requirements.txt

# Cache the pip installed dependencies
# Cache the pip installed dependencies for faster builds when iterating locally.
# NOTE: This `--mount` feature requires BUILDKIT to be used
RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
set -eux; \
pip cache info; \
pip cache list; \
pip install --user -r requirements.txt
${PHYLUM_VENV_PIP} cache info; \
${PHYLUM_VENV_PIP} cache list; \
${PHYLUM_VENV_PIP} install -r requirements.txt
COPY "${PKG_SRC:-.}" .
RUN pip install --user --no-cache-dir ${PKG_NAME:-.}
RUN find /root/.local -type f -name '*.pyc' -delete
RUN ${PHYLUM_VENV_PIP} install --no-cache-dir ${PKG_NAME:-.}
RUN find ${PHYLUM_VENV} -type f -name '*.pyc' -delete

# Place in a directory included in the final layer and also known to be part of the $PATH
COPY entrypoint.sh /root/.local/bin/
COPY entrypoint.sh ${PHYLUM_VENV}/bin/

FROM python:3.10-slim-bullseye

Expand All @@ -105,19 +109,19 @@ ARG CLI_VER
LABEL maintainer="Phylum, Inc. <engineering@phylum.io>"
LABEL org.opencontainers.image.source="https://github.com/phylum-dev/phylum-ci"

# Copy only Python packages to limit the image size
COPY --from=builder /root/.local /root/.local
ENV PHYLUM_VENV="/opt/venv"
ENV PATH=${PHYLUM_VENV}/bin:$PATH
ENV PYTHONDONTWRITEBYTECODE=1

ENV PATH=/root/.local/bin:$PATH \
PYTHONPATH=/root/.local/lib/python3.10/site-packages \
PYTHONDONTWRITEBYTECODE=1
# Copy only Python packages to limit the image size
COPY --from=builder ${PHYLUM_VENV} ${PHYLUM_VENV}

RUN set -eux; \
apt-get update; \
apt-get upgrade --yes; \
apt-get install --yes --no-install-recommends git; \
chmod +x /root/.local/bin/entrypoint.sh; \
phylum-init --phylum-release ${CLI_VER:-latest}; \
chmod +x ${PHYLUM_VENV}/bin/entrypoint.sh; \
phylum-init --phylum-release ${CLI_VER:-latest} --global-install; \
apt-get purge --yes --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*; \
find / -type f -name '*.pyc' -delete
Expand Down
18 changes: 8 additions & 10 deletions src/phylum/ci/ci_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
PROJECT_THRESHOLD_OPTIONS,
SUCCESS_COMMENT,
)
from phylum.constants import SUPPORTED_LOCKFILES, TOKEN_ENVVAR_NAME
from phylum.constants import MIN_CLI_VER_INSTALLED, SUPPORTED_LOCKFILES, TOKEN_ENVVAR_NAME
from phylum.init.cli import get_phylum_bin_path
from phylum.init.cli import main as phylum_init
from ruamel.yaml import YAML
Expand Down Expand Up @@ -155,7 +155,7 @@ def _check_prerequisites(self) -> None:

The current pre-requisites for *all* CI environments/platforms are:
* A `.phylum_project` file exists at the working directory
* Phylum CLI v3.3.0+, to make use of the `parse` command
* A Phylum CLI version with the `parse` command
* Have `git` installed and available for use on the PATH
"""
print(" [+] Confirming pre-requisites ...")
Expand All @@ -165,10 +165,8 @@ def _check_prerequisites(self) -> None:
else:
raise SystemExit(" [!] The `.phylum_project` file was not found at the current working directory")

# The `parse` command was available in the pre-releases, but it makes the
# error message cleaner to only mention the release version.
if Version(self.args.version) < Version("v3.3.0-rc1"):
raise SystemExit(" [!] The CLI version must be at least v3.3.0")
if Version(self.args.version) < Version(MIN_CLI_VER_INSTALLED):
raise SystemExit(f" [!] The CLI version must be at least {MIN_CLI_VER_INSTALLED}")

if shutil.which("git"):
print(" [+] `git` binary found on the PATH")
Expand Down Expand Up @@ -283,7 +281,7 @@ def init_cli(self) -> None:
"--phylum-token", self.args.token,
]
# fmt: on
cli_path, cli_version = get_phylum_bin_path(version=specified_version)
cli_path, cli_version = get_phylum_bin_path()
if cli_path is None:
print(f" [+] Existing Phylum CLI instance not found. Installing version `{specified_version}` ...")
phylum_init(install_args)
Expand All @@ -296,11 +294,11 @@ def init_cli(self) -> None:
phylum_init(install_args)
else:
print(" [+] Attempting to use existing version ...")
if Version(str(cli_version)) < Version("v3.2.0"):
raise SystemExit(" [!] The existing CLI version must be greater than v3.2.0")
if Version(str(cli_version)) < Version(MIN_CLI_VER_INSTALLED):
raise SystemExit(f" [!] The existing CLI version must be at least {MIN_CLI_VER_INSTALLED}")
print(" [+] Version checks succeeded. Using existing version.")

cli_path, cli_version = get_phylum_bin_path(version=specified_version)
cli_path, cli_version = get_phylum_bin_path()
print(f" [+] Using Phylum CLI instance: {cli_version} at {str(cli_path)}")

self._cli_path = cli_path
Expand Down
7 changes: 6 additions & 1 deletion src/phylum/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Provide constants for use throughout the package."""

# This is the minimum CLI version supported for new installs.
# Linux platform support in the CLI was changed from `unknown-linux-musl` to `unknown-linux-gnu` starting with
# v3.8.0-rc2, changing the artifact names available to download and install in a non-backwards compatible manner.
MIN_SUPPORTED_CLI_VERSION = "v3.8.0-rc2"
MIN_CLI_VER_FOR_INSTALL = "v3.8.0-rc2"

# This is the minimum CLI version supported for existing installs.
# The `parse` command was added to the CLI in v3.3.0-rc1 and is relied upon to normalize packages in lockfiles.
MIN_CLI_VER_INSTALLED = "v3.3.0-rc1"

# Keys are lowercase machine hardware names as returned from `uname -m`.
# Values are the mapped rustc architecture.
Expand Down
80 changes: 38 additions & 42 deletions src/phylum/init/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from packaging.version import InvalidVersion, Version
from phylum import __version__
from phylum.constants import (
MIN_SUPPORTED_CLI_VERSION,
MIN_CLI_VER_FOR_INSTALL,
REQ_TIMEOUT,
SUPPORTED_ARCHES,
SUPPORTED_PLATFORMS,
Expand All @@ -28,39 +28,22 @@
from ruamel.yaml import YAML


def use_legacy_paths(version):
"""Predicate to specify whether legacy paths should be used for a given version.

The Phylum config and binary paths changed following the v2.2.0 release, to adhere to the XDG Base Directory Spec.
Reference: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
"""
return Version(canonicalize_version(version)) <= Version("v2.2.0")


def get_phylum_settings_path(version):
"""Get the Phylum settings path based on a provided version."""
def get_phylum_settings_path():
"""Get the Phylum settings path and return it."""
home_dir = pathlib.Path.home()
version = version_check(version)

config_home_path = os.getenv("XDG_CONFIG_HOME")
if not config_home_path:
config_home_path = home_dir / ".config"

phylum_config_path = pathlib.Path(config_home_path) / "phylum" / "settings.yaml"
if use_legacy_paths(version):
phylum_config_path = home_dir / ".phylum" / "settings.yaml"

return phylum_config_path


def get_expected_phylum_bin_path(version):
"""Get the expected path to the Phylum CLI binary based on a provided version."""
home_dir = pathlib.Path.home()
version = version_check(version)

phylum_bin_path = home_dir / ".local" / "bin" / "phylum"
if use_legacy_paths(version):
phylum_bin_path = home_dir / ".phylum" / "phylum"
def get_expected_phylum_bin_path():
"""Get the expected path to the Phylum CLI binary and return it."""
phylum_bin_path = pathlib.Path.home() / ".local" / "bin" / "phylum"

return phylum_bin_path

Expand All @@ -79,19 +62,15 @@ def get_phylum_cli_version(cli_path: Path) -> str:
return version


def get_phylum_bin_path(version: str = None) -> Tuple[Optional[Path], Optional[str]]:
"""Get the current path and corresponding version to the Phylum CLI binary and return them.

Provide a CLI version as a fallback method for looking on an explicit path,
based on the expected path for that version.
"""
def get_phylum_bin_path() -> Tuple[Optional[Path], Optional[str]]:
"""Get the current path and corresponding version to the Phylum CLI binary and return them."""
# Look for `phylum` on the PATH first
which_cli_path = shutil.which("phylum")

if which_cli_path is None and version is not None:
if which_cli_path is None:
# Maybe `phylum` is installed already but not on the PATH or maybe the PATH has not been updated in this
# context. Look in the specific location expected by the provided version.
expected_cli_path = get_expected_phylum_bin_path(version)
# context. Look in the specific expected location.
expected_cli_path = get_expected_phylum_bin_path()
which_cli_path = shutil.which("phylum", path=expected_cli_path)

if which_cli_path is None:
Expand Down Expand Up @@ -142,7 +121,7 @@ def is_supported_version(version: str) -> bool:
"""Predicate for determining if a given version is supported."""
try:
provided_version = Version(canonicalize_version(version))
min_supported_version = Version(MIN_SUPPORTED_CLI_VERSION)
min_supported_version = Version(MIN_CLI_VER_FOR_INSTALL)
except InvalidVersion as err:
raise ValueError("An invalid version was provided") from err

Expand Down Expand Up @@ -254,7 +233,7 @@ def is_token_set(phylum_settings_path, token=None):

def process_token_option(args):
"""Process the token option as parsed from the arguments."""
phylum_settings_path = get_phylum_settings_path(args.version)
phylum_settings_path = get_phylum_settings_path()

# The token option takes precedence over the Phylum API key environment variable.
token = os.getenv(TOKEN_ENVVAR_NAME)
Expand All @@ -279,13 +258,13 @@ def process_token_option(args):
print(" [!] Existing token not found. Use `phylum auth login` or `phylum auth register` command to set it.")

if token and not is_token_set(phylum_settings_path, token=token):
setup_token(token, args)
setup_token(token)


def setup_token(token, args):
"""Setup the CLI credentials with a provided token and path to phylum binary."""
phylum_bin_path = get_expected_phylum_bin_path(args.version)
phylum_settings_path = get_phylum_settings_path(args.version)
def setup_token(token):
"""Setup the CLI credentials with a provided token."""
phylum_bin_path = get_expected_phylum_bin_path()
phylum_settings_path = get_phylum_settings_path()

# The phylum CLI settings.yaml file won't exist upon initial install
# but running a command will trigger the CLI to generate it
Expand All @@ -306,7 +285,7 @@ def setup_token(token, args):


def get_args(args=None):
"""Get the arguments from the command line and return them."""
"""Get the arguments from the command line or input parameter, parse and return them."""
parser = argparse.ArgumentParser(
prog=SCRIPT_NAME,
description="Fetch and install the Phylum CLI",
Expand Down Expand Up @@ -334,6 +313,14 @@ def get_args(args=None):
default=get_target_triple(),
help="The target platform type where the CLI will be installed.",
)
parser.add_argument(
"-g",
"--global-install",
action="store_true",
# Specify this flag to install the Phylum CLI to a globally accessible directory.
# NOTE: This option is hidden from help output b/c it is meant to be used internally, for Docker image creation.
help=argparse.SUPPRESS,
)
parser.add_argument(
"-k",
"--phylum-token",
Expand Down Expand Up @@ -386,7 +373,6 @@ def main(args=None):
minisig_name = f"{archive_name}.minisig"
archive_url = get_archive_url(tag_name, archive_name)
minisig_url = f"{archive_url}.minisig"
phylum_bin_path = get_expected_phylum_bin_path(tag_name)

with tempfile.TemporaryDirectory() as temp_dir:
temp_dir_path = pathlib.Path(temp_dir)
Expand All @@ -404,12 +390,22 @@ def main(args=None):
extracted_dir = temp_dir_path / f"phylum-{target_triple}"
zip_file.extractall(path=temp_dir)

cmd = "sh install.sh".split()
# This may look wrong, but a decision was made to manually handle global installs
# in the places it is required instead of updating the CLI's install script.
# Reference: https://github.com/phylum-dev/cli/pull/671
if args.global_install:
# Current assumptions for this method:
# * the /usr/local/bin directory exists, has proper permissions, is on the PATH for all users
# * the install is on a system with glibc
cmd = "install -m 0755 phylum /usr/local/bin/phylum".split()
else:
cmd = "sh install.sh".split()
subprocess.run(cmd, check=True, cwd=extracted_dir)

process_token_option(args)

# Check to ensure everything is working
phylum_bin_path, _ = get_phylum_bin_path()
cmd = f"{phylum_bin_path} --help".split()
subprocess.run(cmd, check=True)

Expand Down