From 138b599ba6584cbf97790e443df734be88ebbb86 Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Wed, 30 Apr 2025 18:28:45 +0200 Subject: [PATCH 1/6] fix custom image integration test --- .github/workflows/kubernetes-deploy.yaml | 1 - tests/basic/06_function.py | 13 +++---------- tests/docker/test_docker.py | 8 ++------ 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/kubernetes-deploy.yaml b/.github/workflows/kubernetes-deploy.yaml index bf8468c8f..793619048 100644 --- a/.github/workflows/kubernetes-deploy.yaml +++ b/.github/workflows/kubernetes-deploy.yaml @@ -71,7 +71,6 @@ jobs: echo $GATEWAY_HOST # basic tests cd /home/runner/work/qiskit-serverless/qiskit-serverless/tests/basic - rm 06_function.py for f in *.py; do echo "TEST: $f" && python "$f"; done # experimental tests cd /home/runner/work/qiskit-serverless/qiskit-serverless/tests/experimental diff --git a/tests/basic/06_function.py b/tests/basic/06_function.py index 1481f2d07..283c28c54 100644 --- a/tests/basic/06_function.py +++ b/tests/basic/06_function.py @@ -21,19 +21,12 @@ provider=os.environ.get("PROVIDER_ID", "mockprovider"), description=help, ) -serverless.upload(function_with_custom_image) +runnable_function = serverless.upload(function_with_custom_image) -my_functions = serverless.list() -for function in my_functions: - print("Name: " + function.title) - print(function.description) - print() - -my_function = serverless.get("custom-image-function") -job = my_function.run(message="Argument for the custum function") +job = runnable_function.run(message="Argument for the custum function") print(job.result()) print(job.logs()) -jobs = my_function.jobs() +jobs = runnable_function.jobs() print(jobs) diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py index 2e9e2dec1..d786ec10b 100644 --- a/tests/docker/test_docker.py +++ b/tests/docker/test_docker.py @@ -143,11 +143,7 @@ def test_multiple_runs(self, base_client: BaseClient): assert isinstance(retrieved_job1.logs(), str) assert isinstance(retrieved_job2.logs(), str) - @mark.skip( - reason="Images are not working in tests jet and " - + "LocalClient does not manage image instead of working_dir+entrypoint" - ) - def test_error(self, base_client: BaseClient): + def test_custom_image(self, serverless_client: BaseClient): """Integration test to force an error.""" description = """ @@ -166,7 +162,7 @@ def test_error(self, base_client: BaseClient): description=description, ) - runnable_function = base_client.upload(function_with_custom_image) + runnable_function = serverless_client.upload(function_with_custom_image) job = runnable_function.run(message="Argument for the custum function") From 15bde8d6999bc89cf29de4bc5e422049984beeb5 Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Tue, 6 May 2025 12:57:15 +0200 Subject: [PATCH 2/6] add new yaml to test running custom-images Revert "fix custom image integration test" This reverts commit 138b599ba6584cbf97790e443df734be88ebbb86. --- .github/workflows/kubernetes-deploy.yaml | 1 + tests/docker/conftest.py | 15 ++- tests/docker/docker-compose-test.yaml | 115 +++++++++++++++++++++++ tests/docker/function/Sample-Docker | 16 ++++ tests/docker/function/runner.py | 36 +++++++ tests/docker/test_docker.py | 7 +- 6 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 tests/docker/docker-compose-test.yaml create mode 100644 tests/docker/function/Sample-Docker create mode 100755 tests/docker/function/runner.py diff --git a/.github/workflows/kubernetes-deploy.yaml b/.github/workflows/kubernetes-deploy.yaml index 793619048..bf8468c8f 100644 --- a/.github/workflows/kubernetes-deploy.yaml +++ b/.github/workflows/kubernetes-deploy.yaml @@ -71,6 +71,7 @@ jobs: echo $GATEWAY_HOST # basic tests cd /home/runner/work/qiskit-serverless/qiskit-serverless/tests/basic + rm 06_function.py for f in *.py; do echo "TEST: $f" && python "$f"; done # experimental tests cd /home/runner/work/qiskit-serverless/qiskit-serverless/tests/experimental diff --git a/tests/docker/conftest.py b/tests/docker/conftest.py index fda2e3492..4ea087480 100644 --- a/tests/docker/conftest.py +++ b/tests/docker/conftest.py @@ -29,12 +29,12 @@ def local_client(): return LocalClient() -def set_up_serverless_client(): +def set_up_serverless_client(compose_file_name="../../../docker-compose-dev.yaml"): """Auxiliar fixture function to create a serverless client""" compose = DockerCompose( resources_path, - compose_file_name="../../../docker-compose-dev.yaml", - pull=True, + compose_file_name=compose_file_name, + pull=False, ) compose.start() @@ -65,3 +65,12 @@ def serverless_client(): yield serverless compose.stop() + +@fixture(scope="module") +def serverless_custom_image_yaml_client(): + """Fixture for testing files with serverless client.""" + [compose, serverless] = set_up_serverless_client(compose_file_name="../docker-compose-test.yaml") + + yield serverless + + compose.stop() diff --git a/tests/docker/docker-compose-test.yaml b/tests/docker/docker-compose-test.yaml new file mode 100644 index 000000000..b642b4093 --- /dev/null +++ b/tests/docker/docker-compose-test.yaml @@ -0,0 +1,115 @@ +# compose config for running images based on local files +services: + ray-head: + user: "0" + container_name: ray-head + image: test_function + build: + context: ./ + dockerfile: function/Sample-Docker + entrypoint: [ + "env", "RAY_LOG_TO_STDERR=1", "ray", "start", "--head", "--port=6379", + "--dashboard-host=0.0.0.0", "--block" + ] + ports: + - 8265:8265 + volumes: + - host-shm:/dev/shm + networks: + - safe-tier + postgres: + image: postgres + environment: + POSTGRES_DB: serverlessdb + POSTGRES_USER: serverlessuser + POSTGRES_PASSWORD: serverlesspassword + networks: + - safe-tier + ports: + - 5432:5432 + restart: + always + gateway: + container_name: gateway + build: + context: ../../ + dockerfile: gateway/Dockerfile + command: gunicorn main.wsgi:application --bind 0.0.0.0:8000 --workers=1 --threads=1 --max-requests=1200 --max-requests-jitter=50 --timeout=25 + ports: + - 8000:8000 + user: "root" # we use the root user here so the docker-compose watch can sync files into the container + environment: + - DEBUG=1 + - RAY_HOST=http://ray-head:8265 + - DJANGO_SUPERUSER_USERNAME=admin + - DJANGO_SUPERUSER_PASSWORD=123 + - DJANGO_SUPERUSER_EMAIL=admin@noemail.com + - SITE_HOST=http://gateway:8000 + - SETTINGS_AUTH_MECHANISM=mock_token + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_NAME=serverlessdb + - DATABASE_USER=serverlessuser + - DATABASE_PASSWORD=serverlesspassword + networks: + - safe-tier + volumes: + - program-artifacts:/usr/src/app/media/ + depends_on: + - postgres + scheduler: + container_name: scheduler + build: + context: ../../ + dockerfile: gateway/Dockerfile + entrypoint: "./scripts/scheduler.sh" + environment: + - DEBUG=1 + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_NAME=serverlessdb + - DATABASE_USER=serverlessuser + - DATABASE_PASSWORD=serverlesspassword + - RAY_CLUSTER_MODE_LOCAL_HOST=http://ray-head:8265 + - RAY_CLUSTER_MODE_LOCAL=1 + - SETTINGS_AUTH_MECHANISM=mock_token + networks: + - safe-tier + volumes: + - program-artifacts:/usr/src/app/media/ + depends_on: + - postgres + prometheus: + image: prom/prometheus:v2.44.0 + profiles: [ "full" ] + ports: + - 9000:9090 + loki: + image: grafana/loki:2.8.4 + profiles: [ "full" ] + ports: + - 3100:3100 + command: -config.file=/etc/loki/local-config.yaml + networks: + - safe-tier + promtail: + image: grafana/promtail:2.8.4 + profiles: [ "full" ] + volumes: + - host-log:/var/log + command: -config.file=/etc/promtail/config.yml + networks: + - safe-tier + grafana: + image: grafana/grafana:latest + profiles: [ "full" ] + ports: + - 3000:3000 + networks: + - safe-tier +networks: + safe-tier: +volumes: + program-artifacts: + host-shm: + host-log: diff --git a/tests/docker/function/Sample-Docker b/tests/docker/function/Sample-Docker new file mode 100644 index 000000000..5472aca01 --- /dev/null +++ b/tests/docker/function/Sample-Docker @@ -0,0 +1,16 @@ +FROM icr.io/quantum-public/qiskit-serverless/ray-node:latest + +# install all necessary dependencies for your custom image + +# copy our function implementation in `/runner.py` of the docker image +USER 0 +RUN pip install pendulum +RUN mkdir /runner +WORKDIR /runner +COPY function/runner.py . +WORKDIR / + +USER 1000 + + + diff --git a/tests/docker/function/runner.py b/tests/docker/function/runner.py new file mode 100755 index 000000000..dee10b8f2 --- /dev/null +++ b/tests/docker/function/runner.py @@ -0,0 +1,36 @@ +from qiskit import QuantumCircuit +from qiskit.primitives import StatevectorSampler as Sampler + + +def custom_function(arguments): + import pendulum # type: ignore + + dt_toronto = pendulum.datetime(2012, 1, 1, tz="America/Toronto") + dt_vancouver = pendulum.datetime(2012, 1, 1, tz="America/Vancouver") + + diff = dt_vancouver.diff(dt_toronto).in_hours() + + print(diff) + + # all print statement will be available in job logs + print("Running function...") + message = arguments.get("message") + print(message) + + # creating circuit + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + + # running Sampler primitive + sampler = Sampler() + quasi_dists = sampler.run([(circuit)]).result()[0].data.meas.get_counts() + + print("Completed running pattern.") + return quasi_dists + + +class Runner: + def run(self, arguments: dict) -> dict: + return custom_function(arguments) diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py index d786ec10b..3957fa937 100644 --- a/tests/docker/test_docker.py +++ b/tests/docker/test_docker.py @@ -143,7 +143,7 @@ def test_multiple_runs(self, base_client: BaseClient): assert isinstance(retrieved_job1.logs(), str) assert isinstance(retrieved_job2.logs(), str) - def test_custom_image(self, serverless_client: BaseClient): + def test_custom_image(self, serverless_custom_image_yaml_client: BaseClient): """Integration test to force an error.""" description = """ @@ -162,12 +162,11 @@ def test_custom_image(self, serverless_client: BaseClient): description=description, ) - runnable_function = serverless_client.upload(function_with_custom_image) + runnable_function = serverless_custom_image_yaml_client.upload(function_with_custom_image) job = runnable_function.run(message="Argument for the custum function") - with raises(QiskitServerlessException): - job.result() + job.result() def test_update_sub_status(self, serverless_client: ServerlessClient): """Integration test for run functions multiple times.""" From 6c99740d73672aa8f1382db8e10771d64399482b Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Tue, 6 May 2025 15:31:38 +0200 Subject: [PATCH 3/6] lint --- tests/docker/conftest.py | 5 ++++- tests/docker/function/runner.py | 2 +- tests/docker/test_docker.py | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/docker/conftest.py b/tests/docker/conftest.py index 4ea087480..c0a6a9a33 100644 --- a/tests/docker/conftest.py +++ b/tests/docker/conftest.py @@ -66,10 +66,13 @@ def serverless_client(): compose.stop() + @fixture(scope="module") def serverless_custom_image_yaml_client(): """Fixture for testing files with serverless client.""" - [compose, serverless] = set_up_serverless_client(compose_file_name="../docker-compose-test.yaml") + [compose, serverless] = set_up_serverless_client( + compose_file_name="../docker-compose-test.yaml" + ) yield serverless diff --git a/tests/docker/function/runner.py b/tests/docker/function/runner.py index dee10b8f2..f87a46d6f 100755 --- a/tests/docker/function/runner.py +++ b/tests/docker/function/runner.py @@ -3,7 +3,7 @@ def custom_function(arguments): - import pendulum # type: ignore + import pendulum # type: ignore dt_toronto = pendulum.datetime(2012, 1, 1, tz="America/Toronto") dt_vancouver = pendulum.datetime(2012, 1, 1, tz="America/Vancouver") diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py index 3957fa937..78393d7e8 100644 --- a/tests/docker/test_docker.py +++ b/tests/docker/test_docker.py @@ -3,7 +3,7 @@ import os from time import sleep -from pytest import raises, mark +from pytest import mark from qiskit import QuantumCircuit from qiskit.circuit.random import random_circuit @@ -12,7 +12,6 @@ QiskitFunction, BaseClient, ServerlessClient, - QiskitServerlessException, ) @@ -162,7 +161,9 @@ def test_custom_image(self, serverless_custom_image_yaml_client: BaseClient): description=description, ) - runnable_function = serverless_custom_image_yaml_client.upload(function_with_custom_image) + runnable_function = serverless_custom_image_yaml_client.upload( + function_with_custom_image + ) job = runnable_function.run(message="Argument for the custum function") From 30b51159d1fa10936372db6a82b49df76e528f63 Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Wed, 7 May 2025 11:05:06 +0200 Subject: [PATCH 4/6] updated tests --- tests/docker/conftest.py | 26 +++++++++++++----- tests/docker/docker-compose-test.yaml | 18 ++++++------- tests/docker/function/Sample-Docker | 2 +- tests/docker/function/runner.py | 38 ++++----------------------- tests/docker/test_docker.py | 9 ++++++- tests/tox.ini | 2 +- 6 files changed, 44 insertions(+), 51 deletions(-) diff --git a/tests/docker/conftest.py b/tests/docker/conftest.py index c0a6a9a33..da7847ddd 100644 --- a/tests/docker/conftest.py +++ b/tests/docker/conftest.py @@ -1,6 +1,7 @@ # pylint: disable=import-error, invalid-name """ Fixtures for tests """ import os +from subprocess import CalledProcessError from pytest import fixture from testcontainers.compose import DockerCompose @@ -29,22 +30,33 @@ def local_client(): return LocalClient() -def set_up_serverless_client(compose_file_name="../../../docker-compose-dev.yaml"): +def set_up_serverless_client(compose_file_name="../../../docker-compose-dev.yaml", backoffice_port=8000): """Auxiliar fixture function to create a serverless client""" compose = DockerCompose( resources_path, compose_file_name=compose_file_name, - pull=False, + pull=False ) - compose.start() - - connection_url = "http://localhost:8000" + try: + compose.start() + except CalledProcessError as error: + print("COMPOSE START ERROR") + print("STDOUT:") + print(error.stdout) + print("STDERR:") + print(error.stderr) + raise error + + print(f"Docker started... (compose file: ${compose_file_name})") + connection_url = f"http://localhost:{backoffice_port}" compose.wait_for(f"{connection_url}/backoffice") + print(f"backoffice ready... (port: ${backoffice_port})") serverless = ServerlessClient( token=os.environ.get("GATEWAY_TOKEN", "awesome_token"), host=os.environ.get("GATEWAY_HOST", connection_url), ) + print("ServerlessClient verified...") # Initialize serverless folder for current user function = QiskitFunction( @@ -53,6 +65,7 @@ def set_up_serverless_client(compose_file_name="../../../docker-compose-dev.yaml working_dir=resources_path, ) serverless.upload(function) + print("ServerlessClient ready...") return [compose, serverless] @@ -71,7 +84,8 @@ def serverless_client(): def serverless_custom_image_yaml_client(): """Fixture for testing files with serverless client.""" [compose, serverless] = set_up_serverless_client( - compose_file_name="../docker-compose-test.yaml" + compose_file_name="../docker-compose-test.yaml", + backoffice_port=8001 ) yield serverless diff --git a/tests/docker/docker-compose-test.yaml b/tests/docker/docker-compose-test.yaml index b642b4093..4e2c2bebf 100644 --- a/tests/docker/docker-compose-test.yaml +++ b/tests/docker/docker-compose-test.yaml @@ -2,7 +2,7 @@ services: ray-head: user: "0" - container_name: ray-head + container_name: ray-head-test image: test_function build: context: ./ @@ -12,7 +12,7 @@ services: "--dashboard-host=0.0.0.0", "--block" ] ports: - - 8265:8265 + - 8266:8266 volumes: - host-shm:/dev/shm networks: @@ -26,17 +26,17 @@ services: networks: - safe-tier ports: - - 5432:5432 + - 5433:5433 restart: always gateway: - container_name: gateway + container_name: gateway-test build: context: ../../ dockerfile: gateway/Dockerfile - command: gunicorn main.wsgi:application --bind 0.0.0.0:8000 --workers=1 --threads=1 --max-requests=1200 --max-requests-jitter=50 --timeout=25 + command: gunicorn main.wsgi:application --bind 0.0.0.0:8001 --workers=1 --threads=1 --max-requests=1200 --max-requests-jitter=50 --timeout=25 ports: - - 8000:8000 + - 8001:8001 user: "root" # we use the root user here so the docker-compose watch can sync files into the container environment: - DEBUG=1 @@ -44,7 +44,7 @@ services: - DJANGO_SUPERUSER_USERNAME=admin - DJANGO_SUPERUSER_PASSWORD=123 - DJANGO_SUPERUSER_EMAIL=admin@noemail.com - - SITE_HOST=http://gateway:8000 + - SITE_HOST=http://gateway:8001 - SETTINGS_AUTH_MECHANISM=mock_token - DATABASE_HOST=postgres - DATABASE_PORT=5432 @@ -58,7 +58,7 @@ services: depends_on: - postgres scheduler: - container_name: scheduler + container_name: scheduler-test build: context: ../../ dockerfile: gateway/Dockerfile @@ -66,7 +66,7 @@ services: environment: - DEBUG=1 - DATABASE_HOST=postgres - - DATABASE_PORT=5432 + - DATABASE_PORT=5433 - DATABASE_NAME=serverlessdb - DATABASE_USER=serverlessuser - DATABASE_PASSWORD=serverlesspassword diff --git a/tests/docker/function/Sample-Docker b/tests/docker/function/Sample-Docker index 5472aca01..30370c04c 100644 --- a/tests/docker/function/Sample-Docker +++ b/tests/docker/function/Sample-Docker @@ -1,10 +1,10 @@ FROM icr.io/quantum-public/qiskit-serverless/ray-node:latest # install all necessary dependencies for your custom image +RUN pip install pendulum # copy our function implementation in `/runner.py` of the docker image USER 0 -RUN pip install pendulum RUN mkdir /runner WORKDIR /runner COPY function/runner.py . diff --git a/tests/docker/function/runner.py b/tests/docker/function/runner.py index f87a46d6f..2aa7e4c4c 100755 --- a/tests/docker/function/runner.py +++ b/tests/docker/function/runner.py @@ -1,36 +1,8 @@ -from qiskit import QuantumCircuit -from qiskit.primitives import StatevectorSampler as Sampler +import pendulum # type: ignore +dt_toronto = pendulum.datetime(2012, 1, 1, tz="America/Toronto") +dt_vancouver = pendulum.datetime(2012, 1, 1, tz="America/Vancouver") -def custom_function(arguments): - import pendulum # type: ignore +diff = dt_vancouver.diff(dt_toronto).in_hours() - dt_toronto = pendulum.datetime(2012, 1, 1, tz="America/Toronto") - dt_vancouver = pendulum.datetime(2012, 1, 1, tz="America/Vancouver") - - diff = dt_vancouver.diff(dt_toronto).in_hours() - - print(diff) - - # all print statement will be available in job logs - print("Running function...") - message = arguments.get("message") - print(message) - - # creating circuit - circuit = QuantumCircuit(2) - circuit.h(0) - circuit.cx(0, 1) - circuit.measure_all() - - # running Sampler primitive - sampler = Sampler() - quasi_dists = sampler.run([(circuit)]).result()[0].data.meas.get_counts() - - print("Completed running pattern.") - return quasi_dists - - -class Runner: - def run(self, arguments: dict) -> dict: - return custom_function(arguments) +print(diff) diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py index 78393d7e8..2a1fec86f 100644 --- a/tests/docker/test_docker.py +++ b/tests/docker/test_docker.py @@ -161,13 +161,20 @@ def test_custom_image(self, serverless_custom_image_yaml_client: BaseClient): description=description, ) + print("Uploading function...") runnable_function = serverless_custom_image_yaml_client.upload( function_with_custom_image ) + print("Running...") job = runnable_function.run(message="Argument for the custum function") - job.result() + + print("Job:") + print(job) + + print("Result:") + print(job.result()) def test_update_sub_status(self, serverless_client: ServerlessClient): """Integration test for run functions multiple times.""" diff --git a/tests/tox.ini b/tests/tox.ini index 956032a29..3a1183beb 100644 --- a/tests/tox.ini +++ b/tests/tox.ini @@ -20,7 +20,7 @@ deps = -rrequirements-dev.txt commands = pip install ../client pip check - python -m pytest -v --order-dependencies + python -m pytest -v --order-dependencies --capture=no [testenv:lint] skip_install = true From b41d9cbec0383b372ab4cee7982c6483d0b01506 Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Wed, 7 May 2025 11:10:47 +0200 Subject: [PATCH 5/6] lint --- tests/docker/conftest.py | 13 ++++++------- tests/docker/test_docker.py | 3 +-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/docker/conftest.py b/tests/docker/conftest.py index da7847ddd..92a45ae55 100644 --- a/tests/docker/conftest.py +++ b/tests/docker/conftest.py @@ -30,15 +30,15 @@ def local_client(): return LocalClient() -def set_up_serverless_client(compose_file_name="../../../docker-compose-dev.yaml", backoffice_port=8000): +def set_up_serverless_client( + compose_file_name="../../../docker-compose-dev.yaml", backoffice_port=8000 +): """Auxiliar fixture function to create a serverless client""" compose = DockerCompose( - resources_path, - compose_file_name=compose_file_name, - pull=False + resources_path, compose_file_name=compose_file_name, pull=False ) try: - compose.start() + compose.start() except CalledProcessError as error: print("COMPOSE START ERROR") print("STDOUT:") @@ -84,8 +84,7 @@ def serverless_client(): def serverless_custom_image_yaml_client(): """Fixture for testing files with serverless client.""" [compose, serverless] = set_up_serverless_client( - compose_file_name="../docker-compose-test.yaml", - backoffice_port=8001 + compose_file_name="../docker-compose-test.yaml", backoffice_port=8001 ) yield serverless diff --git a/tests/docker/test_docker.py b/tests/docker/test_docker.py index 2a1fec86f..2af1d1238 100644 --- a/tests/docker/test_docker.py +++ b/tests/docker/test_docker.py @@ -169,10 +169,9 @@ def test_custom_image(self, serverless_custom_image_yaml_client: BaseClient): print("Running...") job = runnable_function.run(message="Argument for the custum function") - print("Job:") print(job) - + print("Result:") print(job.result()) From ec4b7bd929aa7a625635d5a1846a032c55fe2403 Mon Sep 17 00:00:00 2001 From: Gregorio Iniesta Date: Wed, 7 May 2025 11:38:25 +0200 Subject: [PATCH 6/6] change pip install in sample docker --- tests/docker/function/Sample-Docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/function/Sample-Docker b/tests/docker/function/Sample-Docker index 30370c04c..5472aca01 100644 --- a/tests/docker/function/Sample-Docker +++ b/tests/docker/function/Sample-Docker @@ -1,10 +1,10 @@ FROM icr.io/quantum-public/qiskit-serverless/ray-node:latest # install all necessary dependencies for your custom image -RUN pip install pendulum # copy our function implementation in `/runner.py` of the docker image USER 0 +RUN pip install pendulum RUN mkdir /runner WORKDIR /runner COPY function/runner.py .