diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 00000000..86d053bf --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,212 @@ +--- + +name: CI/CD + +on: + merge_group: + push: + branches: [ master ] + tags: [ 'v*' ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 6 * * *' # Daily 6AM UTC build + + +env: + PYTHON_LATEST: 3.12 + PROJECT_NAME: aiodocker + + # For re-actors/checkout-python-sdist + dists-artifact-name: python-package-distributions + + +jobs: + + build: + name: 📦 Build the distribution packages + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_LATEST }} + cache: pip + - name: Install core libraries for build + run: python -Im pip install build + - name: Build artifacts + run: python -Im build + - name: Upload built artifacts for testing + uses: actions/upload-artifact@v3 + with: + name: ${{ env.dists-artifact-name }} + path: | + dist/${{ env.PROJECT_NAME }}*.tar.gz + dist/${{ env.PROJECT_NAME }}*.whl + retention-days: 15 + + lint: + name: Linter + needs: + - build + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download all the dists + uses: actions/download-artifact@v3 + with: + name: ${{ env.dists-artifact-name }} + path: dist + - name: Setup Python ${{ env.PYTHON_LATEST }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_LATEST }} + cache: pip + cache-dependency-path: | + setup.py + requirements/lint.txt + - name: Install dependencies + uses: py-actions/py-dependency-install@v2 + with: + path: requirements/lint.txt + - uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit/ + key: pre-commit-4|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Run linters + run: | + make lint + - name: Run twine checker + run: | + pip install twine + twine check --strict dist/* + + test: + name: test + needs: + - build + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + os: [ubuntu] + registry: ['1'] + pytest-arg: [''] + include: + - python-version: '3.12' + os: windows + registry: '0' + pytest-arg: '-k test_integration' + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + setup.py + requirements/test.txt + - name: Install dependencies + uses: py-actions/py-dependency-install@v2 + with: + path: requirements/test.txt + - name: Start Docker services + if: ${{ matrix.registry == '1' }} + run: | + docker run -d --name ${{ env.PROJECT_NAME }}-test-registry -p 5000:5000 registry:2 + docker run -d -p 5001:5001 --name ${{ env.PROJECT_NAME }}-test-registry2 -v `pwd`/tests/certs:/certs -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/certs/htpasswd -e REGISTRY_HTTP_ADDR=0.0.0.0:5001 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key registry:2 + - name: Run unittests + env: + COLOR: 'yes' + DOCKER_VERSION: ${{ matrix.docker }} + run: | + python -m pytest -vv --durations=10 ${{ matrix.pytest-arg }} + - name: Upload coverage artifact + uses: neuro-inc/prepare-coverage@v21.9.1 + with: + key: unit-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.cmd }}-${{ matrix.registry }} + + check: # This job does nothing and is only used for the branch protection + name: ✅ Ensure the required checks passing + if: always() + needs: + - lint + - test + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + - name: Upload code coverage report + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + publish: # Run only on creating release for new tag + name: 📦 Publish to PyPI + needs: + - check + runs-on: ubuntu-latest + # Run only on pushing a tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for trusted publishing & sigstore + + environment: + name: pypi + url: >- + https://pypi.org/project/${{ env.PROJECT_NAME }}/${{ github.ref_name }} + + steps: + - name: Download all the dists + uses: actions/download-artifact@v3 + with: + name: ${{ env.dists-artifact-name }} + path: dist + + - name: Release + uses: aio-libs/create-release@v1.6.5 + with: + changes_file: CHANGES.rst + name: ${{ env.PROJECT_NAME }} + github_token: ${{ secrets.GITHUB_TOKEN }} + head_line: "{version}\\s+\\({date}\\)\n====+\n?" + + - name: >- + Publish 🐍📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + ./dist/${{ env.PROJECT_NAME }}*.tar.gz + ./dist/${{ env.PROJECT_NAME }}*.whl + + - name: Upload artifact signatures to GitHub Release + # Confusingly, this action also supports updating releases, not + # just creating them. This is what we want here, since we've manually + # created the release above. + uses: softprops/action-gh-release@v2 + with: + # dist/ contains the built packages, which smoketest-artifacts/ + # contains the signatures and certificates. + files: dist/** + +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index da71f16f..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - tags: [ 'v*' ] - pull_request: - branches: [ master ] - schedule: - - cron: '0 6 * * *' # Daily 6AM UTC build - - -jobs: - - lint: - name: Linter - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: pip - cache-dependency-path: | - setup.py - requirements/lint.txt - - name: Install dependencies - uses: py-actions/py-dependency-install@v2 - with: - path: requirements/lint.txt - - uses: actions/cache@v4 - with: - path: ~/.cache/pre-commit/ - key: pre-commit-4|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Generate dists - run: | - pip install build - python -m build - - name: Run linters - run: | - make lint - - name: Run twine checker - run: | - pip install twine - twine check dist/* - - test: - name: test - needs: [lint] - strategy: - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] - os: [ubuntu] - registry: ['1'] - pytest-arg: [''] - include: - - python-version: '3.12' - os: windows - registry: '0' - pytest-arg: '-k test_integration' - runs-on: ${{ matrix.os }}-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: | - setup.py - requirements/test.txt - - name: Install dependencies - uses: py-actions/py-dependency-install@v2 - with: - path: requirements/test.txt - - name: Start Docker services - if: ${{ matrix.registry == '1' }} - run: | - docker run -d --name aiodocker-test-registry -p 5000:5000 registry:2 - docker run -d -p 5001:5001 --name aiodocker-test-registry2 -v `pwd`/tests/certs:/certs -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/certs/htpasswd -e REGISTRY_HTTP_ADDR=0.0.0.0:5001 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key registry:2 - - name: Run unittests - env: - COLOR: 'yes' - DOCKER_VERSION: ${{ matrix.docker }} - run: | - python -m pytest -vv --durations=10 ${{ matrix.pytest-arg }} - - name: Upload coverage artifact - uses: neuro-inc/prepare-coverage@v21.9.1 - with: - key: unit-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.cmd }}-${{ matrix.registry }} - - check: - # All's good, upload coverage (also good for branch protection rule) - name: Check - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} - - name: Upload code coverage report - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - deploy: - name: Deploy on PyPI - needs: [lint, test] - runs-on: ubuntu-latest - # Run only on pushing a tag - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install dependencies - run: | - pip install build wheel - - name: Build wheels - run: | - python -m build - - name: Release - uses: aio-libs/create-release@v1.6.5 - with: - changes_file: CHANGES.rst - name: aiodocker - github_token: ${{ secrets.GITHUB_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} - head_line: "{version}\\s+\\({date}\\)\n====+\n?" diff --git a/CHANGES.rst b/CHANGES.rst index 7f15286f..07522c11 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,7 @@ Changes Bugfixes -------- -- Use ssl_context passsed to Docker constructor for creating underlying connection to docker engine. (#536) +- Use ssl_context passed to Docker constructor for creating underlying connection to docker engine. (#536) - Fix an error when attach/exec when container stops before close connection to it. (#608) @@ -98,7 +98,7 @@ Bugfixes Bugfixes -------- -- Improve the errror message when connection is closed by Docker Engine on TCP hijacking. (#424) +- Improve the error message when connection is closed by Docker Engine on TCP hijacking. (#424) 0.18.0 (2020-03-25) diff --git a/CHANGES/837.doc b/CHANGES/837.doc new file mode 100644 index 00000000..c06f7f0e --- /dev/null +++ b/CHANGES/837.doc @@ -0,0 +1 @@ +Update the documentation examples to use the modern `asyncio.run()` pattern and initialize `aiodocker.Docker()` instance inside async functions where there is a valid running event loop diff --git a/README.rst b/README.rst index 6f973a29..120ff2c8 100644 --- a/README.rst +++ b/README.rst @@ -10,9 +10,9 @@ AsyncIO bindings for docker.io :target: https://pypi.org/project/aiodocker/ :alt: Python Versions -.. image:: https://travis-ci.com/aio-libs/aiodocker.svg?branch=master - :target: https://travis-ci.com/aio-libs/aiodocker - :alt: Build Status +.. image:: https://github.com/aio-libs/aiodocker/actions/workflows/ci-cd.yml/badge.svg?branch=master + :target: https://github.com/aio-libs/aiodocker/actions/workflows/ci-cd.yml?query=branch%3Amaster + :alt: GitHub Actions status for the main branch .. image:: https://codecov.io/gh/aio-libs/aiodocker/branch/master/graph/badge.svg :target: https://codecov.io/gh/aio-libs/aiodocker @@ -43,38 +43,37 @@ Examples .. code-block:: python - import asyncio - import aiodocker - - async def list_things(): - docker = aiodocker.Docker() - print('== Images ==') - for image in (await docker.images.list()): - tags = image['RepoTags'][0] if image['RepoTags'] else '' - print(image['Id'], tags) - print('== Containers ==') - for container in (await docker.containers.list()): - print(f" {container._id}") - await docker.close() - - async def run_container(): - docker = aiodocker.Docker() - print('== Running a hello-world container ==') - container = await docker.containers.create_or_replace( - config={ - 'Cmd': ['/bin/ash', '-c', 'echo "hello world"'], - 'Image': 'alpine:latest', - }, - name='testing', - ) - await container.start() - logs = await container.log(stdout=True) - print(''.join(logs)) - await container.delete(force=True) - await docker.close() - - if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(list_things()) - loop.run_until_complete(run_container()) - loop.close() + import asyncio + import aiodocker + + async def list_things(docker): + print('== Images ==') + for image in (await docker.images.list()): + tags = image['RepoTags'][0] if image['RepoTags'] else '' + print(image['Id'], tags) + print('== Containers ==') + for container in (await docker.containers.list()): + print(f" {container._id}") + + async def run_container(docker): + print('== Running a hello-world container ==') + container = await docker.containers.create_or_replace( + config={ + 'Cmd': ['/bin/ash', '-c', 'echo "hello world"'], + 'Image': 'alpine:latest', + }, + name='testing', + ) + await container.start() + logs = await container.log(stdout=True) + print(''.join(logs)) + await container.delete(force=True) + + async def main(): + docker = aiodocker.Docker() + await list_things(docker) + await run_container(docker) + await docker.close() + + if __name__ == "__main__": + asyncio.run(main()) diff --git a/aiodocker/containers.py b/aiodocker/containers.py index a390dcd5..0cc87425 100644 --- a/aiodocker/containers.py +++ b/aiodocker/containers.py @@ -75,7 +75,7 @@ async def run( try: container = await self.create(config, name=name) except DockerError as err: - # image not fount, try pulling it + # image not found, try pulling it if err.status == 404 and "Image" in config: await self.docker.pull(config["Image"], auth=auth) container = await self.create(config, name=name) diff --git a/aiodocker/services.py b/aiodocker/services.py index bd06f95f..afc276d9 100644 --- a/aiodocker/services.py +++ b/aiodocker/services.py @@ -75,7 +75,7 @@ async def create( if auth and registry is None: raise KeyError( - "When auth is specified you need to specifiy also the registry" + "When auth is specified you need to specify also the registry" ) # from {"key":"value"} to ["key=value"] diff --git a/docs/configs.rst b/docs/configs.rst index 4686dafd..c9566b55 100644 --- a/docs/configs.rst +++ b/docs/configs.rst @@ -11,45 +11,46 @@ Create a config import asyncio import aiodocker - docker = aiodocker.Docker() - - async def create_config(): + async def create_config(docker): config = await docker.configs.create( name="my_config", - data="This is my config data" + data="This is my config data", ) - await docker.close() return config - async def create_service(TaskTemplate): + async def create_service(docker, task_template): service = await docker.services.create( - task_template=TaskTemplate, - name="my_service" + task_template=task_template, + name="my_service", ) - await docker.close() + return service - if __name__ == '__main__': - loop = asyncio.get_event_loop() - my_config = loop.run_until_complete(create_config()) - TaskTemplate = { + async def main(): + docker = aiodocker.Docker() + my_config = await create_config(docker) + task_template = { "ContainerSpec": { "Image": "redis", "Configs": [ - { - "File": { - "Name": my_config["Spec"]["Name"], - "UID": "0", - "GID": "0", - "Mode": 292 + { + "File": { + "Name": my_config["Spec"]["Name"], + "UID": "0", + "GID": "0", + "Mode": 292 + }, + "ConfigID": my_config["ID"], + "ConfigName": my_config["Spec"]["Name"], }, - "ConfigID": my_config["ID"], - "ConfigName": my_config["Spec"]["Name"], - } ], }, } - loop.run_until_complete(create_service(TaskTemplate)) - loop.close() + service = await create_service(docker, task_template) + print(service) + await docker.close() + + if __name__ == "__main__": + asyncio.run(main()) ------------ diff --git a/docs/containers.rst b/docs/containers.rst index b825a29f..3a3a9e7a 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -7,31 +7,27 @@ Create a container .. code-block:: python - import asyncio - import aiodocker - - docker = aiodocker.Docker() - - config = { - "Cmd": ["/bin/ls"], - "Image": "alpine:latest", - "AttachStdin": False, - "AttachStdout": False, - "AttachStderr": False, - "Tty": False, - "OpenStdin": False, - } - - async def create_container(): - container = await docker.containers.create(config=config) - print(container) - await docker.close() - - - if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(create_container()) - loop.close() + import asyncio + import aiodocker + + config = { + "Cmd": ["/bin/ls"], + "Image": "alpine:latest", + "AttachStdin": False, + "AttachStdout": False, + "AttachStderr": False, + "Tty": False, + "OpenStdin": False, + } + + async def create_container(): + docker = aiodocker.Docker() + container = await docker.containers.create(config=config) + print(container) + await docker.close() + + if __name__ == "__main__": + asyncio.run(create_container()) --------- Reference diff --git a/docs/index.rst b/docs/index.rst index b7b57e0c..59cdf46f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,41 +44,40 @@ Examples .. code-block:: python - import asyncio - import aiodocker - - async def list_things(): - docker = aiodocker.Docker() - print('== Images ==') - for image in (await docker.images.list()): - tags = image['RepoTags'][0] if image['RepoTags'] else '' - print(image['Id'], tags) - print('== Containers ==') - for container in (await docker.containers.list()): - print(f" {container._id}") - await docker.close() - - async def run_container(): - docker = aiodocker.Docker() - print('== Running a hello-world container ==') - container = await docker.containers.create_or_replace( - config={ - 'Cmd': ['/bin/ash', '-c', 'echo "hello world"'], - 'Image': 'alpine:latest', - }, - name='testing', - ) - await container.start() - logs = await container.log(stdout=True) - print(''.join(logs)) - await container.delete(force=True) - await docker.close() - - if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(list_things()) - loop.run_until_complete(run_container()) - loop.close() + import asyncio + import aiodocker + + async def list_things(docker): + print('== Images ==') + for image in (await docker.images.list()): + tags = image['RepoTags'][0] if image['RepoTags'] else '' + print(image['Id'], tags) + print('== Containers ==') + for container in (await docker.containers.list()): + print(f" {container._id}") + + async def run_container(docker): + print('== Running a hello-world container ==') + container = await docker.containers.create_or_replace( + config={ + 'Cmd': ['/bin/ash', '-c', 'echo "hello world"'], + 'Image': 'alpine:latest', + }, + name='testing', + ) + await container.start() + logs = await container.log(stdout=True) + print(''.join(logs)) + await container.delete(force=True) + + async def main(): + docker = aiodocker.Docker() + await list_things(docker) + await run_container(docker) + await docker.close() + + if __name__ == "__main__": + asyncio.run(main()) Source code @@ -128,6 +127,7 @@ It's *Apache 2* licensed and freely available. secrets services swarm + system volumes tasks log diff --git a/docs/networks.rst b/docs/networks.rst new file mode 100644 index 00000000..23aa1bfe --- /dev/null +++ b/docs/networks.rst @@ -0,0 +1,44 @@ +========== +Networks +========== + +Create a network +================== + +.. code-block:: python + + import asyncio + import aiodocker + + async def create_network(): + docker = aiodocker.Docker() + network_config = { + "Name": "isolated_nw", + "Driver": "bridge", + "EnableIPv6": False, + "IPAM": { + "Driver": "default" + } + } + network = await docker.networks.create(config=network_config) + print(network) + await docker.close() + + if __name__ == "__main__": + asyncio.run(create_network()) + +--------- +Reference +--------- + +DockerNetworks +================ +.. autoclass:: aiodocker.docker.DockerNetworks + :members: + :undoc-members: + +DockerNetwork +=============== +.. autoclass:: aiodocker.docker.DockerNetwork + :members: + :undoc-members: diff --git a/docs/secrets.rst b/docs/secrets.rst index d77b62cf..47607dfb 100644 --- a/docs/secrets.rst +++ b/docs/secrets.rst @@ -11,45 +11,46 @@ Create a secret import asyncio import aiodocker - docker = aiodocker.Docker() - - async def create_secret(): + async def create_secret(docker): secret = await docker.secrets.create( name="my_secret", data="you can not read that terrible secret" ) - await docker.close() return secret - async def create_service(TaskTemplate): + async def create_service(docker, task_template): service = await docker.services.create( - task_template=TaskTemplate, + task_template=task_template, name="my_service" ) - await docker.close() + return service - if __name__ == '__main__': - loop = asyncio.get_event_loop() - my_secret = loop.run_until_complete(create_secret()) - TaskTemplate = { + async def main(): + docker = aiodocker.Docker() + my_secret = await create_secret(docker) + task_template = { "ContainerSpec": { "Image": "redis", "Secrets": [ - { - "File": { - "Name": my_secret["Spec"]["Name"], - "UID": "0", - "GID": "0", - "Mode": 292 + { + "File": { + "Name": my_secret["Spec"]["Name"], + "UID": "0", + "GID": "0", + "Mode": 292 + }, + "SecretID": my_secret["ID"], + "SecretName": my_secret["Spec"]["Name"] }, - "SecretID": my_secret["ID"], - "SecretName": my_secret["Spec"]["Name"] - } ], }, } - loop.run_until_complete(create_service(TaskTemplate)) - loop.close() + service = await create_service(docker, task_template) + print(service) + await docker.close() + + if __name__ == "__main__": + asyncio.run(main()) ------------ diff --git a/docs/services.rst b/docs/services.rst index b15141b1..ba570270 100644 --- a/docs/services.rst +++ b/docs/services.rst @@ -8,29 +8,25 @@ Create a service .. code-block:: python - import asyncio - import aiodocker - - docker = aiodocker.Docker() - - TaskTemplate = { - "ContainerSpec": { - "Image": "redis", - }, - } - - async def create_service(): - service = await docker.services.create( - task_template=TaskTemplate, - name="my_service" - ) - await docker.close() - - - if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(create_service()) - loop.close() + import asyncio + import aiodocker + + async def create_service(): + docker = aiodocker.Docker() + task_template = { + "ContainerSpec": { + "Image": "redis", + }, + } + service = await docker.services.create( + task_template=task_template, + name="my_service" + ) + print(service) + await docker.close() + + if __name__ == "__main__": + asyncio.run(create_service()) ------------ Reference diff --git a/docs/system.rst b/docs/system.rst new file mode 100644 index 00000000..6ea31271 --- /dev/null +++ b/docs/system.rst @@ -0,0 +1,14 @@ +====== +System +====== + +---------- +Reference +---------- + +DockerSystem +============= + +.. autoclass:: aiodocker.system.DockerSystem + :members: + :undoc-members: