Skip to content

Commit

Permalink
[docker] Add GitHub Action for building and publishing Docker images …
Browse files Browse the repository at this point in the history
…to GHCR (linkedin#1264)

- Add a GitHub Action for manual triggering to build Docker images from
specified Git tags.
    - The workflow checks out the repository, validates the tag, builds images
    using a specified script, and pushes them to the GitHub Container Registry.
    Verification steps confirm successful image creation and include metadata
    annotations.
- Upgrade GitHub Actions dependencies:
    - actions/upload-artifact from v3 to v4.  - actions/setup-java from v3 to v4.
    - Replaced gradle/gradle-build-action@v2 with gradle/actions/setup-gradle@v3.
    - Upgraded gradle/wrapper-validation-action@v1 to
    gradle/actions/wrapper-validation@v3.
- Upload unit test artifacts for each JDK in a separate file.
sushantmane authored Oct 29, 2024
1 parent b0eb29e commit f18db1c
Showing 14 changed files with 425 additions and 262 deletions.
12 changes: 6 additions & 6 deletions .github/rawWorkflows/gh-ci-parameterized-flow.txt
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
@@ -22,10 +22,10 @@
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: "$GradleArguments"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run Integration Tests
run: ./gradlew $GradleArguments
- name: Package Build Artifacts
if: success() || failure()
shell: bash
@@ -36,7 +36,7 @@
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
48 changes: 25 additions & 23 deletions .github/workflows/VeniceCI-CompatibilityTests.yml
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
@@ -28,10 +28,10 @@ jobs:
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: "-DmaxParallelForks=2 --parallel :internal:venice-avro-compatibility-test:test --continue"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run Avro Compatibility Tests
run: ./gradlew -DmaxParallelForks=2 --parallel :internal:venice-avro-compatibility-test:test --continue
- name: Package Build Artifacts
if: success() || failure()
shell: bash
@@ -42,7 +42,7 @@ jobs:
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
@@ -63,7 +63,7 @@ jobs:
# Checkout as many commits as needed for the diff
fetch-depth: 2
- name: Check if files have changed
uses: dorny/paths-filter@v2
uses: dorny/paths-filter@v3
id: check_alpini_files_changed
with:
filters: |
@@ -74,7 +74,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
with:
java-version: ${{ matrix.jdk }}
@@ -97,11 +97,12 @@ jobs:
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
- name: Setup Gradle
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
uses: gradle/gradle-build-action@v2
with:
arguments: "--continue --no-daemon -DmaxParallelForks=1 alpiniUnitTest"
uses: gradle/actions/setup-gradle@v4
- name: Run alpini unit tests
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
run: ./gradlew --continue --no-daemon -DmaxParallelForks=1 alpiniUnitTest
- name: Package Build Artifacts
if: steps.check_alpini_files_changed.outputs.alpini == 'true' && (success() || failure())
shell: bash
@@ -112,7 +113,7 @@ jobs:
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: steps.check_alpini_files_changed.outputs.alpini == 'true' && (success() || failure())
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
@@ -133,7 +134,7 @@ jobs:
# Checkout as many commits as needed for the diff
fetch-depth: 2
- name: Check if files have changed
uses: dorny/paths-filter@v2
uses: dorny/paths-filter@v3
id: check_alpini_files_changed
with:
filters: |
@@ -144,7 +145,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
with:
java-version: ${{ matrix.jdk }}
@@ -167,11 +168,12 @@ jobs:
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
- name: Setup Gradle
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
uses: gradle/gradle-build-action@v2
with:
arguments: "--continue --no-daemon -DmaxParallelForks=1 alpiniFunctionalTest"
uses: gradle/actions/setup-gradle@v4
- name: Run alpini functional tests
if: steps.check_alpini_files_changed.outputs.alpini == 'true'
run: ./gradlew --continue --no-daemon -DmaxParallelForks=1 alpiniFunctionalTest
- name: Package Build Artifacts
if: steps.check_alpini_files_changed.outputs.alpini == 'true' && (success() || failure())
shell: bash
@@ -182,7 +184,7 @@ jobs:
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: steps.check_alpini_files_changed.outputs.alpini == 'true' && (success() || failure())
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
@@ -200,7 +202,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
@@ -211,7 +213,7 @@ jobs:
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v4
- name: Build with gradle
run: ./gradlew assemble --continue --no-daemon -DforkEvery=1 -DmaxParallelForks=1

@@ -244,7 +246,7 @@ jobs:
tar -zcvf ${{ github.job }}-artifacts.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-artifacts.tar.gz
384 changes: 192 additions & 192 deletions .github/workflows/VeniceCI-E2ETests.yml

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions .github/workflows/VeniceCI-StaticAnalysisAndUnitTests.yml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ jobs:
with:
fetch-depth: 0
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3

StaticAnalysis:
strategy:
@@ -28,7 +28,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
@@ -38,10 +38,10 @@ jobs:
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: "--continue --no-daemon clean check --parallel -Pspotallbugs -x test -x integrationTest -x jacocoTestCoverageVerification"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run Static Analysis
run: ./gradlew --continue --no-daemon clean check --parallel -Pspotallbugs -x test -x integrationTest -x jacocoTestCoverageVerification
- name: Package Build Artifacts
if: success() || failure()
shell: bash
@@ -52,7 +52,7 @@ jobs:
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
@@ -70,7 +70,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
@@ -80,10 +80,10 @@ jobs:
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: "-x :internal:venice-avro-compatibility-test:test jacocoTestCoverageVerification diffCoverage --continue"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run Unit Tests with Code Coverage
run: ./gradlew -x :internal:venice-avro-compatibility-test:test jacocoTestCoverageVerification diffCoverage --continue
- name: Package Build Artifacts
if: success() || failure()
shell: bash
@@ -94,9 +94,9 @@ jobs:
tar -zcvf ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz ${{ github.job }}-artifacts
- name: Upload Build Artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}
name: ${{ github.job }}-jdk${{ matrix.jdk }}
path: ${{ github.job }}-jdk${{ matrix.jdk }}-logs.tar.gz
retention-days: 30

140 changes: 140 additions & 0 deletions .github/workflows/build-and-publish-docker-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Build and Publish Docker Images

on:
workflow_dispatch:
inputs:
git_tag:
description: "Tag to build Docker images from"
required: true
default: "0.4.17"

permissions:
contents: read
packages: write

jobs:
docker-build-and-push:
runs-on: ubuntu-latest

steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Step 1: Check out the repository
- name: Check out the repository
uses: actions/checkout@v4
with:
fetch-depth: 0

# Step 2: Set up JDK
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"
cache: "gradle"

# Step 3: Fetch all branches and tags
- name: Fetch all branches and tags
run: |
git remote set-head origin --auto
git remote add upstream https://github.com/linkedin/venice
git fetch upstream
git fetch --tags
# Step 4: Validate and check out the tag
- name: Check out tag and validate
id: validate_tag
run: |
TAG="${{ github.event.inputs.git_tag }}"
if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
echo "Error: Tag '$TAG' does not exist."
# Emit an annotation warning that the tag was not valid
echo "::error title=Tag Validation Failed::The specified git tag '$TAG' does not exist in the repository."
exit 1
fi
# Check out the specified tag
git checkout "tags/$TAG"
# Get latest commit information
COMMIT_SHA=$(git rev-parse HEAD)
COMMIT_MSG=$(git log -1 --pretty=%B)
COMMIT_DATE=$(git log -1 --pretty=%cd --date=format:"%Y-%m-%d %H:%M:%S")
echo "Latest commit SHA: $COMMIT_SHA"
echo "Latest commit date: $COMMIT_DATE"
# Set outputs
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "commit_date=$COMMIT_DATE" >> $GITHUB_OUTPUT
# Step 5: Run Docker image build and push script
- name: Build Docker images
env:
TAG_VERSION: ${{ github.event.inputs.git_tag }}
run: |
# Pass the tag as the version
bash docker/build-venice-docker-images.sh "ghcr.io/${{ github.repository }}" "$TAG_VERSION"
# Step 6: List generated Docker images
- name: List generated Docker images
run: |
echo "Listing all Docker images:"
docker images
# Step 7: Verify that images were created
- name: Verify Docker images were created
run: |
TAG_VERSION="${{ github.event.inputs.git_tag }}"
TARGETS=("venice-controller" "venice-server" "venice-router" "venice-client")
for target in "${TARGETS[@]}"; do
IMAGE_NAME="ghcr.io/${{ github.repository }}/$target:$TAG_VERSION"
if ! docker inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Error: Docker image $IMAGE_NAME was not created."
exit 1
else
echo "Docker image $IMAGE_NAME exists."
fi
done
# Step 8: Publish Docker images to repo's GHCR
- name: Push Docker images to repo's GHCR
id: publish_images
run: |
targets=("venice-controller" "venice-server" "venice-router" "venice-client")
TAG_VERSION="${{ github.event.inputs.git_tag }}"
TAG_VERSION=$(echo "$TAG_VERSION" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') # Sanitize tag
IMAGE_URLS=""
for target in "${targets[@]}"; do
IMAGE_NAME="ghcr.io/${{ github.repository }}/$target:$TAG_VERSION"
echo "Pushing image: $IMAGE_NAME"
docker push "$IMAGE_NAME"
# Append URL to list with clickable full URL
IMAGE_URLS+="- $target - [$IMAGE_NAME](https://$IMAGE_NAME)\n"
done
# Save IMAGE_URLS to GITHUB_OUTPUT for later use in the workflow
echo -e "image_urls<<EOF\n$IMAGE_URLS\nEOF" >> $GITHUB_OUTPUT
- name: Display clickable image URLs
run: |
echo "## Published Docker Images"
echo -e "${{ steps.publish_images.outputs.image_urls }}"
# Step 9: Annotate workflow with Docker image metadata
- name: Annotate workflow with Docker image metadata
if: success()
run: |
# Capture the current date and time for the published date
PUBLISHED_DATE=$(date +"%Y-%m-%d %H:%M:%S")
echo "**Docker Images Published:** ✅" >> $GITHUB_STEP_SUMMARY
echo "**Published Date:** $PUBLISHED_DATE" >> $GITHUB_STEP_SUMMARY
echo "**Git Tag Used:** ${{ github.event.inputs.git_tag }}" >> $GITHUB_STEP_SUMMARY
echo "**Latest Commit SHA:** ${{ steps.validate_tag.outputs.commit_sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Latest Commit Date:** ${{ steps.validate_tag.outputs.commit_date }}" >> $GITHUB_STEP_SUMMARY
echo "**Docker Image URLs:**" >> $GITHUB_STEP_SUMMARY
echo -e "${{ steps.publish_images.outputs.image_urls }}" >> $GITHUB_STEP_SUMMARY
14 changes: 7 additions & 7 deletions .github/workflows/build-and-upload-archives-on-demand.yml
Original file line number Diff line number Diff line change
@@ -18,23 +18,23 @@ jobs:
fetch-depth: 0 # all history for all branches and tags

- name: Set up JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: '11'

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3

- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: clean assemble
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build
run: ./gradlew clean assemble

- name: package artifacts
run: mkdir staging && find . -name "*.jar" -exec cp "{}" staging \;

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: Package
path: staging
Original file line number Diff line number Diff line change
@@ -21,16 +21,16 @@ jobs:
fetch-depth: 0 # all history for all branches and tags

- name: Set up JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: '11'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Upload archive
env:
JFROG_USERNAME: ${{ secrets.JFROG_USERNAME }}
JFROG_API_KEY: ${{ secrets.JFROG_API_KEY }}
uses: gradle/gradle-build-action@v2
with:
arguments: |
"-Pversion=${{ github.ref_name }}" publishAllPublicationsToLinkedInJFrogRepository
run: ./gradlew -Pversion=${{ github.ref_name }} publishAllPublicationsToLinkedInJFrogRepository
9 changes: 4 additions & 5 deletions .github/workflows/build-and-upload-archives.yml
Original file line number Diff line number Diff line change
@@ -20,17 +20,16 @@ jobs:
fetch-depth: 0 # all history for all branches and tags

- name: Set up JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: '11'
- name: Get Latest Tag
run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0 main)`" >> $GITHUB_ENV
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Upload archive
env:
JFROG_USERNAME: ${{ secrets.JFROG_USERNAME }}
JFROG_API_KEY: ${{ secrets.JFROG_API_KEY }}
uses: gradle/gradle-build-action@v2
with:
arguments: |
"-Pversion=${{ env.GIT_TAG }}" publishAllPublicationsToLinkedInJFrogRepository
run: ./gradlew -Pversion=${{ env.GIT_TAG }} publishAllPublicationsToLinkedInJFrogRepository
10 changes: 5 additions & 5 deletions .github/workflows/publish-javadoc.yml
Original file line number Diff line number Diff line change
@@ -25,11 +25,11 @@ jobs:
git rebase origin/main
git push -f origin javadoc
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: "aggregateJavadoc"
uses: gradle/actions/wrapper-validation@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build Javadoc
run: ./gradlew aggregateJavadoc
- name: Deploy to GitHub Page
uses: JamesIves/github-pages-deploy-action@v4.6.1
with:
24 changes: 19 additions & 5 deletions docker/build-venice-docker-images.sh
Original file line number Diff line number Diff line change
@@ -13,20 +13,34 @@ echo "Building docker images for repository $repository, version $oss_release"
head_hash=$(git rev-parse --short HEAD)
version=$oss_release

cp *py venice-client/
cp *py venice-client/
cp ../clients/venice-push-job/build/libs/venice-push-job-all.jar venice-client/
cp ../clients/venice-thin-client/build/libs/venice-thin-client-all.jar venice-client/
cp ../clients/venice-admin-tool/build/libs/venice-admin-tool-all.jar venice-client/
cp *py venice-server/
cp *py venice-server/
cp ../services/venice-server/build/libs/venice-server-all.jar venice-server/
cp *py venice-controller/
cp *py venice-controller/
cp ../services/venice-controller/build/libs/venice-controller-all.jar venice-controller/
cp *py venice-router/
cp *py venice-router/
cp ../services/venice-router/build/libs/venice-router-all.jar venice-router/

targets=(venice-controller venice-server venice-router venice-client)

# Define image descriptions
declare -A image_descriptions=(
["venice-controller"]="Venice Controller: responsible for managing administrative operations such as store creation, deletion, updates, and starting new pushes or versions."
["venice-server"]="Venice Server: Acts as a Venice storage node, handling data ingestion, storage, and serving from RocksDB."
["venice-router"]="Venice Router: responsible for routing requests from clients to the appropriate Venice Servers."
["venice-client"]="Venice Client: Includes tools for store administration (e.g., create, delete), data pushing (VPJ), and a CLI for querying store data."
)

# Build each target with labels
for target in ${targets[@]}; do
docker buildx build --load --platform linux/amd64 \
--label "org.opencontainers.image.source=https://github.com/linkedin/venice" \
--label "org.opencontainers.image.authors=VeniceDB" \
--label "org.opencontainers.image.description=${image_descriptions[$target]}" \
--label "org.opencontainers.image.licenses=BSD-2-Clause" \
-t "$repository/$target:$version" -t "$repository/$target:latest-dev" $target
docker buildx build --load --platform linux/amd64 -t "$repository/$target:$version" -t "$repository/$target:latest-dev" $target
done

2 changes: 2 additions & 0 deletions docker/venice-client/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu

LABEL org.opencontainers.image.description="Venice Client: Includes tools for store administration (e.g., create, delete), data pushing (VPJ), and a CLI for querying store data."

ENV VENICE_DIR=/opt/venice

RUN apt-get update
2 changes: 2 additions & 0 deletions docker/venice-controller/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu

LABEL org.opencontainers.image.description="Venice Controller: responsible for managing administrative operations such as store creation, deletion, updates, and starting new pushes or versions."

ENV VENICE_DIR=/opt/venice

RUN apt-get update
2 changes: 2 additions & 0 deletions docker/venice-router/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu

LABEL org.opencontainers.image.description="Venice Router: responsible for routing requests from clients to the appropriate Venice Servers."

ENV VENICE_DIR=/opt/venice

RUN apt-get update
2 changes: 2 additions & 0 deletions docker/venice-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu

LABEL org.opencontainers.image.description="Venice Server: Acts as a Venice storage node, handling data ingestion, storage, and serving from RocksDB."

ENV VENICE_DIR=/opt/venice

RUN apt-get update

0 comments on commit f18db1c

Please sign in to comment.