diff --git a/.github/actions/format-docker-build-args/action.yml b/.github/actions/format-docker-build-args/action.yml new file mode 100644 index 000000000000..6fa8e7ea22c5 --- /dev/null +++ b/.github/actions/format-docker-build-args/action.yml @@ -0,0 +1,34 @@ +name: "format-docker-build-args" +description: "Format the build args to work with the wretry.action" +inputs: + build-args: + description: "List of build-time variables." +outputs: + build-args: + description: "Build args formatted to work with the wretry.action" + value: ${{ steps.build-args.outputs.build-args }} +runs: + using: "composite" + steps: + - id: build-args + shell: python + run: | + import os + + # Adds two spaces to the line breaks to ensure proper indentation + # when passing the multi-line string to the wretry.action. + # Without it, the multi-line string is passed like this: + # + # build-args: | + # ARG1= + # ARG2= + # ARG3= + # + # This causes the Docker action to interpret ARG2 and ARG3 as keys instead + # of values ​​of the multi-line string. + build_args = '''${{ inputs.build-args }}'''.replace("\n", "\n ") + + with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: + print("build-args< matrix.json + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + echo "base-file-dir=$(jq -r .base.images[0].file_dir matrix.json)" >> $GITHUB_OUTPUT + echo "base-namespace-repository=$(jq -r .base.images[0].namespace_repository matrix.json)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + with: + # https://github.com/docker/build-push-action/issues/1116#issuecomment-2132899905 + driver: docker + + - name: Build base image + uses: Wandalen/wretry.action@6feedb7dedadeb826de0f45ff482b53b379a7844 # v3.5.0 + with: + action: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + attempt_limit: 60 # 60 attempts * (9 secs delay + 1 sec retry) = ~10 mins + attempt_delay: 9000 # 9 secs + with: | + pull: true + context: ${{ steps.matrix.outputs.base-file-dir }} + load: true + tags: ${{ steps.matrix.outputs.base-namespace-repository }}:e2e-test + build-args: | + ${{ steps.build-args.outputs.build-args }} + + - name: Build binary images + run: | + set -e -o pipefail + + jq -c '.binary.images[]' <<< '${{ steps.matrix.outputs.matrix }}' | while read -r i; do + FILE_DIR=$(echo "$i" | jq -r .file_dir) + NAMESPACE_REPOSITORY=$(echo "$i" | jq -r .namespace_repository) + docker build --build-arg BASE_IMAGE="e2e-test" -t "${NAMESPACE_REPOSITORY}:e2e-test" "${FILE_DIR}" + done + + - name: Bootstrap + uses: ./.github/actions/bootstrap + with: + python-version: 3.9 # Explicitly set this so that we can add a matrix later + poetry-skip: 'true' + - name: Install Flower + run: python -m pip install . + + - name: Test images + env: + FLWR_VERSION: e2e-test + PROJECT_DIR: e2e-test + working-directory: src/docker/complete + run: | + flwr new ${PROJECT_DIR} --framework NumPy --username flwrlabs + + docker compose -f certs.yml up --build + sudo chown -R 49999:49999 superexec-certificates/* superlink-certificates/* + + cat <<- EOT >> "${PROJECT_DIR}/pyproject.toml" + [tool.flwr.federations.local-deployment-tls] + address = "127.0.0.1:9093" + root-certificates = "../superexec-certificates/ca.crt" + EOT + + docker compose -f compose.yml -f with-tls.yml up --build -d + docker compose logs -f & + + timeout 30 flwr run ${PROJECT_DIR} local-deployment-tls + timeout 120 docker compose logs -f superexec | grep -q 'Run finished' diff --git a/.github/workflows/docker-build-main.yml b/.github/workflows/docker-build-main.yml index 81ef845eae29..4b6de6263b42 100644 --- a/.github/workflows/docker-build-main.yml +++ b/.github/workflows/docker-build-main.yml @@ -15,6 +15,7 @@ jobs: pip-version: ${{ steps.versions.outputs.pip-version }} setuptools-version: ${{ steps.versions.outputs.setuptools-version }} flwr-version-ref: ${{ steps.versions.outputs.flwr-version-ref }} + matrix: ${{ steps.versions.outputs.matrix }} steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -26,6 +27,8 @@ jobs: echo "pip-version=${{ steps.bootstrap.outputs.pip-version }}" >> "$GITHUB_OUTPUT" echo "setuptools-version=${{ steps.bootstrap.outputs.setuptools-version }}" >> "$GITHUB_OUTPUT" echo "flwr-version-ref=git+${{ github.server_url }}/${{ github.repository }}.git@${{ github.sha }}" >> "$GITHUB_OUTPUT" + python dev/build-docker-image-matrix.py --flwr-version unstable --simple > matrix.json + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT build-docker-base-images: name: Build base images @@ -33,8 +36,8 @@ jobs: uses: ./.github/workflows/_docker-build.yml needs: parameters with: - namespace-repository: flwr/base - file-dir: src/docker/base/ubuntu + namespace-repository: ${{ fromJson(needs.parameters.outputs.matrix).base[0].namespace_repository }} + file-dir: ${{ fromJson(needs.parameters.outputs.matrix).base[0].file_dir }} build-args: | PIP_VERSION=${{ needs.parameters.outputs.pip-version }} SETUPTOOLS_VERSION=${{ needs.parameters.outputs.setuptools-version }} @@ -48,17 +51,10 @@ jobs: name: Build binary images if: github.repository == 'adap/flower' uses: ./.github/workflows/_docker-build.yml - needs: build-docker-base-images + needs: [parameters, build-docker-base-images] strategy: fail-fast: false - matrix: - images: [ - { repository: "flwr/superlink", file_dir: "src/docker/superlink" }, - { repository: "flwr/supernode", file_dir: "src/docker/supernode" }, - { repository: "flwr/serverapp", file_dir: "src/docker/serverapp" }, - { repository: "flwr/superexec", file_dir: "src/docker/superexec" }, - { repository: "flwr/clientapp", file_dir: "src/docker/clientapp" } - ] + matrix: ${{ fromJson(needs.parameters.outputs.matrix).binary }} with: namespace-repository: ${{ matrix.images.repository }} file-dir: ${{ matrix.images.file_dir }} diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml new file mode 100644 index 000000000000..dbb4e3a2358a --- /dev/null +++ b/.github/workflows/docker-test.yml @@ -0,0 +1,43 @@ +name: Test Docker + +on: + push: + +jobs: + # parameters: + # if: github.repository == 'adap/flower' + # name: Collect docker build parameters + # runs-on: ubuntu-22.04 + # timeout-minutes: 10 + # outputs: + # pip-version: ${{ steps.versions.outputs.pip-version }} + # setuptools-version: ${{ steps.versions.outputs.setuptools-version }} + # flwr-version-ref: ${{ steps.versions.outputs.flwr-version-ref }} + # matrix: ${{ steps.versions.outputs.matrix }} + # steps: + # - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # - uses: ./.github/actions/bootstrap + # id: bootstrap + + # - id: versions + # run: | + # echo "pip-version=${{ steps.bootstrap.outputs.pip-version }}" >> "$GITHUB_OUTPUT" + # echo "setuptools-version=${{ steps.bootstrap.outputs.setuptools-version }}" >> "$GITHUB_OUTPUT" + # echo "flwr-version-ref=git+${{ github.server_url }}/${{ github.repository }}.git@${{ github.sha }}" >> "$GITHUB_OUTPUT" + # python dev/build-docker-image-matrix.py --flwr-version unstable --simple > matrix.json + # echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + build-docker-base-images: + name: Build base images + if: github.repository == 'adap/flower' + uses: ./.github/workflows/_docker-test.yml + # needs: parameters + with: + build-args: | + PIP_VERSION=24.1 + SETUPTOOLS_VERSION=70 + FLWR_VERSION=1.11.0 + secrets: + dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index fcefff300cb7..a9c021fd0850 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -18,6 +18,7 @@ jobs: skip: ${{ steps.release.outputs.skip }} pip-version: ${{ steps.release.outputs.pip-version }} setuptools-version: ${{ steps.release.outputs.setuptools-version }} + matrix: ${{ steps.release.outputs.matrix }} steps: - uses: actions/checkout@v4 - name: Bootstrap @@ -37,6 +38,8 @@ jobs: echo "version=$(poetry version -s)" >> $GITHUB_OUTPUT echo "pip-version=${{ steps.bootstrap.outputs.pip-version }}" >> "$GITHUB_OUTPUT" echo "setuptools-version=${{ steps.bootstrap.outputs.setuptools-version }}" >> "$GITHUB_OUTPUT" + python dev/build-docker-image-matrix.py --flwr-version nightly --simple > matrix.json + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT build-docker-base-images: name: Build nightly base images @@ -44,8 +47,8 @@ jobs: uses: ./.github/workflows/_docker-build.yml needs: release-nightly with: - namespace-repository: flwr/base - file-dir: src/docker/base/ubuntu + namespace-repository: ${{ fromJson(needs.release-nightly.outputs.matrix).base[0].namespace_repository }} + file-dir: ${{ fromJson(needs.release-nightly.outputs.matrix).base[0].file_dir }} build-args: | PIP_VERSION=${{ needs.release-nightly.outputs.pip-version }} SETUPTOOLS_VERSION=${{ needs.release-nightly.outputs.setuptools-version }} @@ -65,16 +68,9 @@ jobs: needs: [release-nightly, build-docker-base-images] strategy: fail-fast: false - matrix: - images: [ - { repository: "flwr/superlink", file_dir: "src/docker/superlink" }, - { repository: "flwr/supernode", file_dir: "src/docker/supernode" }, - { repository: "flwr/serverapp", file_dir: "src/docker/serverapp" }, - { repository: "flwr/superexec", file_dir: "src/docker/superexec" }, - { repository: "flwr/clientapp", file_dir: "src/docker/clientapp" } - ] + matrix: ${{ fromJson(needs.release-nightly.outputs.matrix).binary }} with: - namespace-repository: ${{ matrix.images.repository }} + namespace-repository: ${{ matrix.images.namespace_repository }} file-dir: ${{ matrix.images.file_dir }} build-args: BASE_IMAGE=${{ needs.release-nightly.outputs.version }} tags: | diff --git a/dev/build-docker-image-matrix.py b/dev/build-docker-image-matrix.py index c19949e358b9..928f0c69d3fb 100644 --- a/dev/build-docker-image-matrix.py +++ b/dev/build-docker-image-matrix.py @@ -29,6 +29,9 @@ class Distro: DOCKERFILE_ROOT = "src/docker" +UBUNTU_22_04 = Distro(DistroName.UBUNTU, "22.04") +ALPINE_3_19 = Distro(DistroName.ALPINE, "3.19") + @dataclass class BaseImage: @@ -121,26 +124,23 @@ def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: return [image.tag] -if __name__ == "__main__": - arg_parser = argparse.ArgumentParser( - description="Generate Github Docker workflow matrix" - ) - arg_parser.add_argument("--flwr-version", type=str, required=True) - args = arg_parser.parse_args() - - flwr_version = args.flwr_version +def build_complete_matrix(flwr_version: str): + """Generates a matrix comprising Ubuntu and Alpine images. For Alpine, the matrix + includes only the latest supported Python version, whereas for Ubuntu, it includes all + supported Python versions. + """ # ubuntu base images for each supported python version ubuntu_base_images = generate_base_images( flwr_version, SUPPORTED_PYTHON_VERSIONS, - [Distro(DistroName.UBUNTU, "22.04")], + [UBUNTU_22_04], ) # alpine base images for the latest supported python version alpine_base_images = generate_base_images( flwr_version, [LATEST_SUPPORTED_PYTHON_VERSION], - [Distro(DistroName.ALPINE, "3.19")], + [ALPINE_3_19], ) base_images = ubuntu_base_images + alpine_base_images @@ -154,6 +154,7 @@ def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: lambda image: image.python_version == LATEST_SUPPORTED_PYTHON_VERSION, ) # ubuntu images for each supported python version + # and alpine image for the latest supported python version + generate_binary_images( "supernode", base_images, @@ -187,6 +188,53 @@ def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: ) ) + return base_images, binary_images + + +def build_ubuntu_python_latest_matrix(flwr_version: str): + """Generates a matrix comprising Ubuntu images. It includes only the latest supported Python + version. + """ + + # ubuntu base image for the latest supported python version + base_images = generate_base_images( + flwr_version, + [LATEST_SUPPORTED_PYTHON_VERSION], + [UBUNTU_22_04], + ) + + binary_images = ( + # ubuntu and alpine images for the latest supported python version + generate_binary_images("superlink", base_images) + # ubuntu images for the latest supported python version + + generate_binary_images("supernode", base_images) + # ubuntu images for the latest supported python version + + generate_binary_images("serverapp", base_images) + # ubuntu images for the latest supported python version + + generate_binary_images("superexec", base_images) + # ubuntu images for the latest supported python version + + generate_binary_images("clientapp", base_images) + ) + + return base_images, binary_images + + +if __name__ == "__main__": + arg_parser = argparse.ArgumentParser( + description="Generate Github Docker workflow matrix" + ) + arg_parser.add_argument("--flwr-version", type=str, required=True) + arg_parser.add_argument("--simple", action="store_true") + args = arg_parser.parse_args() + + flwr_version = args.flwr_version + simple = args.simple + + if simple: + base_images, binary_images = build_ubuntu_python_latest_matrix(flwr_version) + else: + base_images, binary_images = build_complete_matrix(flwr_version) + print( json.dumps( {