diff --git a/.allstar/binary_artifacts.yaml b/.allstar/binary_artifacts.yaml new file mode 100644 index 0000000000..0b70480559 --- /dev/null +++ b/.allstar/binary_artifacts.yaml @@ -0,0 +1,4 @@ +# Ignore reason: jars are used for testing purposes only +ignorePaths: + - jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/libs/dependency-1.0.0.jar + - jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..24a7442795 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.bat eol=crlf +*.cmd eol=crlf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 79001c0a57..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,26 +0,0 @@ - - -**Description of the issue**: - -**Expected behavior**: - -**Steps to reproduce**: - -**Environment**: - -**`jib-maven-plugin` Configuration**: -```xml -PASTE YOUR pom.xml CONFIGURATION HERE -``` - -**`jib-gradle-plugin` Configuration**: -```groovy -PASTE YOUR build.gradle CONFIGURATION HERE -``` - -**Log output**: - -**Additional Information**: - - - diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md new file mode 100644 index 0000000000..804b3a3f1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_report.md @@ -0,0 +1,59 @@ +--- +name: Issue report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +**Environment**: + +- *Jib version:* +- *Build tool:* +- *OS:* + +**Description of the issue**: + + +**Expected behavior**: + + +**Steps to reproduce**: + + + 1. + 2. + 3. + +**`jib-maven-plugin` Configuration**: +```xml +PASTE YOUR pom.xml CONFIGURATION HERE +``` + +**`jib-gradle-plugin` Configuration**: +```groovy +PASTE YOUR build.gradle CONFIGURATION HERE +``` + +**Log output**: + +**Additional Information**: + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..f47aa2784e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +Thank you for your interest in contributing! For general guidelines, please refer to +the [contributing guide](https://github.com/GoogleContainerTools/jib/blob/master/CONTRIBUTING.md). + +Please follow the guidelines below before opening an issue or a PR: +- [ ] Ensure the issue was not already reported. +- [ ] Create a new issue at https://github.com/GoogleContainerTools/jib/issues/new/choose if you are unable to find an existing issue addressing your problem. Make sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. +- [ ] Discuss the priority and potential solutions with the maintainers in the issue. The maintainers would review the issue and add a label "Accepting Contributions" once the issue is ready for accepting contributions. +- [ ] Open a PR only if the issue is labeled with "Accepting Contributions", ensure the PR description clearly describes the problem and solution. Note that an open PR without an issues labeled with "Accepting Contributions" will not be accepted. +- [ ] Verify that integration tests and unit tests are passing after the change. +- [ ] Address all checkstyle issues. Refer to the [style guide](https://github.com/GoogleContainerTools/jib/blob/master/STYLE_GUIDE.md). + +Fixes # 🛠️ diff --git a/.github/RELEASE_TEMPLATES/cli_release_checklist.md b/.github/RELEASE_TEMPLATES/cli_release_checklist.md new file mode 100644 index 0000000000..1252a51182 --- /dev/null +++ b/.github/RELEASE_TEMPLATES/cli_release_checklist.md @@ -0,0 +1,15 @@ +--- +title: CLI Release {{ env.RELEASE_NAME }} +labels: release +--- +## Requirements +- [ ] ⚠️ Ensure the release process has succeeded before proceeding +- [ ] ⚠️ Publish [Release]({{ env.RELEASE_DRAFT }}) after adding CHANGELOG entries ([example](https://github.com/GoogleContainerTools/jib/releases/tag/v0.8.0-cli)) + +## GCS +- [ ] Run {{ env.GCS_UPDATE_SCRIPT }} script to update GCS with the latest version number + +## Github +- [ ] Update [CHANGELOG.md]({{ env.CHANGELOG_URL }}) +- [ ] Update [README.md]({{ env.README_URL }}) +- [ ] Merge PR ({{ env.RELEASE_PR }}) diff --git a/.github/RELEASE_TEMPLATES/core_release_checklist.md b/.github/RELEASE_TEMPLATES/core_release_checklist.md new file mode 100644 index 0000000000..bf696ace77 --- /dev/null +++ b/.github/RELEASE_TEMPLATES/core_release_checklist.md @@ -0,0 +1,12 @@ +--- +title: Core Release {{ env.RELEASE_NAME }} +labels: release +--- +## Requirements +- [ ] ⚠️ Ensure the release process has succeeded before proceeding + +## Github +- [ ] Update [CHANGELOG.md]({{ env.CHANGELOG_URL }}) +- [ ] Update [README.md]({{ env.README_URL }}) +- [ ] Publish [Release]({{ env.RELEASE_DRAFT }}) +- [ ] Merge PR ({{ env.RELEASE_PR }}) diff --git a/.github/RELEASE_TEMPLATES/plugin_release_checklist.md b/.github/RELEASE_TEMPLATES/plugin_release_checklist.md new file mode 100644 index 0000000000..c5e26e157c --- /dev/null +++ b/.github/RELEASE_TEMPLATES/plugin_release_checklist.md @@ -0,0 +1,20 @@ +--- +title: Plugin Release {{ env.RELEASE_NAME }} +labels: release +--- +## Requirements +- [ ] ⚠️ Ensure the release process has succeeded before proceeding + +## GCS +- [ ] Run {{ env.GCS_UPDATE_SCRIPT }} script to update GCS with the latest version number + +## Github +- [ ] Update [CHANGELOG.md]({{ env.CHANGELOG_URL }}) +- [ ] Update [README.md]({{ env.README_URL }}) +- [ ] Search/replace the old published version with the new published version in [examples](https://github.com/GoogleContainerTools/jib/tree/master/examples) +- [ ] Publish [Release]({{ env.RELEASE_DRAFT }}) +- [ ] Merge PR ({{ env.RELEASE_PR }}) + +#### Jib-Extensions +- [ ] Update versions in [Jib Extensions](https://github.com/GoogleContainerTools/jib-extensions) +- [ ] If there were Gradle API or Jib API changes, double-check compatibility and update Version Matrix on jib-extensions. It may require re-releasing first-party extensions. See [jib-extensions#45](https://github.com/GoogleContainerTools/jib-extensions/pull/45), [jib-extensions#44](https://github.com/GoogleContainerTools/jib-extensions/pull/44), and [jib-extensions#42](https://github.com/GoogleContainerTools/jib-extensions/pull/42) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..1ef0730a67 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000000..1d76e03ecd --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,10 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Gradle wrapper validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v3.5.0 diff --git a/.github/workflows/jib-cli-release.yml b/.github/workflows/jib-cli-release.yml new file mode 100644 index 0000000000..cea1c54d30 --- /dev/null +++ b/.github/workflows/jib-cli-release.yml @@ -0,0 +1,156 @@ +name: Release Jib CLI +on: + workflow_dispatch: + inputs: + release_version: + description: new release version + required: true + default: (for example, 0.1.0) + +permissions: + contents: write + issues: write + +jobs: + release: + name: Release Jib CLI + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + upload_url: ${{ steps.create-release.outputs.upload_url }} + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + + - name: Build project + run: | + if [[ ! "${GITHUB_EVENT_INPUTS_RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo 'version "${GITHUB_EVENT_INPUTS_RELEASE_VERSION}" not in ###.###.### format' + exit 1 + fi + # TODO: run integration test? (Requries auth with GCP.) + ./gradlew clean build --stacktrace + env: + GITHUB_EVENT_INPUTS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} + + - name: Run Gradle release + run: | + git checkout -b cli-release-v${GITHUB_EVENT_INPUTS_RELEASE_VERSION} + git config user.email ${GITHUB_ACTOR}@users.noreply.github.com + git config user.name ${GITHUB_ACTOR} + # This creates the tag (e.g., "v0.1.0-cli") and pushes the updated + # branch (e.g., "cli-release-v0.1.0") and the new tag. + ./gradlew jib-cli:release \ + -Prelease.useAutomaticVersion=true \ + -Prelease.releaseVersion=${GITHUB_EVENT_INPUTS_RELEASE_VERSION} + env: + GITHUB_EVENT_INPUTS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} + + - name: Build Jib CLI release binaries + run: | + git checkout v${GITHUB_EVENT_INPUTS_RELEASE_VERSION}-cli + ./gradlew jib-cli:instDist --stacktrace + + cd jib-cli/build/distributions + mv jib-${GITHUB_EVENT_INPUTS_RELEASE_VERSION}.zip jib-jre-${GITHUB_EVENT_INPUTS_RELEASE_VERSION}.zip + sha256sum jib-jre-${GITHUB_EVENT_INPUTS_RELEASE_VERSION}.zip > zip.sha256 + env: + GITHUB_EVENT_INPUTS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} + + - name: Generate SLSA subject for Jib CLI release binaries + id: hash + working-directory: jib-cli/build/distributions + run: echo "hashes=$(cat zip.sha256 | base64 -w0)" >> $GITHUB_OUTPUT + + - name: Create pull request + uses: repo-sync/pull-request@v2.12.1 + id: create-pr + with: + github_token: ${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }} + source_branch: cli-release-v${{ github.event.inputs.release_version }} + pr_title: "CLI release v${{ github.event.inputs.release_version }}" + pr_body: "To be merged after the release is complete." + pr_label: "PR: Merge After Release" + + - name: Draft GitHub release + uses: actions/create-release@v1.1.4 + id: create-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.event.inputs.release_version }}-cli + release_name: jib-cli v${{ github.event.inputs.release_version }} + draft: true + body: | + ### Major Changes + + See [CHANGELOG.md](https://github.com/GoogleContainerTools/jib/blob/master/jib-cli/CHANGELOG.md) for more details. + + - name: Upload Jib CLI + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: ./jib-cli/build/distributions/jib-jre-${{ github.event.inputs.release_version }}.zip + asset_name: jib-jre-${{ github.event.inputs.release_version }}.zip + asset_content_type: application/zip + + - name: Upload Jib CLI checksum + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: ./jib-cli/build/distributions/zip.sha256 + asset_name: jib-jre-${{ github.event.inputs.release_version }}.zip.sha256 + asset_content_type: text/plain + + - name: Create Jib CLI release checklist issue + uses: JasonEtco/create-an-issue@v2.9.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NAME: v${{ github.event.inputs.release_version }}-cli + CHANGELOG_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-cli/CHANGELOG.md + README_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-cli/README.md + GCS_UPDATE_SCRIPT: "`./jib-cli/scripts/update_gcs_latest.sh ${{ github.event.inputs.release_version }}`" + RELEASE_DRAFT: ${{ steps.create-release.outputs.html_url }} + RELEASE_PR: ${{steps.create-pr.outputs.pr_url}} + with: + filename: .github/RELEASE_TEMPLATES/cli_release_checklist.md + + provenance: + needs: [release] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 + with: + base64-subjects: "${{ needs.release.outputs.hashes }}" + + upload: + needs: [release, provenance] + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Download attestation + uses: actions/download-artifact@v3 + with: + name: "${{ needs.provenance.outputs.attestation-name }}" + + - uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.release.outputs.upload_url }} + asset_path: "${{ needs.provenance.outputs.attestation-name }}" + asset_name: "${{ needs.provenance.outputs.attestation-name }}" + asset_content_type: application/json diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000000..4633439026 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,144 @@ +name: Prepare Jib release +on: + workflow_dispatch: + inputs: + project: + description: Jib project to release + required: true + default: (build-plan | core | maven | gradle | extension-common | maven-extension | gradle-extension) + release_version: + description: new release version + required: true + default: (for example, 0.1.0) + +permissions: + contents: write + issues: write + +jobs: + release: + name: Prepare Jib release + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up JDK 8 + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + - name: Check input + run: | + echo '* input project: "${GITHUB_EVENT_INPUTS_PROJECT}"' + + case ${GITHUB_EVENT_INPUTS_PROJECT} in + build-plan|core|maven|gradle|extension-common|maven-extension|gradle-extension) ;; + *) echo 'invalid input project name "${GITHUB_EVENT_INPUTS_PROJECT}"' + exit 1 + ;; + esac + if [[ ! "${GITHUB_EVENT_INPUTS_RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo 'version "${GITHUB_EVENT_INPUTS_RELEASE_VERSION}" not in ###.###.### format' + exit 1 + fi + env: + GITHUB_EVENT_INPUTS_PROJECT: ${{ github.event.inputs.project }} + GITHUB_EVENT_INPUTS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} + + - name: Build project + run: | + # TODO: run integration test? (Requries auth with GCP.) + ./gradlew clean build --stacktrace + + - name: Run Gradle release + run: | + git checkout -b ${GITHUB_EVENT_INPUTS_PROJECT}-release-v${GITHUB_EVENT_INPUTS_RELEASE_VERSION} + git config user.email ${GITHUB_ACTOR}@users.noreply.github.com + git config user.name ${GITHUB_ACTOR} + + PROJECT=$( case ${GITHUB_EVENT_INPUTS_PROJECT} in + extension-common) echo jib-plugins-extension-common ;; + maven-extension) echo jib-maven-plugin-extension-api ;; + gradle-extension) echo jib-gradle-plugin-extension-api ;; + maven|gradle) echo jib-${GITHUB_EVENT_INPUTS_PROJECT}-plugin ;; + *) echo jib-${GITHUB_EVENT_INPUTS_PROJECT} ;; + esac ) + # This creates the tag (e.g., "v0.1.0-gradle") and pushes the updated + # branch (e.g., "gradle-release-v0.1.0") and the new tag. + ./gradlew "${PROJECT}":release \ + -Prelease.useAutomaticVersion=true \ + -Prelease.releaseVersion=${GITHUB_EVENT_INPUTS_RELEASE_VERSION} + env: + GITHUB_EVENT_INPUTS_PROJECT: ${{ github.event.inputs.project }} + GITHUB_EVENT_INPUTS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} + + - name: Create pull request + uses: repo-sync/pull-request@v2.12.1 + id: create-pr + with: + github_token: ${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }} + source_branch: ${{ github.event.inputs.project }}-release-v${{ github.event.inputs.release_version }} + pr_title: "${{ github.event.inputs.project }} release v${{ github.event.inputs.release_version }}" + pr_body: "To be merged after the release is complete." + pr_label: "PR: Merge After Release" + + - name: Draft Maven/Gradle GitHub release + uses: actions/create-release@v1.1.4 + id: create-plugin-release + if: ${{ github.event.inputs.project == 'maven' || github.event.inputs.project == 'gradle' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.event.inputs.release_version }}-${{ github.event.inputs.project }} + release_name: jib-${{ github.event.inputs.project }}-plugin v${{ github.event.inputs.release_version }} + draft: true + body: | + **Run `./jib-${{ github.event.inputs.project }}-plugin/scripts/update_gcs_latest.sh ${{ github.event.inputs.release_version }}` + when the release is complete to update the latest version string on GCS.** + + --- + ### Major Changes + + See [CHANGELOG.md](https://github.com/GoogleContainerTools/jib/blob/master/jib-${{ github.event.inputs.project }}-plugin/CHANGELOG.md) for more details. + + - name: Create Maven/Gradle release checklist issue + uses: JasonEtco/create-an-issue@v2.9.2 + if: ${{ github.event.inputs.project == 'maven' || github.event.inputs.project == 'gradle' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NAME: v${{ github.event.inputs.release_version }}-${{ github.event.inputs.project }} + CHANGELOG_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-${{ github.event.inputs.project }}-plugin/CHANGELOG.md + README_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-${{ github.event.inputs.project }}-plugin/README.md + GCS_UPDATE_SCRIPT: "`./jib-${{ github.event.inputs.project }}-plugin/scripts/update_gcs_latest.sh ${{ github.event.inputs.release_version }}`" + RELEASE_DRAFT: ${{ steps.create-plugin-release.outputs.html_url }} + RELEASE_PR: ${{steps.create-pr.outputs.pr_url}} + with: + filename: .github/RELEASE_TEMPLATES/plugin_release_checklist.md + + - name: Draft Core GitHub release + uses: actions/create-release@v1.1.4 + id: create-core-release + if: ${{ github.event.inputs.project == 'core' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.event.inputs.release_version }}-core + release_name: jib-core v${{ github.event.inputs.release_version }} + draft: true + body: | + ### Major Changes + + See [CHANGELOG.md](https://github.com/GoogleContainerTools/jib/blob/master/jib-core/CHANGELOG.md) for more details. + + - name: Create Core release checklist issue + uses: JasonEtco/create-an-issue@v2.9.2 + if: ${{ github.event.inputs.project == 'core' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NAME: v${{ github.event.inputs.release_version }}-core + CHANGELOG_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-core/CHANGELOG.md + README_URL: https://github.com/GoogleContainerTools/jib/blob/master/jib-core/README.md + RELEASE_DRAFT: ${{ steps.create-core-release.outputs.html_url }} + RELEASE_PR: ${{steps.create-pr.outputs.pr_url}} + with: + filename: .github/RELEASE_TEMPLATES/core_release_checklist.md diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000000..0cb03a0d4e --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,53 @@ +name: SonarCloud Analysis +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + schedule: + - cron: '00 6 * * *' # 06:00 UTC every day + +jobs: + sonar: + if: github.repository == 'GoogleContainerTools/jib' # Only run on upstream branch + name: Build with Sonar + runs-on: ubuntu-20.04 + steps: + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d' --utc)" >> $GITHUB_OUTPUT + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar-${{ steps.date.outputs.date }} + - uses: actions/cache@v4 + with: + path: | + ~/.m2/repository + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Test w/ coverage + continue-on-error: true + run: | + ./gradlew clean build jacocoTestReport --stacktrace + - name: Build and analyze + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + ./gradlew sonarqube --stacktrace diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..9d3d9eccf4 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,38 @@ +name: Unit Tests +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [8, 11] + env: + # for gradle + TERM: dumb + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: ${{ matrix.java }} + - uses: actions/cache@v4 + with: + path: | + ~/.m2/repository + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Run tests + run: | + ./gradlew clean build jacocoTestReport --stacktrace diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 898930fb52..5030f209bf --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,16 @@ -build -target +build/ +!jib-gradle-plugin/src/test/resources/gradle/application/build/ +target/ out +bin/ *.iml *.ipr *.iws .idea -.gradle -/.settings -/.classpath -/.project +.gradle/ +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +**/.mvn/wrapper/maven-wrapper.jar +.settings/ +.classpath +.project +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac78fdca5b..aad54536bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,18 @@ + +This project is currently stable, and we are primarily focused on critical bug fixes and platform evolution to ensure it continues to work for its supported use cases. + # Contributing to Jib -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. +Please follow the guidelines below before opening an issue or a PR: +1. Ensure the issue was not already reported. +2. Open a new issue if you are unable to find an existing issue addressing your problem. Make sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. +3. Discuss the priority and potential solutions with the maintainers in the issue. The maintainers would review the issue and add a label "Accepting Contributions" once the issue is ready for accepting contributions. +4. Open a PR only if the issue is labeled with "Accepting Contributions", ensure the PR description clearly describes the problem and solution. Note that an open PR without an issues labeled with "Accepting Contributions" will not be accepted. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; +Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. @@ -17,17 +23,141 @@ again. ## Code Reviews -1. Set your git user.email property to the address used for step 1. E.g. +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +Before submitting a pull request, please make sure to: + +- Identify an existing [issue](https://github.com/GoogleContainerTools/jib/issues) to associate + with your proposed change, or [file a new issue](https://github.com/GoogleContainerTools/jib/issues/new). +- Describe any implementation plans in the issue and wait for a review from the repository maintainers. + +### Typical Contribution Cycle + +1. Set your git user.email property to the address used for signing the CLA. E.g. ``` git config --global user.email "janedoe@google.com" ``` If you're a Googler or other corporate contributor, use your corporate email address here, not your personal address. 2. Fork the repository into your own Github account. +3. We follow our own [Java style guide](STYLE_GUIDE.md) that extends the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). 3. Please include unit tests (and integration tests if applicable) for all new code. -4. Make sure all existing tests pass. - * In `jib-core`, run `./gradlew clean goJF build integrationTest` - * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` - * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -Pintegration-tests` +4. Make sure all existing tests pass (but see the note below about integration tests). + * run `./gradlew clean goJF build integrationTest` 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! + +## Building Jib + +Jib comes as 3 public components: + +- `jib-core`: a library for building containers +- `jib-maven-plugin`: a Maven plugin that uses `jib-core` and `jib-plugins-common` +- `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` and `jib-plugins-common` + +And 1 internal component: + +- `jib-plugins-common`: a library with helpers for maven/gradle plugins + +The project is configured as a single gradle build. Run `./gradlew build` to build the +whole project. Run `./gradlew install` to install all public components into the +local maven repository. + +### Integration Tests +**Note** that in order to run integration tests, you will need to set one of the +following environment variables: + + - If you are using a GCP project then set `JIB_INTEGRATION_TESTING_PROJECT` to the GCP project to use for testing; + the registry tested will be `gcr.io/`. + - Configure authentication to Container Registry by following these [steps](https://cloud.google.com/container-registry/docs/advanced-authentication). + - Enable the Google Container Registry API [here](https://console.cloud.google.com/apis/library/containerregistry.googleapis.com). + - If you're not using a GCP project then set `JIB_INTEGRATION_TESTING_LOCATION` to a specific registry for testing. (For example, you can run `docker run -d -p 9990:5000 registry:2` to set up a local registry and set the variable to `localhost:9990`.) + +You will also need Docker installed with the daemon running. Note that the +integration tests will create local registries on ports 5000 and 6000. + +To run select integration tests, use `--tests=`, see [gradle docs](https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/TestFilter.html) for `testPattern` examples. + +# Development Tips + +## Java version + +Use Java 8 or 11 for development. https://sdkman.io/ is a helpful tool to switch between Java versions. + +## Configuring Eclipse + +Although jib is a mix of Gradle and Maven projects, we build everything using one +unified gradle build. There is special code to include some projects directly as +source, but importing your project should be pretty straight forward. + + 1. Ensure you have installed the Gradle tooling for Eclipse, called + _Buildship_ (available from [the Eclipse + Marketplace](https://marketplace.eclipse.org/content/buildship-gradle-integration)). + 1. **Import the Gradle project:** Buildship does [not yet support + Eclipse Smart Import](https://github.com/eclipse/buildship/issues/356). + Use _File → Import → Gradle → Existing Gradle Project_ + and import `jib`. + +Note that you will likely need to re-apply these changes whenever +you refresh or update these projects. + +## Debugging the Jib Maven Plugin (`jib-maven-plugin`) + +### Build and use a local snapshot + +To use a local build of the `jib-maven-plugin`: + + 1. Build and install `jib-maven-plugin` into your local `~/.m2/repository` + with `./gradlew jib-maven-plugin:install`; + 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` + version of the `com.google.cloud.tools.jib` plugin. + +If developing from within Eclipse with M2Eclipse (the Maven tooling for Eclipse): + + 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` + version of the `com.google.cloud.tools.jib` plugin. + 1. Create and launch a _Maven Build_ launch configuration for the + test project, and ensure the _Resolve Workspace artifacts_ is checked. + +### Attaching a debugger + +Run `mvnDebug jib:build` and attach to port 8000. + +If developing with Eclipse and M2Eclipse (the Maven tooling for Eclipse), just launch the _Maven Build_ with _Debug_. + +## Debugging the Jib Gradle Plugin (`jib-gradle-plugin`) + +### Build and use a local snapshot + +To use a local build of the `jib-gradle-plugin`: + + 1. Build and install `jib-gradle-plugin` into your local `~/.m2/repository` + with `./gradlew jib-gradle-plugin:install` + 1. Add a `pluginManagement` block to your test project's `settings.gradle` to enable reading plugins from the local maven repository. It must be the first block in the file before any `include` directives. + ```groovy + pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } + } + ``` + 1. Modify your test project's `build.gradle` to use the [latest snapshot version](jib-gradle-plugin/gradle.properties) + ```groovy + plugins { + // id 'com.google.cloud.tools.jib' version 'major.minor.patch' + id 'com.google.cloud.tools.jib' version 'major.minor.patch-SNAPSHOT' + } + + ``` + +### Attaching a debugger + +Attach a debugger to a Gradle instance by running Gradle as follows: + +```shell +./gradlew jib \ + --no-daemon \ + -Dorg.gradle.jvmargs='-agentlib:jdwp:transport=dt_socket,server=y,address=5005,suspend=y' +``` diff --git a/README.md b/README.md index ad7658b7a3..e6633f53e4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,32 @@ -[![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.4-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) +[![Maven Central](https://img.shields.io/maven-central/v/com.google.cloud.tools/jib-maven-plugin)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) +[![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-ubuntu-master-orb.svg) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-windows-master-orb.svg) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-macos-master-orb.svg) +[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib Jib - Containerize your Java applications. +| ☑️ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + ## What is Jib? -Jib builds Docker and OCI images for your Java applications and is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin). +Jib builds optimized Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications without a Docker daemon - and without deep mastery of Docker best-practices. It is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin) and as a Java library. + +- [Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin). +- [Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin). +- [Jib Core](jib-core): A general-purpose container-building library for Java. +- [Jib CLI](jib-cli): A command-line interface for building images that uses Jib Core. -[Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin).\ -[Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin). +Jib works well with Google Cloud Build. For details, see [how to use Jib on Google Cloud Build](docs/google-cloud-build.md). -*Jib as a container-building library for Java is work-in-progress. Watch for updates.* +For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). ## Goals @@ -27,25 +38,58 @@ Jib builds Docker and OCI images for your Java applications and is available as ## Quickstart -### Maven +* **Maven** - See the jib-maven-plugin [Quickstart](jib-maven-plugin#quickstart). + +* **Gradle** - See the jib-gradle-plugin [Quickstart](jib-gradle-plugin#quickstart). -See documentation for using [jib-maven-plugin](jib-maven-plugin#quickstart). +* **Jib Core** - See the Jib Core [Quickstart](jib-core#adding-jib-core-to-your-build). -### Gradle +* **Jib CLI** - See the Jib CLI [doc](jib-cli). -See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). +## Examples + +The [examples](examples) directory includes the following examples (and more). + * [helloworld](examples/helloworld) + * [Spring Boot](examples/spring-boot) + * [Micronaut](examples/micronaut) + * [Multi-module project](examples/multi-module) + * [Spark Java using Java Agent](examples/java-agent) ## How Jib Works -Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. +Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of an [OpenJDK base image](docs/default_base_image.md), but you can also configure a custom base image. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). -## Frequently Asked Questions (FAQ) +## Need Help? + +A lot of questions are already answered! + +* [Frequently Asked Questions (FAQ)](docs/faq.md) +* [Stack Overflow](https://stackoverflow.com/questions/tagged/jib) +* [GitHub issues](https://stackoverflow.com/questions/tagged/jib) + +_For usage questions, please ask them on Stack Overflow._ + +## Privacy + +See the [Privacy page](docs/privacy.md). -See the [Frequently Asked Questions (FAQ) wiki page](/../../wiki/Frequently-Asked-Questions-(FAQ)). +## Get involved with the community -## Community +We welcome contributions! Here's how you can contribute: +* [Browse issues](https://github.com/GoogleContainerTools/jib/issues) or [file an issue](https://github.com/GoogleContainerTools/jib/issues/new) * Chat with us on [gitter](https://gitter.im/google/jib) -* [jib-users mailing list](https://groups.google.com/forum/#!forum/jib-users) +* Join the [jib-users mailing list](https://groups.google.com/forum/#!forum/jib-users) +* Contribute: + * *Read the [contributing guide](https://github.com/GoogleContainerTools/jib/blob/master/CONTRIBUTING.md) before starting work on an issue* + * Try to fix [good first issues](https://github.com/GoogleContainerTools/jib/labels/good%20first%20issue) + * Help out on [issues that need help](https://github.com/GoogleContainerTools/jib/labels/kind%2Fquestion) + * Join in on [discussion issues](https://github.com/GoogleContainerTools/jib/labels/discuss) + +*Make sure to follow the [Code of Conduct](https://github.com/GoogleContainerTools/jib/blob/master/CODE_OF_CONDUCT.md) when contributing so we can foster an open and welcoming community.* + +## Disclaimer + +This is not an officially supported Google product. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..09a1522637 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| jib-maven-plugin v3.x | :heavy_check_mark: | +| jib-gradle-plugin v3.x | :heavy_check_mark: | +| jib-core v0.x | :heavy_check_mark: | +| jib-cli v0.x | :heavy_check_mark: | + + +## Reporting a Vulnerability + +To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). +We use g.co/vulnz for our intake, and do coordination and disclosure here on +GitHub (including using GitHub Security Advisory). The Google Security Team will +respond within 5 working days of your report on g.co/vulnz. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000000..f5e28467ef --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,50 @@ +# Style guide + +This style guide defines specific coding standards and advice for this Java codebase. The rules here are extensions to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). + +Please see the [contributing guide](CONTRIBUTING.md) for general guidance for contributing to this project. + +### Automatic formatting + +Automatic formatting should be performed with `./gradlew goJF` or `./mvnw fmt:format`. Formatting all projects can be done with `./build.sh format`. + +### Class member order + +*Extends [3.4.2](https://google.github.io/styleguide/javaguide.html#s3.4.2-ordering-class-contents)* + +Class members should be in the following order, in decreasing priority: + +1. Static before non-static +1. Nested classes/interfaces before fields before constructors before methods +1. Public before private +1. Final before non-final + +### Public APIs + +User-facing methods (such as those in Jib Core) should not have types in their signature that are not standard JDK classes. For example, a parameter should take type `List` rather than Guava's `ImmutableList`. + +Jib Core's formal API should not expose internal Jib types. In other words, public classes in the `com.google.cloud.tools.jib.api` package should not contain any public methods that have internal types (Jib classes outside of the `api` package) in the method signature. This includes return types, parameters, thrown types, and javadoc links on public methods. + +### Package hierarchy + +Packages should depend on each other without cycles. + +The following is a list of current `jib-core` packages (under `com.google.cloud.tools.jib`) and their immediate dependencies. These can be amended as code changes, but there should not be cyclical dependencies. + +- `api` +- `async` +- `blob` - `filesystem`, `hash`, `image` (cycle - should fix) +- `builder` - `async`, `blob`, `builder`, `cache` `configuration`, `docker`, `event`, `filesystem`, `global`, `http`, `image`, `json`, `registry` +- `cache` - `blob`, `filesystem`, `hash`, `image`, `json` +- `configuration` - `cache`, `filesystem`, `event`, `image`, `registry` +- `docker` - `blob`, `cache`, `image`, `json`, `tar` +- `event` +- `filesystem` +- `frontend` - `configuration`, `event`, `filesystem`, `image`, `registry` +- `global` +- `hash` - `blob`, `image` +- `http` - `blob` +- `image` - `blob`, `configuration` (cycle - should fix - `ImageToJsonTranslator`), `filesystem`, `json`, `tar` +- `json` - `blob` +- `registry` - `blob`, `builder` (cycle - should fix - `RegistryClient`), `configuration` (cycle - should fix - `DockerConfigCredentialRetriever`), `event`, `global`, `http`, `image`, `json` +- `tar` - `blob` diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..fadc71dd8c --- /dev/null +++ b/build.gradle @@ -0,0 +1,457 @@ +// define all versioned plugins here and apply in subprojects as necessary without version +plugins { + id 'com.github.sherter.google-java-format' version '0.9' apply false + id 'net.ltgt.errorprone' version '3.1.0' apply false + id 'net.researchgate.release' version '2.8.1' apply false + id 'com.gradle.plugin-publish' version '1.2.0' apply false + id 'io.freefair.maven-plugin' version '5.3.3.3' apply false + + // apply so that we can collect quality metrics at the root project level + id 'org.sonarqube' version '4.0.0.2929' +} + +/* PROJECT DEPENDENCY VERSIONS */ +// define all common versioned dependencies here +project.ext.dependencyStrings = [ + // For Google libraries, check the following boms for best compatibility. + // - https://github.com/googleapis/java-shared-dependencies + // - https://github.com/googleapis/java-cloud-bom + GOOGLE_HTTP_CLIENT: 'com.google.http-client:google-http-client:1.42.2', + GOOGLE_HTTP_CLIENT_APACHE_V2: 'com.google.http-client:google-http-client-apache-v2:1.42.2', + GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: 'com.google.auth:google-auth-library-oauth2-http:1.10.0', + GUAVA: 'com.google.guava:guava:32.1.2-jre', + JSR305: 'com.google.code.findbugs:jsr305:3.0.2', // transitively pulled in by GUAVA + + // for Build Plan and Jib Plugins Extension API + BUILD_PLAN: 'com.google.cloud.tools:jib-build-plan:0.4.0', + EXTENSION_COMMON: 'com.google.cloud.tools:jib-plugins-extension-common:0.2.0', + GRADLE_EXTENSION: 'com.google.cloud.tools:jib-gradle-plugin-extension-api:0.4.0', + MAVEN_EXTENSION: 'com.google.cloud.tools:jib-maven-plugin-extension-api:0.4.0', + + COMMONS_COMPRESS: 'org.apache.commons:commons-compress:1.26.0', + ZSTD_JNI: 'com.github.luben:zstd-jni:1.5.5-5', + COMMONS_TEXT: 'org.apache.commons:commons-text:1.10.0', + JACKSON_BOM: 'com.fasterxml.jackson:jackson-bom:2.21.1', + JACKSON_DATABIND: 'com.fasterxml.jackson.core:jackson-databind', + JACKSON_DATAFORMAT_YAML: 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml', + JACKSON_DATATYPE_JSR310: 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310', + ASM: 'org.ow2.asm:asm:9.9', + PICOCLI: 'info.picocli:picocli:4.7.4', + + MAVEN_API: 'org.apache.maven:maven-plugin-api:3.9.3', + MAVEN_CORE: 'org.apache.maven:maven-core:3.9.3', + MAVEN_COMPAT: 'org.apache.maven:maven-compat:3.9.6', + MAVEN_PLUGIN_ANNOTATIONS: 'org.apache.maven.plugin-tools:maven-plugin-annotations:3.9.0', + + //test + TRUTH: 'com.google.truth:truth:1.1.5', + TRUTH8: 'com.google.truth.extensions:truth-java8-extension:1.1.5', // should match TRUTH version + JUNIT: 'junit:junit:4.13.2', + JUNIT_PARAMS: 'pl.pragmatists:JUnitParams:1.1.1', + MAVEN_TESTING_HARNESS: 'org.apache.maven.plugin-testing:maven-plugin-testing-harness:3.3.0', + MAVEN_VERIFIER: 'org.apache.maven.shared:maven-verifier:1.8.0', + MOCKITO_CORE: 'org.mockito:mockito-core:4.11.0', + SISU_PLEXUS: 'org.eclipse.sisu:org.eclipse.sisu.plexus:0.3.5', + SLF4J_API: 'org.slf4j:slf4j-api:2.0.7', + SLF4J_SIMPLE: 'org.slf4j:slf4j-simple:2.0.9', + SYSTEM_RULES: 'com.github.stefanbirkner:system-rules:1.19.0', + JBCRYPT: 'org.mindrot:jbcrypt:0.4', +] + +import net.ltgt.gradle.errorprone.CheckSeverity + +// `java-library` must be applied before `java`. +// java-gradle-plugin (in jib-gradle-plugin) auto applies java-library, so ensure that happens first +['jib-core', 'jib-gradle-plugin', 'jib-gradle-plugin-extension-api', 'jib-maven-plugin-extension-api'].each { projectName -> + project(projectName).apply plugin: 'java-library' +} + +subprojects { + group 'com.google.cloud.tools' + + repositories { + mavenCentral() + } + + apply plugin: 'java' + apply plugin: 'checkstyle' + apply plugin: 'com.github.sherter.google-java-format' + apply plugin: 'net.ltgt.errorprone' + apply plugin: 'jacoco' + + // Guava update breaks unit tests. Workaround mentioned in https://github.com/google/guava/issues/6612#issuecomment-1614992368. + sourceSets.all { + configurations.getByName(runtimeClasspathConfigurationName) { + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") + } + configurations.getByName(compileClasspathConfigurationName) { + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") + } + } + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + compileJava.options.encoding = 'UTF-8' + compileJava.options.compilerArgs += [ '-Xlint:deprecation' ] + compileTestJava.options.compilerArgs += [ '-Xlint:deprecation' ] + + // Use this to ensure we correctly override transitive dependencies + // TODO: There might be a plugin that does this + task ensureTransitiveDependencyOverrides { + def dependenciesList = [dependencyStrings.GOOGLE_HTTP_CLIENT, dependencyStrings.GOOGLE_HTTP_CLIENT_APACHE_V2] + def rules = dependenciesList.collectEntries{[/*name*/ it.split(':')[1], /*version*/ it.split(':')[2]]} + doLast { + configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + def dependency = artifact.moduleVersion.id + if (rules[dependency.name] && rules[dependency.name] != dependency.version) { + throw new GradleException( + dependency.name + ' version error in ' + project + + ', expected:' + rules[dependency.name] + + ', found:' + dependency.version); + } + } + } + } + compileJava.dependsOn ensureTransitiveDependencyOverrides + /* PROJECT DEPENDENCY VERSIONS */ + + /* ERROR PRONE */ + dependencies { + // NullAway errorprone plugin + annotationProcessor 'com.uber.nullaway:nullaway:0.10.7' + errorprone 'com.google.errorprone:error_prone_core:2.10.0' + // Using github.com/google/error-prone-javac is required when running on + // JDK 8. Remove when migrating to JDK 11. + if (System.getProperty('java.version').startsWith('1.8.')) { + errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') + } + } + + // Adds NullAway errorprone checks. + tasks.withType(JavaCompile) { + if (!name.toLowerCase().contains('test')) { + options.errorprone { + check('NullAway', CheckSeverity.ERROR) + option('NullAway:ExcludedFieldAnnotations', 'org.apache.maven.plugins.annotations.Component') + option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') + } + } + } + /* ERROR PRONE */ + + /* GOOGLE JAVA FORMAT */ + googleJavaFormat { + toolVersion = '1.7' + } + check.dependsOn verifyGoogleJavaFormat + /* GOOGLE JAVA FORMAT */ + + /* CHECKSTYLE */ + checkstyle { + toolVersion = '8.29' + + // use google checks from the jar + def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() + + // set the location of the suppressions file referenced in google_checks.xml + configProperties['org.checkstyle.google.suppressionfilter.config'] = getConfigDirectory().file('checkstyle-suppressions.xml').get().toString() + + // add in copyright header check on only java files (replace the last in file) + def copyrightChecks = ''' + + + + + + + ''' + googleChecks = googleChecks.substring(0, googleChecks.lastIndexOf('')) + copyrightChecks + + // this is the actual checkstyle config + config = resources.text.fromString(googleChecks) + + maxErrors = 0 + maxWarnings = 0 + } + /* CHECKSTYLE */ + + /* TEST CONFIG */ + tasks.withType(Test).configureEach { + reports.html.outputLocation = file("${reporting.baseDir}/${name}") + } + + test { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } + } + // jar to export tests classes for import in other project by doing: + // testCompile project(path:':project-name', configuration:'tests') + task testJar(type: Jar) { + from sourceSets.test.output.classesDirs + archiveClassifier = 'tests' + } + // to import resources do: sourceSets.test.resources.srcDirs project(':project-name').sourceSets.test.resources + + configurations { + tests + } + + artifacts { + tests testJar + } + /* TEST CONFIG */ + + /* INTEGRATION TESTS */ + sourceSets { + integrationTest { + java.srcDir file('src/integration-test/java') + resources.srcDir file('src/integration-test/resources') + compileClasspath += sourceSets.main.output + sourceSets.test.output + runtimeClasspath += sourceSets.main.output + sourceSets.test.output + } + } + + configurations { + integrationTestImplementation.extendsFrom testImplementation + integrationTestImplementation.setCanBeResolved(true) + integrationTestRuntime.extendsFrom testRuntime + } + + // Integration tests must be run explicitly + task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperty '_JIB_DISABLE_USER_AGENT', true + } + + + task integrationTestJar(type: Jar) { + from sourceSets.integrationTest.output.classesDirs + archiveClassifier = 'integration-tests' + } + + configurations { + integrationTests + } + + artifacts { + integrationTests integrationTestJar + } + + integrationTest { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } + } + /* INTEGRATION TESTS */ + + /* JAVADOC ENFORCEMENT */ + // Fail build on javadoc warnings + tasks.withType(Javadoc) { + options.addBooleanOption('Xwerror', true) + } + assemble.dependsOn javadoc + /* JAVADOC ENFORCEMENT */ + + /* JAR */ + jar { + manifest { + attributes 'Implementation-Title': project.name, + 'Implementation-Version': archiveVersion, + 'Built-By': System.getProperty('user.name'), + 'Built-Date': new Date(), + 'Built-JDK': System.getProperty('java.version'), + 'Built-Gradle': gradle.gradleVersion + } + } + normalization { + runtimeClasspath { + metaInf { + ignoreAttribute("Built-By") + ignoreAttribute("Built-Date") + } + } + } + /* JAR */ + + /* MAVEN CENTRAL RELEASES */ + // for projects that release to maven central + project.ext.configureMavenRelease = { + apply plugin: 'maven-publish' + task sourceJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = 'sources' + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + from javadoc.destinationDir + archiveClassifier = 'javadoc' + } + + publishing { + publications { + mavenJava(MavenPublication) { + pom { + // to be filled by subproject after calling configure configureMavenRelease + // name = '' + // description = '' + + url = 'https://github.com/GoogleContainerTools/jib' + inceptionYear = '2018' + + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + developers { + developer { + id = 'chanseokoh' + name = 'Chanseok Oh' + email = 'chanseok@google.com' + } + developer { + id = 'loosebazooka' + name = 'Appu Goundan' + email = 'appu@google.com' + } + developer { + id = 'TadCordle' + name = 'Tad Cordle' + email = 'tcordle@google.com' + } + developer { + id = 'briandealwis' + name = 'Brian de Alwis' + email = 'bdealwis@google.com' + } + developer { + id = 'coollog' + name = 'Qingyang Chen' + } + } + scm { + url = 'https://github.com/GoogleContainerTools/jib' + connection = 'scm:https://github.com/GoogleContainerTools/jib.git' + developerConnection = 'scm:git://github.com/GoogleContainerTools/jib.git' + } + } + } + } + } + generatePomFileForMavenJavaPublication { + destination = file("${project.buildDir}/pom/${project.name}-${project.version}.pom") + } + // define a special install task that handles installing locally for manual testing + task install { + dependsOn publishToMavenLocal + } + + // For kokoro sign and release to maven central + task prepareRelease(type: Copy) { + from jar + from sourceJar + from javadocJar + from generatePomFileForMavenJavaPublication + into "${project.buildDir}/release-artifacts" + dependsOn build + dependsOn cleanPrepareRelease + } + } + /* MAVEN CENTRAL RELEASE */ + + /* INCLUDED PROJECT DEPENDENCY HELPER */ + // to keep track of all source projects + project.ext.sourceProjects = [] + // sourceProject(Project) accepts a project and adds it as a dependency in a special manner: + // 1. force evaluation of the project first + // 2. add the project classes as "compileOnly" and make it available to tests in "testImplementation" + // 3. add the project's dependencies as "implementation" + // 4. remove any transitive reference of any sourceProject depenency that may have appeared + // 5. add the project's classes to the final jar + // Other nice effects (vs shadowJar) + // 1. Generated poms will be correct + // 2. Configuration is isolated to this single "sourceProject" call + // 3. These configurations are compliant with IDEs + project.ext.sourceProject = { Project dependencyProject -> + // make sure those projects are evaluated first so we know their dependencies + project.evaluationDependsOn dependencyProject.path + // add the sourceProjecect dependency + def dependencyProjectClasses = dependencyProject.sourceSets.main.output + dependencies { + // add the dependencyProject classes as compileOnly, make it available to tests + compileOnly(dependencyProject) { transitive = false } + testImplementation dependencyProjectClasses + // add dependencyProject's dependencies as implementation dependencies + implementation dependencyProject.configurations.implementation.dependencies + if (dependencyProject.configurations.hasProperty('api')) { + implementation dependencyProject.configurations.api.dependencies + } + } + // keep track of all dependencyProjects for removal + sourceProjects += dependencyProject + // if we find any project dependencies that were brought in transitively, go remove them + project.configurations.implementation.dependencies.removeAll { d -> + return d instanceof ProjectDependency && sourceProjects.contains(d.dependencyProject) + } + // adds dependencyProject's classes to jar (fat jar-esque) + jar { + from dependencyProjectClasses + } + // also configure the java-gradle-plugin if necessary + if (project.hasProperty('gradlePlugin')) { + project.tasks.pluginUnderTestMetadata.pluginClasspath.from dependencyProjectClasses + } + } + + // ensure no dependencies in the implementation group are project dependencies + project.ext.ensureNoProjectDependencies = { + project.afterEvaluate { + project.configurations.implementation.dependencies.each { dependency -> + if (dependency instanceof ProjectDependency) { + throw new GradleException('disallowed project dependency:' + dependency + ', in project:' + project); + } + } + } + } + + /* TEST COVERAGE */ + jacocoTestReport { + reports { + xml.required = true + html.required = false + } + } + /* TEST COVERAGE */ + + /* INCLUDED PROJECT DEPENDENCY HELPER */ + + /* LOCAL DEVELOPMENT HELPER TASKS */ + tasks.register('dev') { + classes.dependsOn tasks.googleJavaFormat + dependsOn check + dependsOn javadoc + } + + tasks.register('devFull') { + dependsOn dev + dependsOn integrationTest + } + /* LOCAL DEVELOPMENT HELPER TASKS */ +} + +/* SONARQUBE */ +sonarqube { + properties { + property 'sonar.projectName', 'jib' + property 'sonar.projectKey', 'GoogleContainerTools_jib' + property 'sonar.host.url', 'https://sonarcloud.io' + property 'sonar.organization', 'googlecontainertools-1' + } +} +/* SONARQUBE */ diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 0000000000..cc57c5a807 --- /dev/null +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jib-maven-plugin/config/copyright.header b/config/checkstyle/copyright-java.header similarity index 88% rename from jib-maven-plugin/config/copyright.header rename to config/checkstyle/copyright-java.header index bd7ddacfd4..e60211cae3 100644 --- a/jib-maven-plugin/config/copyright.header +++ b/config/checkstyle/copyright-java.header @@ -1,9 +1,9 @@ ^/\*$ -^ \* Copyright 2018 Google LLC\. All rights reserved\.$ +^ \* Copyright 20(17|18|19|20|21|22|23|24) Google LLC\.$ ^ \*$ ^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$ ^ \* use this file except in compliance with the License\. You may obtain a copy of$ -^ \* the License at$ +^ \* the License at ^ \*$ ^ \* http://www\.apache\.org/licenses/LICENSE-2\.0$ ^ \*$ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index d72cead04e..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/docs)](https://github.com/igrigorik/ga-beacon) - -These docs generate [googlecontainertools.github.io/jib](https://googlecontainertools.github.io/jib/). diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 2b14561636..0000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,5 +0,0 @@ -theme: jekyll-theme-slate - -# Slate theme-specific configuration. -# See https://github.com/pages-themes/slate -google_analytics: UA-121724379-1 diff --git a/docs/configure-gcp-credentials.md b/docs/configure-gcp-credentials.md index 91e7dfa467..22c2b47553 100644 --- a/docs/configure-gcp-credentials.md +++ b/docs/configure-gcp-credentials.md @@ -1,6 +1,6 @@ # Configuring Credentials for [Google Container Registry (GCR)](https://cloud.google.com/container-registry/) -There are a few ways of supplying Jib with the credentials to push and pull images from your private GCR regsitry. +There are a few ways of supplying Jib with the credentials to push and pull images from your private GCR registry. ## Using the Docker credential helper diff --git a/docs/default_base_image.md b/docs/default_base_image.md new file mode 100644 index 0000000000..4d98a4c57b --- /dev/null +++ b/docs/default_base_image.md @@ -0,0 +1,84 @@ +# Default Base Images in Jib + +## Jib Build Plugins 3+ + +Starting from version 3.2, the default base image is the official [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin) image on Docker Hub. Note that Eclipse Temurin by Adoptium is the [successor of AdoptOpenJDK](https://blog.adoptopenjdk.net/2021/08/goodbye-adoptopenjdk-hello-adoptium/). (For versions 3.0 and 3.1, the default is the official [`adoptopenjdk`](https://hub.docker.com/_/adoptopenjdk) image.) + +For WAR projects, the default is the official [`jetty`](https://hub.docker.com/_/jetty) image on Docker Hub. + +Note that Jib's default choice for Temurin, AdoptOpenJDK, and Jetty does not imply any endorsement to them. In fact, for strong reproducibility (which also results in better performance and efficiency), we always recommend configuring [`jib.from.image`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#from-closure) (Gradle) or [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#from-object) (Maven) to pin to a specific base image using a digest (or at least a tag). And while doing so, you should do your due diligence to figure out which base image will work best for you. + +### Docker Hub Download Rate Limit + +Docker Hub enforces download rate limit. Because Jib pulls base images from Docker Hub, you can occasionally lead to the following error: +``` + > com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException: 429 Too Many Requests + GET https://registry-1.docker.io/v2/library/adoptopenjdk/manifests/sha256:9db4a57b38b6d928761ec9c5a250677d306095df0f6a6bdd8936628e033b9f1a + { + "errors": [ + { + "code": "TOOMANYREQUESTS", + "message": "You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit" + } + ] + } +``` +Note that, even after Jib fully cached a base image, Jib still connects to Docker Hub to check the image every time it runs (unless you pin to a specific base image using a SHA digest). This is to check if the cached base image is up-to-date. + +Some options: +* Configure a registry mirror. +* Prevent Jib from accessing Docker Hub (after Jib cached a base image locally). + - Pin to a specific base image using a SHA digest (for example, `jib.from.image='eclipse-temurin:11-jre@sha256:...'`). + - Do offline building. + - Read a base from a local Docker daemon. + - Set up a local registry, store a base image, and read it from the local registry. +* Retry with increasing backoffs. + +See this [FAQ](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-am-hitting-docker-hub-rate-limits-how-can-i-configure-registry-mirrors) for more details about the options. + +### Migration from Pre-3.0 + +Prior to 3.0, Jib Maven and Gradle plugins built images based on Distroless Java when the user didn't make an explicit choice. The Jib team carefully assessed all the surrounding factors and ultimately decided to switch from Distroless for the best interest of the Jib users. That is, to meet one of the core missions of Jib to build a secure and optimized image. + +If you are already setting a specific base image, there will be absolutely no difference when you upgrade to 3.0. It will continue to use the base image you specified. Even if you are not specifying a base image, most likely upgrading to 3.0 will keep working fine, because it's just about getting a JRE from a different image. (But if you are not setting a base image, as we explained in the previous section, we highly recommend configuring it.) + +For some reason if you have to keep the exact same behavior when using 3.0, you can always specify the Distroless Java as a base image. + +* non-WAR projects + ```groovy + jib { + from.image = 'gcr.io/distroless/java:11' // or ":8" + ... + } + ``` + ```xml + + gcr.io/distroless/java:11 + + ``` + However, even when you decide to keep using Distroless, at least we strongly recommend `gcr.io/distroless/java-debian10:11`, because, as of Apr 2021, `gcr.io/distroless/java:{8,11}` is based on Debian 9 that reached end-of-life. (Note `gcr.io/distroless/java-debian10` doesn't have `:8`.) + +* WAR projects + ```gradle + jib { + from.image = 'gcr.io/distroless/java/jetty:java11' // or ":java8" + container { + entrypoint = 'INHERIT' + appRoot = '/jetty/webapps/ROOT' + } + ... + } + ``` + ```xml + + gcr.io/distroless/java/jetty:java11 + + INHERIT + /jetty/webapps/ROOT + + + ``` + +## Jib CLI + +For the JAR mode, Jib CLI versions prior to 0.8 have always used AdoptOpenJDK. Starting with 0.8, the tool uses [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin). The WAR mode uses `jetty`. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..273854ead9 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,910 @@ +## Frequently Asked Questions (FAQ) + +If a question you have is not answered below, please [submit an issue](/../../issues/new). + +| ☑️ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + +[But, I'm not a Java developer.](#but-im-not-a-java-developer)\ +[My build process doesn't let me integrate with the Jib Maven or Gradle plugin](#my-build-process-doesnt-let-me-integrate-with-the-jib-maven-or-gradle-plugin)\ +[How do I run the image I built?](#how-do-i-run-the-image-i-built)\ +[Where is bash?](#where-is-bash)\ +[What image format does Jib use?](#what-image-format-does-jib-use)\ +[Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ +[Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ +[How are Jib applications layered?](#how-are-jib-applications-layered)\ +[Can I learn more about container images?](#can-i-learn-more-about-container-images)\ +[Which base image (JDK) does Jib use?](#which-base-image-jdk-does-jib-use) + +**How-Tos**\ +[How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ +[Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint-at-runtime)\ +[I want to containerize a JAR.](#i-want-to-containerize-a-jar)\ +[I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ +[Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ +[I need to add files generated during the build process to a custom directory on the image.](#i-need-to-add-files-generated-during-the-build-process-to-a-custom-directory-on-the-image)\ +[Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ +[How do I enable debugging?](#how-do-i-enable-debugging)\ +[What would a Dockerfile for a Jib-built image look like?](#what-would-a-dockerfile-for-a-jib-built-image-look-like)\ +[How can I inspect the image Jib built?](#how-can-i-inspect-the-image-jib-built)\ +[I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ +[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp)\ +[How do I specify a platform in the manifest list (or OCI index) of a base image?](#how-do-i-specify-a-platform-in-the-manifest-list-or-oci-index-of-a-base-image)\ +[I want to exclude files from layers, have more fine-grained control over layers, change file ownership, etc.](#i-want-to-exclude-files-from-layers-have-more-fine-grained-control-over-layers-change-file-ownership-etc)\ +[Jib build plugins don't have the feature that I need.](#jib-build-plugins-dont-have-the-feature-that-i-need)\ +[I am hitting Docker Hub rate limits. How can I configure registry mirrors?](#i-am-hitting-docker-hub-rate-limits-how-can-i-configure-registry-mirrors)\ +[Where is the global Jib configuration file and how I can configure it?](#where-is-the-global-jib-configuration-file-and-how-i-can-configure-it) + +**Build Problems**\ +[How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ +[What should I do when the registry responds with Forbidden or DENIED?](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied)\ +[What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized)\ +[How do I configure a proxy?](#how-do-i-configure-a-proxy)\ +[How can I examine network traffic?](#how-can-i-examine-network-traffic)\ +[How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ +[I am seeing `Method Not Found` or `Class Not Found` errors when building.](#i-am-seeing-method-not-found-or-class-not-found-errors-when-building)\ +[I am seeing `Unsupported class file major version` when building.](#i-am-seeing-unsupported-class-file-major-version-when-building)\ +[I am seeing `NoClassDefFoundError: com/github/luben/zstd/ZstdOutputStream` when building.](#i-am-seeing-noclassdeffounderror-comgithublubenzstdzstdoutputstream-when-building) + +**Launch Problems**\ +[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ +[Why won't my container start?](#why-wont-my-container-start) + +**Jib CLI**\ +[How does the `jar` command support Standard JARs?](#how-does-the-jar-command-support-standard-jars)\ +[How does the `jar` command support Spring Boot JARs?](#how-does-the-jar-command-support-spring-boot-jars)\ +[How does the `war` command work?](#how-does-the-war-command-work) + +--- + +### But, I'm not a Java developer. + +Check out [Jib CLI](https://github.com/GoogleContainerTools/jib/tree/master/jib-cli), a general-purpose command-line tool for building containers images from filesystem content. + +Also see [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). The tool can build images for languages such as Python, NodeJS, Java, Scala, Groovy, C, Go, Rust, and D. + +### My build process doesn't let me integrate with the Jib Maven or Gradle plugin + +The [Jib CLI](https://github.com/GoogleContainerTools/jib/tree/master/jib-cli) can be useful for users with complex build workflows that make it hard to integrate the Jib Maven or Gradle plugin. It is a standalone application that is powered by [Jib Core](https://github.com/GoogleContainerTools/jib/tree/master/jib-core) and offers two commands: + +* [Build](https://github.com/GoogleContainerTools/jib/tree/master/jib-cli#build-command): Builds images from the filesystem content. + +* [Jar](https://github.com/GoogleContainerTools/jib/tree/master/jib-cli#jar-command): Examines your JAR and builds an image with optimized layers or containerizes the JAR as-is. + +Check out the [Jib CLI section](#jib-cli) of the FAQ for more information. + +### How do I run the image I built? + +If you built your image directly to the Docker daemon using `jib:dockerBuild` (Maven) or `jibDockerBuild` (Gradle), you simply need to use `docker run `. + +If you built your image to a registry using `jib:build` (Maven) or `jib` (Gradle), you will need to pull the image using `docker pull ` before using `docker run`. + +To run your image on Kubernetes, you can use kubectl: + +```shell +kubectl run jib-deployment --image= +``` + +For more information, see [steps 4-6 of the Kubernetes Engine deployment tutorial](https://cloud.google.com/kubernetes-engine/docs/tutorials/hello-app#step_4_create_a_container_cluster). + +### Where is bash? + +By default, Jib Maven and Gradle plugin versions prior to 3.0 used [`distroless/java`](https://github.com/GoogleContainerTools/distroless/tree/master/java) as the base image, which did not have a shell program (such as `sh`, `bash`, or `dash`). However, recent Jib tools use [default base images](default_base_image.md) that come with shell programs: Adoptium Eclipse Temurin (formerly AdoptOpenJDK) and Jetty for WAR projects. + +Note that you can always set a different base image. Jib's default choice for Temurin or AdoptOpenJDK does not imply any endorsement to it; you should do your due diligence to choose the right image that works best for you. Also note that the default base image is unpinned (the tag can point to different images over time), so we recommend configuring a base image with a SHA digest for strong reproducibility. + +* Configuring a base image in Maven + ```xml + + + openjdk:11-jre-slim@sha256:... + + + ``` + +* Configuring a base image in Gradle + ```groovy + jib.from.image = 'openjdk:11-jre-slim@sha256:...' + ``` + +* Configuring a base image in Jib CLI + ``` + $ jib jar --from openjdk:11-jre-slim@sha256:... --target ... app.jar + ``` + +### What image format does Jib use? + +Jib currently builds into the [Docker V2.2](https://docs.docker.com/registry/spec/manifest-v2-2/) image format or [OCI image format](https://github.com/opencontainers/image-spec). + +#### Maven + +See [Extended Usage](../jib-maven-plugin#extended-usage) for the `` configuration. + +#### Gradle + +See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.format` configuration. + +### Why is my image created 48+ years ago? + +For reproducibility purposes, Jib sets the creation time of the container images to the Unix epoch (00:00:00, January 1st, 1970 in UTC). If you would like to use a different timestamp, set the `jib.container.creationTime` / `` parameter to an ISO 8601 date-time. You may also use the value `USE_CURRENT_TIMESTAMP` to set the creation time to the actual build time, but this sacrifices reproducibility since the timestamp will change with every build. + +
+Setting creationTime parameter (click to expand) +

+ +#### Maven + +```xml + + + 2019-07-15T10:15:30+09:00 + + +``` + +#### Gradle + +```groovy +jib.container.creationTime = '2019-07-15T10:15:30+09:00' +``` + +

+
+ +Note that the modification time of the files in the built image put by Jib will still be 1 second past the epoch. The file modification time can be configured using [``](../jib-maven-plugin#container-object) (Maven) or [`jib.container.filesModificationTime`](../jib-gradle-plugin#container-closure) (Gradle). + +#### Please tell me more about reproducibility! + +_Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. + +To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.creationTime` to `USE_CURRENT_TIMESTAMP` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. + +For more details see [reproducible-builds.org](https://reproducible-builds.org). + +### Where is the application in the container filesystem? + +Jib packages your Java application into the following paths on the image: + +* `/app/libs/` contains all the dependency artifacts +* `/app/resources/` contains all the resource files +* `/app/classes/` contains all the classes files +* the contents of the extra directory (default `src/main/jib`) are placed relative to the container's root directory (`/`) + +### How are Jib applications layered? + +Jib makes use of [layering](https://containers.gitbook.io/build-containers-the-hard-way/#layers) to allow for fast rebuilds - it will only rebuild the layers containing files that changed since the previous build and will reuse cached layers containing files that didn't change. Jib organizes files in a way that groups frequently changing files separately from large, rarely changing files. For example, `SNAPSHOT` dependencies are placed in a separate layer from other dependencies, so that a frequently changing `SNAPSHOT` will not force the entire dependency layer to rebuild itself. + +Jib applications are split into the following layers: + +* All other dependencies +* Snapshot dependencies +* Project dependencies +* Resources +* Classes +* Each extra directory (`jib.extraDirectories` in Gradle, `` in Maven) builds to its own layer + +### Which base image (JDK) does Jib use? + +[`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin) by Adoptium (formerly [`adoptopenjdk`](https://hub.docker.com/_/adoptopenjdk)) and [`jetty`](https://hub.docker.com/_/jetty) (for WAR). See [Default Base Images in Jib](default_base_image.md) for details. + +### Can I learn more about container images? + +If you'd like to learn more about container images, [@coollog](https://github.com/coollog) has a guide: [Build Containers the Hard Way](https://containers.gitbook.io/build-containers-the-hard-way/), which takes a deep dive into everything involved in getting your code into a container and onto a container registry. + + +## Configuring Jib + +### How do I set parameters for my image at runtime? + +#### JVM Flags + +For the default base image, you can use the `JAVA_TOOL_OPTIONS` environment variable (note that other JRE images may require using other environment variables): + +Using Docker: `docker run -e "JAVA_TOOL_OPTIONS=" ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + env: + - name: JAVA_TOOL_OPTIONS + value: +``` +Note that many JVMs may only support a max length of **1024** characters for the `JAVA_TOOL_OPTIONS` environment variable, and anything longer than this may be cut off by the JVM. + +For Java 9+, often you may want to use [`JDK_JAVA_OPTIONS`](https://stackoverflow.com/questions/52986487/what-is-the-difference-between-jdk-java-options-and-java-tool-options-when-using) instead of `JAVA_TOOL_OPTIONS`. + +#### Other Environment Variables + +Using Docker: `docker run -e "NAME=VALUE" ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + env: + - name: NAME + value: VALUE +``` + +#### Arguments to Main + +Using Docker: `docker run ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + args: + - + - + - +``` + +For more information, see the [`JAVA_TOOL_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html), the [`docker run -e` reference](https://docs.docker.com/engine/reference/run/#env-environment-variables), and [defining environment variables for a container in Kubernetes](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/). + +### Can I define a custom entrypoint at runtime? + +Normally, the plugin sets a default entrypoint for java applications, or lets you configure a custom entrypoint using the `container.entrypoint` configuration parameter. You can also override the default/configured entrypoint by defining a custom entrypoint when running the container. See [`docker run --entrypoint` reference](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) for running the image with Docker and overriding the entrypoint command, or see [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. + +### I want to containerize a JAR. + +The intention of Jib is to add individual class files, resources, and dependency JARs into the container instead of putting a JAR. This lets Jib choose an opinionated, optimal layout for the application on the container image, which also allows it to skip the extra JAR-packaging step. + +However, you can set `packaged` (Maven) or `jib.containerizingMode = 'packaged'` (Gradle) to containerize a JAR, but note that your application will always be run via `java -cp ... your.MainClass` (even if it is an executable JAR). Some disadvantages of setting `containerizingMode='packaged'`: + +- You need to run the JAR-packaging step (`mvn package` in Maven or the `jar` task in Gradle). +- Reduced granularity in building and caching: if any of your Java source files or resource files are updated, not only the JAR has to be rebuilt, but the entire layer containing the JAR in the image has to be recreated and pushed to the destination. +- If it is a fat or shaded JAR embedding all dependency JARs, you are duplicating the dependency JARs in the image. Worse, it results in far more reduced granularity in building and caching, as dependency JARs can be huge and all of them need to be pushed repeatedly even if they do not change. + +Note that for runnable JARs/WARs, currently Jib does not natively support creating an image that runs a JAR (or WAR) through `java -jar runnable.jar` (although it is not impossible to configure Jib to do so at the expense of more complex project setup.) + +### I need to RUN commands like `apt-get`. + +Running commands like `apt-get` slows down the container build process. We **do not recommend or support** running commands as part of the build. + +However, if you need to run commands, you can build a custom image and configure Jib to use it as the base image. + +
+Base image configuration examples (click to expand) +

+ +#### Maven + +In [`jib-maven-plugin`](../jib-maven-plugin), you can then use this custom base image by adding the following configuration: + +```xml + + + custom-base-image + + +``` + +#### Gradle + +In [`jib-gradle-plugin`](../jib-gradle-plugin), you can then use this custom base image by adding the following configuration: + +```groovy +jib.from.image = 'custom-base-image' +``` +

+
+ +### Can I ADD a custom directory to the image? + +Yes, using the _extra directories_ feature. See the [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#adding-arbitrary-files-to-the-image) and [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#adding-arbitrary-files-to-the-image) docs for examples. + +### I need to add files generated during the build process to a custom directory on the image. + +If the current extra directories design doesn't meet your needs (e.g. you need to set up the extra files directory with files generated during the build process), you can use additional goals/tasks to create the extra directory as part of your build. + +
+File copying examples (click to expand) +

+ +#### Maven + +In Maven, you can use the `maven-resources-plugin` to copy files to your extra directory. For example, if you generate files in `target/generated/files` and want to add them to `/my/files` on the container, you can add the following to your `pom.xml`: + +```xml + + ... + + jib-maven-plugin + ... + + + + ${project.basedir}/target/extra-directory/ + + + + + ... + + maven-resources-plugin + 3.2.0 + + ${project.basedir}/target/extra-directory/my/files + + + ${project.basedir}/target/generated/files + + + + + ... + +``` + +The `copy-resources` goal will run automatically before compile, so if you are copying files from your build output to the extra directory, you will need to either set the life-cycle phase to `post-compile` or later, or run the goal manually: + +```sh +mvn compile resources:copy-resources jib:build +``` + +#### Gradle + +The same can be accomplished in Gradle by using a `Copy` task. In your `build.gradle`: + +```groovy +jib.extraDirectories.paths = ['build/extra-directory'] + +task setupExtraDir(type: Copy) { + from file('build/generated/files') + into file('build/extra-directory/my/files') +} +tasks.jib.dependsOn setupExtraDir +``` + +The files will be copied to your extra directory when you run the `jib` task. + +

+
+ +### Can I build to a local Docker daemon? + +There are several ways of doing this: + +- Use [`jib:dockerBuild` for Maven](../jib-maven-plugin#build-to-docker-daemon) or [`jibDockerBuild` for Gradle](../jib-gradle-plugin#build-to-docker-daemon) to build directly to your local Docker daemon. +- Use [`jib:buildTar` for Maven](../jib-maven-plugin#build-an-image-tarball) or [`jibBuildTar` for Gradle](../jib-gradle-plugin#build-an-image-tarball) to build the image to a tarball, then use `docker load --input` to load the image into Docker (the tarball built with these commands will be located in `target/jib-image.tar` for Maven and `build/jib-image.tar` for Gradle by default). +- [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. +- Alternatively, instead of using a Docker daemon, you can run a local container registry, such as [Docker registry](https://docs.docker.com/registry/deploying/) or other repository managers, and point Jib to push to the local registry. + +### How do I enable debugging? + +Use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept local debug connections on port 5005, but not suspend: +``` +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005 +``` + +Then connect your debugger to port 5005 on your local host. You can port-forward the container port to a localhost port for easy access. + +Using Docker: `docker run -p 5005:5005 ` + +Using Kubernetes: `kubectl port-forward 5005:5005` + +Beware: in Java 8 and earlier, specifying only a port meant that the JDWP socket was open to all incoming connections which is insecure. It is recommended to limit the debug port to localhost. + + +### I would like to run my application with a javaagent. + +You can run your container with a javaagent by placing it somewhere in the `src/main/jib/myfolder` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. + +#### Maven + +```xml + + + + -javaagent:/myfolder/agent.jar + + + +``` + +#### Gradle + +```groovy +jib.container.jvmFlags = ['-javaagent:/myfolder/agent.jar'] +``` + +See also: +- [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image) +- [Javaagent sample](https://github.com/GoogleContainerTools/jib/tree/master/examples/java-agent): dynamically downloads a javaagent during build +- (Gradle) [javaagent-gradle-plugin](https://github.com/ryandens/javaagent-gradle-plugin#jib-integration): third-party Jib extension + +### How can I tag my image with a timestamp? + +#### Maven + +To tag the image with a simple timestamp, add the following to your `pom.xml`: + +```xml + + yyyyMMdd-HHmmssSSS + +``` + +Then in the `jib-maven-plugin` configuration, set the `tag` to: + +```xml + + + my-image-name:${maven.build.timestamp} + + +``` + +You can then use the same timestamp to reference the image in other plugins. + +#### Gradle + +To tag the image with a timestamp, simply set the timestamp as the tag for `to.image` in your `jib` configuration. For example: + +```groovy +jib.to.image = 'gcr.io/my-gcp-project/my-app:' + System.nanoTime() +``` + + +### What would a Dockerfile for a Jib-built image look like? + +A Dockerfile that performs a Jib-like build is shown below: + +```Dockerfile +# Jib uses Adoptium Eclipse Temurin (formerly AdoptOpenJDK). +FROM eclipse-temurin:11-jre + +# Multiple copy statements are used to break the app into layers, +# allowing for faster rebuilds after small changes +COPY dependencyJars /app/libs +COPY snapshotDependencyJars /app/libs +COPY projectDependencyJars /app/libs +COPY resources /app/resources +COPY classFiles /app/classes + +# Jib's extra directory ("src/main/jib" by default) is used to add extra, non-classpath files +COPY src/main/jib / + +# Jib's default entrypoint when container.entrypoint is not set +ENTRYPOINT ["java", jib.container.jvmFlags, "-cp", "/app/resources:/app/classes:/app/libs/*", jib.container.mainClass] +CMD [jib.container.args] +``` + +When unset, Jib will infer the value for `jib.container.mainClass`. + +Some plugins, such as the [Docker Prepare Gradle Plugin](https://github.com/gclayburg/dockerPreparePlugin), will even automatically generate a Docker context for your project, including a Dockerfile. + +### How can I inspect the image Jib built? + +To inspect the image that is produced from the build using Docker, you can use commands such as `docker inspect your/image:tag` to view the image configuration, or you can also download the image using `docker save` to manually inspect the container image. Other tools, such as [dive](https://github.com/wagoodman/dive), provide nicer UI to inspect the image. + +### How do I specify a platform in the manifest list (or OCI index) of a base image? + +Newer Jib versions added an _incubating feature_ that provides support for selecting base images with the desired platforms from a manifest list. For example, + +```xml + + ... image reference to a manifest list ... + + + amd64 + linux + + + arm64 + linux + + + +``` + +```gradle +jib.from { + image = '... image reference to a manifest list ...' + platforms { + platform { + architecture = 'amd64' + os = 'linux' + } + platform { + architecture = 'arm64' + os = 'linux' + } + } +} +``` + +The default when not specified is a single "amd64/linux" platform, whose behavior is backward-compatible. + +When multiple platforms are specified, Jib creates and pushes a manifest list (also known as a fat manifest) after building and pushing all the images for the specified platforms. + +As an incubating feature, there are certain limitations: +- OCI image indices are not supported (as opposed to Docker manifest lists). +- Only `architecture` and `os` are supported. If the base image manifest list contains multiple images with the given architecture and os, the first image will be selected. +- Does not support using a local Docker daemon or tarball image for a base image. +- Does not support pushing to a Docker daemon (`jib:dockerBuild` / `jibDockerBuild`) or building a local tarball (`jib:buildTar` / `jibBuildTar`). + +Make sure to specify a manifest _list_ in `` (whether by a tag name or a digest (`@sha256:...`)). For troubleshooting, you may want to check what platforms the manifest list contains. To view a manifest list, [enable experimental docker CLI](https://docs.docker.com/engine/reference/commandline/cli/#experimental-features) features and then run the [manifest inspect](https://docs.docker.com/engine/reference/commandline/manifest_inspect/) command. + +``` +$ docker manifest inspect openjdk:8 +{ + ... + // This confirms that openjdk:8 points to a manifest list. + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + // This entry in the list points to the manifest for the ARM64/Linux manifest. + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + ... + "digest": "sha256:1fbd49e3fc5e53154fa93cad15f211112d899a6b0c5dc1e8661d6eb6c18b30a6", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + } + } + ] +} +``` + +### I want to exclude files from layers, have more fine-grained control over layers, change file ownership, etc. + +See ["Jib build plugins don't have the feature that I need"](#jib-build-plugins-dont-have-the-feature-that-i-need). + +### Jib build plugins don't have the feature that I need. + +The Jib build plugins have an extension framework that enables anyone to easily extend Jib's behavior to their needs. We maintain select [first-party](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party) plugins for popular use cases like [fine-grained layer control](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-layer-filter-extension-gradle) and [Quarkus support](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-quarkus-extension-gradle), but anyone can write and publish an extension. Check out the [jib-extensions](https://github.com/GoogleContainerTools/jib-extensions) repository for more information. + +### I am hitting Docker Hub rate limits. How can I configure registry mirrors? + +See the [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#global-jib-configuration), [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#global-jib-configuration) or [Jib CLI](https://github.com/GoogleContainerTools/jib/blob/master/jib-cli/README.md#global-jib-configuration) docs. Note that the example in the docs uses [Google's Docker Hub mirror on `mirror.gcr.io`](https://cloud.google.com/container-registry/docs/pulling-cached-images). + +Starting from Jib build plugins 3.0, Jib by default uses [base images on Docker Hub](default_base_image.md), so you may start to encounter the rate limits if you are not explicitly setting a base image. + +Some other alternatives to get around the rate limits: + +* Prevent Jib from accessing Docker Hub (after Jib cached a base image locally). + - **Pin to a specific base image using a SHA digest.** For example, `jib.from.image='eclipse-temurin:11-jre@sha256:...'`. If you are not setting a base image with a SHA digest (which is the case if you don't set `jib.from.image` at all), then every time Jib runs, it reaches out to the registry to check if the base image is up-to-date. On the other hand, if you pin to a specific image with a digest, then the image is immutable. Therefore, if Jib has cached the image once (by running Jib online once to fully cache the image), Jib will not reach out to the Docker Hub. See [this Stack Overflow answer](https://stackoverflow.com/a/61190005/1701388) for more details. + - (Maven/Gradle plugins only) **Do offline building.** Pass `--offline` to Maven or Gradle. Before that, you need to run Jib online once to cache the image. However, `--offline` means you cannot push to a remote registry. See [this Stack Overflow answer](https://stackoverflow.com/a/61190005/1701388) for more details. + - **Retrieve a base image from a local Docker daemon.** Store an image to your local Docker daemon, and set, say, `jib.from.image='docker://eclipse-temurin:11-jre'`. It can be slow for an initial build where Jib has to cache the image in Jib's format. For performance reasons, we usually recommend using an image on a registry. + - **Set up a local registry, store a base image, and read it from the local registry.** Setting up a local registry is as simple as running `docker run -d -p5000:5000 registry:2`, but nevertheless, the whole process is a bit involved. +* Retry with increasing backoffs. For example, using the [`retry`](https://github.com/kadwanev/retry) tool. + +### Where is the global Jib configuration file and how I can configure it? + +See the [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#global-jib-configuration), [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#global-jib-configuration) or [Jib CLI](https://github.com/GoogleContainerTools/jib/blob/master/jib-cli/README.md#global-jib-configuration) docs. + + +## Build Problems + +### How can I diagnose problems pulling or pushing from remote registries? + +There are a few reasons why Jib may be unable to connect to a remote registry, including: + +- **Registry reports FORBIDDEN.** See [_What should I do when the registry responds with Forbidden or DENIED?_](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied) +- **Registry reports UNAUTHORIZED.** See [_What should I do when the registry responds with UNAUTHORIZED?_](#what-should-i-do-when-the-registry-responds-with-unauthorized) +- **Access requires a proxy.** See [_How do I configure a proxy?_](#how-do-i-configure-a-proxy) for details. +- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.9_). +- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.9_). +- **The registry does not support the [Docker Image Format V2 Schema 2](https://github.com/GoogleContainerTools/jib/issues/601)** (sometimes referred to as _v2-2_). This problem is usually shown by failures wth `INVALID_MANIFEST` errors. Some registries can be configured to support V2-2 such as [Artifactory](https://www.jfrog.com/confluence/display/RTF/Docker+Registry#DockerRegistry-LocalDockerRepositories) and [OpenShift](https://docs.openshift.com/container-platform/3.9/install_config/registry/extended_registry_configuration.html#middleware-repository-acceptschema2). Other registries, such as Quay.io/Quay Enterprise, are in the process of adding support. + + +### What should I do when the registry responds with Forbidden or DENIED? + +If the registry returns `403 Forbidden` or `"code":"DENIED"`, it often means Jib successfully authenticated using your credentials but the credentials do not have permissions to pull or push images. Make sure your account/role has the permissions to do the operation. + +Depending on registry implementations, it is also possible that the registry actually meant you are not authenticated. See [What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized) to ensure you have set up credentials correctly. + +### What should I do when the registry responds with UNAUTHORIZED? + +If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is often due to credential misconfiguration. Examples: + +* You did not configure auth information in the default places where Jib searches. (See also [Authentication Methods](https://github.com/GoogleContainerTools/jib/blob/master/jib-maven-plugin/README.md#authentication-methods)). + + - Docker credential file (as generated by `docker login` or `podman login`) at + - `$XDG_RUNTIME_DIR/containers/auth.json`, `$XDG_CONFIG_HOME/containers/auth.json` or `$HOME/.config/containers/auth.json` + - `$DOCKER_CONFIG/config.json` + - `$HOME/.docker/config.json` + + This is [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` or `podman` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). + + If Jib was able to retrieve auth information from a Docker credential file, you should see a log message similar to `Using credentials from Docker config (/home/myuser/.docker/config.json)` where you can verify which credential file was picked up by Jib. + + - Jib configurations + + - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) (Maven) / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) (Gradle) + - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) (Maven) / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) (Gradle) + - These parameters can also be set through properties: [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#system-properties) / [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#system-properties) + +* For Google Cloud Registry (GCR), the Container Registry API is not yet enabled for your project. + - You enable the API from [Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=containerregistry.googleapis.com) or with the following [Cloud SDK](https://cloud.google.com/sdk/docs) command: `gcloud services enable containerregistry.googleapis.com` + +* `$HOME/.docker/config.json` may also contain short-lived authorizations in the `auths` block that may have expired. In the case of Google Container Registry, if you had previously used `gcloud docker` to configure these authorizations, you should remove these stale authorizations by editing your `config.json` and deleting lines from `auths` associated with `gcr.io` (for example: `"https://asia.gcr.io"`). You can then run `gcloud auth configure-docker` to correctly configure the `credHelpers` block for more robust interactions with gcr. + +* Different auth configurations exist in multiple places, and Jib is not picking up the auth information you are working on. + +* You configured a credential helper, but the helper is not on `$PATH`. This is especially common when running Jib inside IDE where the IDE binary is launched directly from an OS menu and does not have access to your shell's environment. + +* Configured credentials have access to the base image repository but not to the target image repository (or vice versa). + +* Typos in username, password, image names, repository names, or registry names. This is a very common error. + +* Image names do not conform to the structure or policy that a registry requires. For example, [Docker Hub returns 401 Unauthorized](https://github.com/GoogleContainerTools/jib/issues/2650#issuecomment-667323777) when trying to use a multi-level repository name. + +* Incorrect port number in image references (`registry.hostname:/...`). + +* You are using a private registry without HTTPS. See [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). + +Note, if Jib was able to retrieve credentials, you should see a log message like these: + +``` +Using credentials from Docker config (/home/user/.docker/config.json) for localhost:5000/java +``` +``` +Using credential helper docker-credential-gcr for gcr.io/project/repo +``` +``` +Using credentials from Maven settings file for gcr.io/project/repo +``` +``` +Using credentials from for gcr.io/project/repo +``` +``` +Using credentials from to.auth for gcr.io/project/repo +``` + +If you encounter issues interacting with a registry other than `UNAUTHORIZED`, check ["How can I diagnose problems pulling or pushing from remote registries?"](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). + +### How do I configure a proxy? + +Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). + + +### How can I examine network traffic? + +It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jib.serialize` property. + +To see the HTTP traffic, create a `logging.properties` file with the following: +``` +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=ALL + +# CONFIG hides authentication data +# ALL includes authentication data +com.google.api.client.http.level=CONFIG +``` + +And then launch your build tool as follows: +```sh +mvn --batch-mode -Djava.util.logging.config.file=path/to/logging.properties -Djib.serialize=true ... +``` +or +```sh +gradle --no-daemon --console=plain --info -Djava.util.logging.config.file=path/to/logging.properties -Djib.serialize=true ... +``` + +**Note**: Jib Gradle plugins prior to version 2.2.0 have an issue generating HTTP logs ([#2356](https://github.com/GoogleContainerTools/jib/issues/2356)). + +You may want to enable the debug logs too (`-X` for Maven, or `--debug --stacktrace` for Gradle). + +When configured correctly, you should see logs like this: +``` +Mar 31, 2020 9:55:52 AM com.google.api.client.http.HttpResponse +CONFIG: -------------- RESPONSE -------------- +HTTP/1.1 202 Accepted +Content-Length: 0 +Docker-Distribution-Api-Version: registry/2.0 +Docker-Upload-Uuid: 6292f0d7-93cb-4a8e-8336-78a1bf7febd2 +Location: https://registry-1.docker.io/v2/... +Range: 0-657292 +Date: Tue, 31 Mar 2020 13:55:52 GMT +Strict-Transport-Security: max-age=31536000 + +Mar 31, 2020 9:55:52 AM com.google.api.client.http.HttpRequest execute +CONFIG: -------------- REQUEST -------------- +PUT https://registry-1.docker.io/v2/... +Accept: +Accept-Encoding: gzip +Authorization: +User-Agent: jib 2.1.1-SNAPSHOT jib-maven-plugin Google-HTTP-Java-Client/1.34.0 (gzip) +``` + +### How do I view debug logs for Jib? + +Maven: use `mvn -X -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. + +Gradle: use `gradle --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. + +### I am seeing `Method Not Found` or `Class Not Found` errors when building. + +Sometimes when upgrading your Gradle build plugin versions, you may experience errors due to mismatching versions of dependencies pulled in (for example: [issues/2183](https://github.com/GoogleContainerTools/jib/issues/2183)). This can be due to the buildscript classpath loading behavior described [on gradle forums](https://discuss.gradle.org/t/version-is-root-build-gradle-buildscript-is-overriding-subproject-buildscript-dependency-versions/20746/3). + +This commonly appears in multi module Gradle projects. A solution to this problem is to define all of your plugins in the base project and apply them selectively in your subprojects as needed. This should help alleviate the problem of the buildscript classpath using older versions of a library. + +`build.gradle` (root) +```groovy +plugins { + id 'com.google.cloud.tools.jib' version 'x.y.z' apply false +} +``` + +`build.gradle` (sub-project) +```groovy +plugins { + id 'com.google.cloud.tools.jib' +} +``` + +### I am seeing `Unsupported class file major version` when building. + +When you're using latest Java versions to write an app (or using an old version of Jib), you may see the error _coming from Jib when building an image_ (not when compiling your code): + +``` +Failed to execute goal com.google.cloud.tools:jib-maven-plugin:3.2.0:dockerBuild (default-cli) on project demo: Execution default-cli of goal com.google.cloud.tools:jib-maven-plugin:3.2.0:dockerBuild failed: Unsupported class file major version 61 +``` + +Jib uses the [ASM library](https://asm.ow2.io/) to examine compiled Java bytecode to automatically infer a main class (in other words, the class that defines `public static void main()` to start your app). In this way, if you have only one such class, Jib can automatically infer and use that class to set an image entrypoint (basically, a command to start your app). When new Java versions come out, often the ASM library version used in Jib doesn't support the new bytecode format. If this is the case, check if you are using the latest Jib. If you still get the error with the latest Jib, file a [bug](https://github.com/GoogleContainerTools/jib/issues/new/choose) to have the Jib team upgrade the ASM library. + +**Workaround**: to prevent Jib from doing auto-inference, you can manually set your desired main class via [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#container-object) (for example, `com.example.your.Main`). As with other Jib parameters, it can be set through system/Maven properties or on the command-line (for example, `-Dcontainer.mainClass=...`). + +Note that although the ASM library is the common cause of this error coming from Jib, it may be due to other reasons. Always check the full stack (`-e` or `-X` for Maven and `--stacktrace` for Gradle) to see where the error is coming from. + +### I am seeing `NoClassDefFoundError: com/github/luben/zstd/ZstdOutputStream` when building. + +Jib supports base image layers with media-type `application/vnd.oci.image.layer.v1.tar+zstd`, i.e. compressed with zstd algorithm instead of gzip. + +However, the dependency to zstd is optional, so pulling such layers will result in: + +``` +java.lang.NoClassDefFoundError: com/github/luben/zstd/ZstdOutputStream +at org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream. +``` + +This can be solved by adding a dependency to artifact `com.github.luben:zstd-jni:1.5.2-3` to the plugin. + +## Launch problems + +### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). + +When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). + +```shell +kubectl create secret docker-registry registry-json-key \ + --docker-server= \ + --docker-username= \ + --docker-password= \ + --docker-email= + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"registry-json-key"}]}' +``` + +For example, if you are using GCR, the commands would look like (see [Advanced Authentication Methods](https://cloud.google.com/container-registry/docs/advanced-authentication)): + +```shell +kubectl create secret docker-registry gcr-json-key \ + --docker-server=https://gcr.io \ + --docker-username=_json_key \ + --docker-password="$(cat keyfile.json)" \ + --docker-email=any@valid.com + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"gcr-json-key"}]}' +``` + +See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). + +### Why won't my container start? + +There are some common reasons why containers fail on launch. + +#### My shell script won't run. + +Jib Maven and Gradle plugins prior to 3.0 used Distroless Java as the default base image, which does not have a shell. See [Where is bash?](#where-is-bash) for more details. + +#### The container fails with `exec` errors. + +A Jib user reported an error launching their container: +``` +standard_init_linux.go:211 exec user process caused "no such file or directory" +``` + +On examining the container structure with [Dive](https://github.com/wagoodman/dive), the user discovered that the contents of the `/lib` directory had disappeared. + +The user had used Jib's ability to install extra files into the image ([Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#adding-arbitrary-files-to-the-image), [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#adding-arbitrary-files-to-the-image)) to install a library file by placing it in `src/main/jib/lib/libfoo.so`. This would normally cause the `libfoo.so` to be installed in the image as `/lib/libfoo.so`. But `/lib` and `/lib64` in the user's base image were symbolic links. Jib does not follow such symbolic links when creating the image. And at container initialization time, Docker treats these symlinks as a file, and thus the symbolic link was replaced with `/lib` as a new directory. As a result, none of the system shared libraries were resolved and dynamically-linked programs failed. + +Solution: The user installed the file in a different location. + +## Jib CLI + +### How does the `jar` command support Standard JARs? + +The Jib CLI supports both [thin JARs](https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html) (where dependencies are specified in the JAR's manifest) and fat JARs. + +The current limitation of using a fat JAR is that the embedded dependencies will not be placed into the designated dependencies layers. They will instead be placed into the classes or resources layer. Therefore, for efficiency, we recommend against containerizing fat JARs (Spring Boot fat JARs are an [exception](#how-does-the-jar-command-support-spring-boot-jars)) if you can prepare thin JARs. We hope to have better support for fat JARs in the future. + +A standard JAR can be containerized by the `jar` command in two modes, exploded or packaged. + +#### Exploded Mode (Recommended) + +Achieved by calling `jib jar --target ${TARGET_REGISTRY} ${JAR_NAME}.jar` + +The default mode for containerizing a JAR. It will open up the JAR and optimally place files into the following layers: + +* Other Dependencies Layer +* Snapshot-Dependencies Layer +* Resources Layer +* Classes Layer + +**Entrypoint** : `java -cp /app/dependencies/:/app/explodedJar/ ${MAIN_CLASS}` + +#### Packaged Mode + +Achieved by calling `jib jar --target ${TARGET_REGISTRY} ${JAR_NAME}.jar --mode packaged`. + +It will result in the following layers on the container: + +* Dependencies Layer +* Jar Layer + +**Entrypoint** : `java -jar ${JAR_NAME}.jar` + +### How does the `jar` command support Spring Boot JARs? + +The `jar` command currently supports containerization of Spring Boot fat JARs. +A Spring-Boot fat JAR can be containerized in two modes, exploded or packaged. + +#### Exploded Mode (Recommended) + +Achieved by calling `jib jar --target ${TARGET_REGISTRY} ${JAR_NAME}.jar` + +The default mode for containerizing a JAR. It will respect [`layers.idx`](https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3) in the JAR (if present) or create optimized layers in the following format: + +* Other Dependencies Layer +* Spring-Boot-Loader Layer +* Snapshot-Dependencies Layer +* Resources Layer +* Classes Layer + +**Entrypoint** : `java -cp /app org.springframework.boot.loader.JarLauncher` + +#### Packaged Mode + +Achieved by calling `jib jar --target ${TARGET_REGISTRY} ${JAR_NAME}.jar --mode packaged` + +It will containerize the JAR as is. However, **note** that we highly recommend against using packaged mode for containerizing Spring Boot fat JARs. + +**Entrypoint**: `java -jar ${JAR_NAME}.jar` + +### How does the `war` command work? + +The `war` command currently supports containerization of standard WARs. It uses the official [`jetty`](https://hub.docker.com/_/jetty) on Docker Hub as the default base image and explodes out the WAR into `/var/lib/jetty/webapps/ROOT` on the container. It creates the following layers: + +* Other Dependencies Layer +* Snapshot-Dependencies Layer +* Resources Layer +* Classes Layer + +The default entrypoint when using a jetty base image will be `java -jar /usr/local/jetty/start.jar --module=ee10-deploy` unless you choose to specify a custom one. + +You can use a different Servlet engine base image with the help of the `--from` option and customize `--app-root`, `--entrypoint` and `--program-args`. If you don't set the `entrypoint` or `program-arguments`, Jib will inherit them from the base image. However, setting the `--app-root` is **required** if you use a non-jetty base image. Here is how the `war` command may look if you're using a Tomcat image: +``` + $ jib war --target= myapp.war --from=tomcat:8.5-jre8-alpine --app-root=/usr/local/tomcat/webapps/ROOT +``` + diff --git a/docs/google-cloud-build.md b/docs/google-cloud-build.md new file mode 100644 index 0000000000..d3996ce82c --- /dev/null +++ b/docs/google-cloud-build.md @@ -0,0 +1,33 @@ +# Jib on Google Cloud Build + +You can use Jib on [Google Cloud Build](https://cloud.google.com/build) in a simple step: + +```yaml +steps: + - name: 'gcr.io/cloud-builders/javac:8' + entrypoint: './gradlew' + args: ['--console=plain', '--no-daemon', ':server:jib', '-Djib.to.image=gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA'] +``` + +Any Java container can be used for building, not only the `gcr.io/cloud-builders/javac:*` (from [gcr.io/cloud-builders/javac](https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/javac)), for example with [Temurin](https://adoptium.net/en-GB/temurin/)'s: + +```yaml +steps: + - name: 'docker.io/library/eclipse-temurin:25' + entrypoint: './gradlew' + args: ['--console=plain', '--no-daemon', ':server:jib', '-Djib.to.image=gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA'] +``` + +To use [Google "Distroless" Container Images](https://github.com/GoogleContainerTools/distroless) to build with Jib on Google Cloud Build, and avoid running into `Step #1: standard_init_linux.go:228: exec user process caused: no such file or directory` errors (because Google's _distroless_ containers are based on `busybox`), you have to do something like this: + +```yaml +steps: + - name: 'gcr.io/distroless/java17-debian11:debug' + entrypoint: '/busybox/sh' + args: + - -c + - | + ln -s /busybox/sh /bin/sh + ln -s /busybox/env /usr/bin/env + /workspace/gradlew --console=plain --no-daemon --gradle-user-home=/home/.gradle :server:jib -Djib.to.image=gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA +``` diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 4eb628bb0a..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Documentation for older versions: - -jib-maven-plugin -- [v0.1.7](https://github.com/GoogleContainerTools/jib/blob/03d42652e82c543d0a6c6b69bd8827ec8effd82d/docs/v0.1.7/jib-maven-plugin.md) - -jib-gradle-plugin -- [v0.1.1](https://github.com/GoogleContainerTools/jib/blob/03d42652e82c543d0a6c6b69bd8827ec8effd82d/docs/v0.1.1/jib-gradle-plugin.md) diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000000..c0c78b3dc8 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,17 @@ +The privacy of our users is very important to us. +Your use of this software is subject to the Google Privacy Policy. + +## Update check +Many Jib users are unaware of new releases. To encourage users to stay up-to-date, the Jib Maven and Jib Gradle plugins (2.0.0 and later) and Jib CLI (0.6.0 and later) will +periodically check to see if there is a new version of Jib is available. This check fetches a simple text +file hosted in Google Cloud Storage. As a side effect this request is logged, which includes the request path, +source IP address, and the user-agent string. The user-agent is set by Jib and includes the Jib tool name +and version. + +### How to disable update checks + +1. set the `jib.disableUpdateChecks` system property to `true` +2. set `disableUpdateCheck` to `true` in Jib's global config. The global config is in the following locations by default: + * Linux: `$XDG_CONFIG_HOME/google-cloud-tools-java/jib/config.json` (if `$XDG_CONFIG_HOME` is defined), else `$HOME/.config/google-cloud-tools-java/jib/config.json` + * Mac: `$XDG_CONFIG_HOME/Google/Jib/config.json` (if `$XDG_CONFIG_HOME` is defined), else `$HOME/Library/Preferences/Google/Jib/config.json` + * Windows: `$XDG_CONFIG_HOME\Google\Jib\Config\config.json` (if `$XDG_CONFIG_HOME` is defined), else `%LOCALAPPDATA%\Google\Jib\Config\config.json` \ No newline at end of file diff --git a/docs/self_sign_cert-kse-import.png b/docs/self_sign_cert-kse-import.png new file mode 100644 index 0000000000..b4fce5e0fe Binary files /dev/null and b/docs/self_sign_cert-kse-import.png differ diff --git a/docs/self_sign_cert.md b/docs/self_sign_cert.md new file mode 100644 index 0000000000..686d8f84de --- /dev/null +++ b/docs/self_sign_cert.md @@ -0,0 +1,109 @@ +# Accessing a private docker registry with self-signed certificate + +Jib relies on the Java Runtime Environment's list of approved _Certification Authority Certificates_ for validating SSL certificates, and will hence fail when connecting to a docker registry that uses a self-signed `https` certificate. This document describes two approaches for handling registries with self-signed certificates. Both approaches configure the JRE's list of approved CA Certificates. + +These CA Certificates for JRE are managed through a type of a keystore file called _truststore_. An easy way to manipulate truststores is using the [KeyStore Explorer](http://keystore-explorer.org/), an open source GUI replacement for the Java command-line `keytool` and `jarsigner` utilities. Download and install KeyStore Explorer from the [official website](http://keystore-explorer.org/downloads.html). + + +## Step 1. Identify Java runtime used by build tool + +We must first identify the location of your build-tool's JRE's list of CA Certificates. + +### Maven + +Run `mvn --version` and take note of the Java runtime location: + +```shell +$ mvn --version +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T06:33:14+12:00) +Maven home: /usr/local/Cellar/maven/3.5.4/libexec +Java version: 1.8.0_172, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre +Default locale: en_NZ, platform encoding: UTF-8 +OS name: "mac os x", version: "10.13.6", arch: "x86_64", family: "mac" +``` + +In this example the Java runtime location is `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre`. + +### Gradle + +Create an init script with the following: + +``` +println org.gradle.internal.jvm.Jvm.current().getJavaHome() +``` + +And run `gradle -I /path/to/script` to output the executing JRE location. + +```shell +$ gradle -I /tmp/printjrelocation +/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home + +> Task :help + +Welcome to Gradle 4.6. +[...] +``` + +### JRE vs JDK Distributions + +The Maven and Gradle examples above report two different directories, where the Maven example reported a `.../jre` subdirectory. Java Development Kits usually include a standalone Java Runtime Environment inside the `jre/` directory. If present, use the `jre/` directory as the runtime location. + +## 2. Load JRE CA Certificates + +Having identified your Java runtime location: + +* Launch `KeyStore Explorer` +* Select _Open an existing KeyStore_ +* Navigate to the Java runtime location identified previously, and then continue to open the file at `jre/lib/security/cacerts`. If there is no `jre/` directory then this is a JRE distribution and should navigate and instead open the file at `lib/security/cacerts`. + * In the example above, this file would be `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/security/cacerts`. +* You will likely be prompted for a password. The default password for the `cacerts` file is `changeit`. + +## 3. Import Self-Signed Certificate + +If you have the self-signed certificate in a file then: + +* Select _Tools > Import Trusted Certificate_ +* Select the certifcate file on disk +* Give it a name, or use suggested name, and click _OK_ +* Click _OK_ on the success window + +Otherwise use _Examine > Examine SSL_ to connect to your service and click the _Import_ button to import its SSL certificate. Then click _OK_. + +![Importing certificate with KeyStore Explorer](self_sign_cert-kse-import.png) + +## 4. Save the CA Certificates + +Now we save the updated truststore. We can either save to a new truststore and configure our build's JVM to use this new truststore, or modify the JRE's list of CA Certificates. + +#### Option 1: Create a New Truststore + +This option creates a _new_ list of CA Certificates and configures your build tool to use this new list as the JRE's list of approved CA certificates. + +Within _KeyStore Explorer_, select _File > Save As..._ and save the new truststore file as a _JKS_ file within your project location. You will be prompted for a password; we use `password` in the examples below. + +##### Maven + +The following snippet shows how to configure Maven to use this new truststore file: + +```shell +$ ./mvnw -Djavax.net.ssl.trustStore=path/to/truststore.jks \ + -Djavax.net.ssl.trustStorePassword=password \ + -Dimage=:/ jib:build +``` + +##### Gradle + +The following snippet shows how to configure Gradle to use this new truststore file: + +```shell +$ ./gradlew jib \ + -Djavax.net.ssl.trustStore=path/to/truststore.jks \ + -Djavax.net.ssl.trustStorePassword=password +``` + +#### Option 2: Modify the JRE `cacerts` + +The other approach modifies the JRE's list of CA Certificates to include the registry's self-signed certificate. The certificate will be trusted at the JRE level, affecting all Java applications running on it. You must re-import the certificate when you update to a new JRE. + +Basically you instruct KeyStore Explorer to save your modified `cacerts` and replace what was previously configured with the JRE. Depending on your operating system and permissions, you may need to save to a new file and then replace the original `lib/security/cacerts` file with administrative privileges. + diff --git a/examples/README.md b/examples/README.md index efd97f50cf..d5b70e0270 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1 +1,43 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples)](https://github.com/igrigorik/ga-beacon) +# Example projects containerizing with Jib + +Please [file an issue](/../../issues/new) if you find any problems with the examples or would like to request other examples. + +For examples on using Jib Core, see [jib-core/examples](../jib-core/examples). + +### Simple example + +See [helloworld](helloworld) for containerizing a simple `Hello World` application. + +### Multi-module example (DRAFT) + +See [multi-module](multi-module) for containerizing projects with multiple modules. + +### Vert.x + +See [vertx](vertx) for containerizing [Eclipse Vert.x](https://vertx.io/) applications. + +### Spring Boot example + +See [spring-boot](spring-boot) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). + +### Ktor example + +See [ktor](ktor) for containerizing a [Ktor](https://ktor.io) Kotlin Application using the Kotlin Gradle DSL. + +### Dropwizard example + +See [dropwizard](dropwizard) for containerizing [Dropwizard](https://dropwizard.io) applications. + +### Micronaut example + +See [micronaut](micronaut) for containerizing a [Micronaut framework](https://micronaut.io/) Groovy/Java application. + +### Java agents example + +See [java-agent](java-agent) for launching with the Stackdriver Debugger Java agent. + +### Kafka Connect+Streams example + +See the following projects for using Jib-packaged applications in coordination with [Apache Kafka](http://kafka.apache.org/documentation). + - [Kafka Streams](http://kafka.apache.org/documentation/streams) - [`OneCricketeer/kafka-streams-jib-example`](https://github.com/OneCricketeer/kafka-streams-jib-example) + - [Kafka Connect](http://kafka.apache.org/documentation#connect) - [`OneCricketeer/apache-kafka-connect-docker`](https://github.com/OneCricketeer/apache-kafka-connect-docker) diff --git a/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java b/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000000..fa4f7b499f --- /dev/null +++ b/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties b/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000000..b6e6781222 --- /dev/null +++ b/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip \ No newline at end of file diff --git a/examples/dropwizard/README.md b/examples/dropwizard/README.md new file mode 100644 index 0000000000..384c121893 --- /dev/null +++ b/examples/dropwizard/README.md @@ -0,0 +1,42 @@ +# Containerize a [Dropwizard](https://dropwizard.io) application with Jib + +## How to start the Dropwizard application + +1. Run `./mvnw clean package` to build your container +1. Start the application + - **With Docker**: `docker run --rm -p 8080:8080 dropwizard-jib-example:1` + - **Without Docker**: `./mvnw exec:java` +1. Check that your application is running at http://localhost:8080 + +## Health Check + +See your application's health at http://localhost:8080/admin/healthcheck + +## Extras + +FreeMaker templating is setup for [`dropwizard.yml`](src/main/resources/dropwizard.yml) through [`tkrille/dropwizard-template-config`](https://github.com/tkrille/dropwizard-template-config); this allows one to heavily customize the properties file via the container environment with FTL conditional checks and for loops, for example. + +## How this example was generated + +Starter Maven template generated with [`dropwizard-archetypes`](https://github.com/dropwizard/dropwizard/tree/master/dropwizard-archetypes) + +```sh +mvn archetype:generate \ + -DarchetypeGroupId=io.dropwizard.archetypes \ + -DarchetypeArtifactId=example \ + -DarchetypeVersion=[REPLACE ME WITH A VALID DROPWIZARD VERSION] +``` + +Ref. [Dropwizard - Getting Started, Setting up With Maven](https://www.dropwizard.io/1.3.5/docs/getting-started.html#setting-up-using-maven) + +The remainder of the archetype code was filled-in following the above guide. + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [more about Dropwizard](https://dropwizard.io). + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/dropwizard) diff --git a/jib-maven-plugin/mvnw b/examples/dropwizard/mvnw similarity index 71% rename from jib-maven-plugin/mvnw rename to examples/dropwizard/mvnw index e96ccd5fbb..5551fde8e7 100755 --- a/jib-maven-plugin/mvnw +++ b/examples/dropwizard/mvnw @@ -200,6 +200,65 @@ if [ -z "$BASE_DIR" ]; then exit 1; fi +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR diff --git a/jib-maven-plugin/mvnw.cmd b/examples/dropwizard/mvnw.cmd similarity index 86% rename from jib-maven-plugin/mvnw.cmd rename to examples/dropwizard/mvnw.cmd index 4f0b068a03..e5cfb0ae9e 100755 --- a/jib-maven-plugin/mvnw.cmd +++ b/examples/dropwizard/mvnw.cmd @@ -1,145 +1,161 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml new file mode 100644 index 0000000000..1817cb11dc --- /dev/null +++ b/examples/dropwizard/pom.xml @@ -0,0 +1,111 @@ + + + + 4.0.0 + + example + dropwizard-jib-example + 1 + jar + + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + + example.JibExampleApplication + + 1.3.16 + dropwizard.yml + 1.5.0 + + /app + 3.5.2 + + + + + + io.dropwizard + dropwizard-bom + ${dropwizard.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + + de.thomaskrille + dropwizard-template-config + ${dropwizard-template-config.version} + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + + server + ${jib.container.appRoot}/resources/${dropwizard.server.config} + + + 8080 + + + + + -server + -Djava.awt.headless=true + -XX:InitialRAMFraction=2 + -XX:MinRAMFraction=2 + -XX:MaxRAMFraction=2 + -XX:+UseG1GC + -XX:MaxGCPauseMillis=100 + -XX:+UseStringDeduplication + + + + + + dockerBuild + + dockerBuild + + package + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + ${mainClass} + + server + ${project.build.sourceDirectory}/../resources/${dropwizard.server.config} + + + + + + diff --git a/examples/dropwizard/src/main/java/example/JibExampleApplication.java b/examples/dropwizard/src/main/java/example/JibExampleApplication.java new file mode 100644 index 0000000000..edc75a979f --- /dev/null +++ b/examples/dropwizard/src/main/java/example/JibExampleApplication.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import de.thomaskrille.dropwizard_template_config.TemplateConfigBundle; +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import example.health.TemplateHealthCheck; +import example.resources.HelloWorldResource; + +/** + * + * Refer Dropwizard User Manual + */ +public class JibExampleApplication extends Application { + + public static void main(final String[] args) throws Exception { + new JibExampleApplication().run(args); + } + + @Override + public String getName() { + return "Dropwizard Jib Example"; + } + + @Override + public void initialize(final Bootstrap bootstrap) { + // Enable FreeMarker config templates + bootstrap.addBundle(new TemplateConfigBundle()); + } + + @Override + public void run(final JibExampleConfiguration configuration, final Environment environment) { + final TemplateHealthCheck healthCheck = + new TemplateHealthCheck(configuration.getHelloConfiguration().getTemplate()); + environment.healthChecks().register("template", healthCheck); + + environment.jersey().register(HelloWorldResource.from(configuration)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java b/examples/dropwizard/src/main/java/example/JibExampleConfiguration.java similarity index 52% rename from jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java rename to examples/dropwizard/src/main/java/example/JibExampleConfiguration.java index 9fdf13e093..65ecc6b295 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java +++ b/examples/dropwizard/src/main/java/example/JibExampleConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,18 +14,23 @@ * the License. */ -package com.google.cloud.tools.jib.async; +package example; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.Configuration; -/** Static methods for {@link AsyncStep}. */ -public class AsyncSteps { +import example.config.HelloWorldConfiguration; - public static AsyncStep immediate(T returnValue) { - ListenableFuture future = Futures.immediateFuture(returnValue); - return () -> future; - } +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@SuppressWarnings("unused") +public class JibExampleConfiguration extends Configuration { + @Valid @NotNull @JsonProperty + private HelloWorldConfiguration hello; - private AsyncSteps() {} + @JsonProperty("hello") + public HelloWorldConfiguration getHelloConfiguration() { + return hello; + } } diff --git a/examples/dropwizard/src/main/java/example/api/Saying.java b/examples/dropwizard/src/main/java/example/api/Saying.java new file mode 100644 index 0000000000..86aae674d1 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/api/Saying.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.api; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Saying { + private long id; + + private String content; + + public Saying() { + // Jackson deserialization + } + + public Saying(long id, String content) { + this.id = id; + this.content = content; + } + + @JsonProperty + public long getId() { + return id; + } + + @JsonProperty + public String getContent() { + return content; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java b/examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java similarity index 52% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java rename to examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java index a9abd630b8..7692b796b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java +++ b/examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,18 +14,28 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package example.config; -/** Thrown because registry authentication failed. */ -public class RegistryAuthenticationFailedException extends Exception { +import io.dropwizard.Configuration; - private static final String REASON_PREFIX = "Failed to authenticate with the registry because: "; +import org.hibernate.validator.constraints.NotEmpty; - RegistryAuthenticationFailedException(Throwable cause) { - super(REASON_PREFIX + cause.getMessage(), cause); +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("unused") +public class HelloWorldConfiguration extends Configuration { + + @NotEmpty private String template; + + @NotEmpty private String defaultName = "Stranger"; + + @JsonProperty + public String getTemplate() { + return template; } - RegistryAuthenticationFailedException(String reason) { - super(REASON_PREFIX + reason); + @JsonProperty + public String getDefaultName() { + return defaultName; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java b/examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java similarity index 52% rename from jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java rename to examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java index 2fab895a7a..15220d9bf1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java +++ b/examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,18 +14,23 @@ * the License. */ -package com.google.cloud.tools.jib.async; +package example.health; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.concurrent.ExecutionException; +import com.codahale.metrics.health.HealthCheck; -/** Static utility for ensuring {@link ListenableFuture#get} does not block. */ -public class NonBlockingSteps { +public class TemplateHealthCheck extends HealthCheck { + private final String template; - public static T get(AsyncStep asyncStep) throws ExecutionException { - return Futures.getDone(asyncStep.getFuture()); + public TemplateHealthCheck(String template) { + this.template = template; } - private NonBlockingSteps() {} + @Override + protected Result check() throws Exception { + final String saying = String.format(template, "TEST"); + if (!saying.contains("TEST")) { + return Result.unhealthy("template doesn't include a name"); + } + return Result.healthy(); + } } diff --git a/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java new file mode 100644 index 0000000000..987eca2810 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.resources; + +import com.codahale.metrics.annotation.Timed; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import example.JibExampleConfiguration; +import example.api.Saying; +import example.config.HelloWorldConfiguration; + +@Path("/") +@Produces(MediaType.APPLICATION_JSON) +public class HelloWorldResource { + + private final String template; + private final String defaultName; + private final AtomicLong counter; + + public HelloWorldResource(String template, String defaultName) { + this.template = template; + this.defaultName = defaultName; + this.counter = new AtomicLong(); + } + + public static HelloWorldResource from(JibExampleConfiguration conf) { + final HelloWorldConfiguration helloConfiguration = conf.getHelloConfiguration(); + return new HelloWorldResource( + helloConfiguration.getTemplate(), helloConfiguration.getDefaultName()); + } + + @GET + @Timed + public Saying sayHello(@QueryParam("name") Optional name) { + final String value = String.format(template, name.orElse(defaultName)); + return new Saying(counter.incrementAndGet(), value); + } +} diff --git a/examples/dropwizard/src/main/resources/banner.txt b/examples/dropwizard/src/main/resources/banner.txt new file mode 100644 index 0000000000..57c331f963 --- /dev/null +++ b/examples/dropwizard/src/main/resources/banner.txt @@ -0,0 +1,6 @@ +================================================================================ + + Dropwizard Jib Example + +================================================================================ + diff --git a/examples/dropwizard/src/main/resources/dropwizard.yml b/examples/dropwizard/src/main/resources/dropwizard.yml new file mode 100644 index 0000000000..db13ff16a4 --- /dev/null +++ b/examples/dropwizard/src/main/resources/dropwizard.yml @@ -0,0 +1,18 @@ +<#-- FreeMarker Enabled - https://github.com/tkrille/dropwizard-template-config --> +hello: + template: ${DW_TEMPLATE!'Hello, %s!'} + defaultName: ${DW_DEFAULT_NAME!'Stranger'} + +server: + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 + +logging: + level: ${LOG_LEVEL!'INFO'} + loggers: + <#-- Log level for a specific package --> + example: DEBUG diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md index 769b60c2d7..dacd0e6f9c 100644 --- a/examples/helloworld/README.md +++ b/examples/helloworld/README.md @@ -2,6 +2,6 @@ Builds a container image that outputs `Hello World` when run. To build the image: -1. In `pom.xml`, replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project. +1. In `pom.xml` or `build.gradle`, replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project. -1. Run `mvn compile jib:build`. +1. Run `mvn compile jib:build` or `./gradlew jib`. diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 85058498f9..9e8542d03f 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.4' + id 'com.google.cloud.tools.jib' version '3.5.4' } sourceCompatibility = 1.8 @@ -11,7 +11,7 @@ repositories { } dependencies { - compile 'com.google.guava:guava:23.6-jre' + implementation 'com.google.guava:guava:23.6-jre' } jib.to.image = 'gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib' diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.jar b/examples/helloworld/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/helloworld/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jib-core/gradle/wrapper/gradle-wrapper.properties b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from jib-core/gradle/wrapper/gradle-wrapper.properties rename to examples/helloworld/gradle/wrapper/gradle-wrapper.properties index bf3de21830..6b3851a8ad 100644 --- a/jib-core/gradle/wrapper/gradle-wrapper.properties +++ b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jib-core/gradlew b/examples/helloworld/gradlew similarity index 99% rename from jib-core/gradlew rename to examples/helloworld/gradlew index cccdd3d517..af6708ff22 100755 --- a/jib-core/gradlew +++ b/examples/helloworld/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/jib-core/gradlew.bat b/examples/helloworld/gradlew.bat similarity index 95% rename from jib-core/gradlew.bat rename to examples/helloworld/gradlew.bat index e95643d6a2..6d57edc706 100644 --- a/jib-core/gradlew.bat +++ b/examples/helloworld/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index b203e0adb2..63debdf293 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 example @@ -6,14 +8,16 @@ 1 - 0.9.4 + UTF-8 + 3.5.2 + 3.8.0 com.google.guava guava - 23.6-jre + 32.0.0-jre @@ -22,6 +26,7 @@ org.apache.maven.plugins maven-compiler-plugin + ${maven-compiler-plugin.version} 1.8 1.8 @@ -49,4 +54,4 @@ - \ No newline at end of file + diff --git a/jib-core/src/test/resources/application/classes/some.class b/examples/helloworld/settings.gradle similarity index 100% rename from jib-core/src/test/resources/application/classes/some.class rename to examples/helloworld/settings.gradle diff --git a/examples/helloworld/src/main/java/example/HelloWorld.java b/examples/helloworld/src/main/java/example/HelloWorld.java index a6b526cfd8..89bd1f67d5 100644 --- a/examples/helloworld/src/main/java/example/HelloWorld.java +++ b/examples/helloworld/src/main/java/example/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,21 +16,21 @@ package example; -import com.google.common.io.Resources; +import com.google.common.io.CharStreams; +import java.io.InputStreamReader; import java.io.IOException; +import java.io.Reader; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; public class HelloWorld { public static void main(String[] args) throws URISyntaxException, IOException { - Path worldFile = Paths.get(Resources.getResource("world").toURI()); - String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); - - System.out.println("Hello " + world); + try (Reader reader = new InputStreamReader( + HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8)) { + String world = CharStreams.toString(reader); + System.out.println("Hello " + world); + } } } diff --git a/examples/java-agent/README.md b/examples/java-agent/README.md new file mode 100644 index 0000000000..672ff2a5d7 --- /dev/null +++ b/examples/java-agent/README.md @@ -0,0 +1,14 @@ +This SparkJava-based example builds a container image that includes the [Stackdriver Debugger Java Agent](https://cloud.google.com/debugger/docs/). +This project assumes the resulting image will be run inside Google Cloud Platform. + +To build the image: + +1. Replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project in `pom.xml` or `build.gradle`. + +1. Run `mvn package` or `./gradlew` to build the image. + +SparkJava listens on port 4567 by default. + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/java-agent) diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle new file mode 100644 index 0000000000..eaf5099b84 --- /dev/null +++ b/examples/java-agent/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' version '3.5.4' + id 'de.undercouch.download' version '4.0.0' + id 'com.gorylenko.gradle-git-properties' version '2.2.0' +} + +ext { + // where to download the Stackdriver Debugger agent https://cloud.google.com/debugger/docs/setup/java + stackdriverDebuggerAgentUrl = 'https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz' + + // where to place the Cloud Debugger agent in the container + stackdriverDebuggerLocation = '/opt/cdbg' + + // location for jib extras, including the Java agent + jibExtraDirectory = "${buildDir}/jib-agents" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.sparkjava:spark-core:2.9.1' + implementation 'org.slf4j:slf4j-simple:1.7.28' +} + +// Download and extract the Cloud Debugger Java Agent +task downloadAgent(type: Download) { + src stackdriverDebuggerAgentUrl + dest "${buildDir}/cdbg_java_agent_gce.tar.gz" +} +task extractAgent(dependsOn: downloadAgent, type: Copy) { + from tarTree(downloadAgent.dest) + into "${jibExtraDirectory}/${stackdriverDebuggerLocation}" +} + +jib { + to { + image = 'gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib' + } + extraDirectories.paths = [file(jibExtraDirectory)] + container { + ports = ['4567'] + jvmFlags = [ + '-agentpath:' + stackdriverDebuggerLocation + '/cdbg_java_agent.so=--logtostderr=1', + '-Dcom.google.cdbg.module=' + project.name, + '-Dcom.google.cdbg.version=' + version] + } +} + +tasks.jib.dependsOn extractAgent +tasks.jibDockerBuild.dependsOn extractAgent +tasks.jibBuildTar.dependsOn extractAgent +defaultTasks 'jib' diff --git a/examples/java-agent/gradle.properties b/examples/java-agent/gradle.properties new file mode 100644 index 0000000000..ac2e54ee6d --- /dev/null +++ b/examples/java-agent/gradle.properties @@ -0,0 +1 @@ +version = 0.0.1-SNAPSHOT diff --git a/examples/java-agent/gradle/wrapper/gradle-wrapper.jar b/examples/java-agent/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/java-agent/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties rename to examples/java-agent/gradle/wrapper/gradle-wrapper.properties index 32a96c59be..6b3851a8ad 100644 --- a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jib-gradle-plugin/gradlew b/examples/java-agent/gradlew similarity index 99% rename from jib-gradle-plugin/gradlew rename to examples/java-agent/gradlew index cccdd3d517..af6708ff22 100755 --- a/jib-gradle-plugin/gradlew +++ b/examples/java-agent/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/jib-gradle-plugin/gradlew.bat b/examples/java-agent/gradlew.bat similarity index 95% rename from jib-gradle-plugin/gradlew.bat rename to examples/java-agent/gradlew.bat index e95643d6a2..6d57edc706 100644 --- a/jib-gradle-plugin/gradlew.bat +++ b/examples/java-agent/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml new file mode 100644 index 0000000000..214d43a093 --- /dev/null +++ b/examples/java-agent/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + example + java-agent + 1 + + + UTF-8 + 3.5.2 + 3.8.0 + 1.4.2 + 3.0.1 + + + ${project.build.directory}/jib-agents + + + https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz + + + /opt/cdbg + + + + + com.sparkjava + spark-core + 2.9.1 + + + org.slf4j + slf4j-simple + 1.7.28 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + + revision + + + initialize + + true + + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib + + + ${agent-extraction-root} + + + 4567 + + -agentpath:${stackdriver-debugger-agent-location}/cdbg_java_agent.so=--logtostderr=1 + -Dcom.google.cdbg.module=${project.artifactId} + -Dcom.google.cdbg.version=${project.version} + + + + + + package + + build + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + ${download-maven-plugin.version} + + + install-stackdriver-debugger + prepare-package + + wget + + + ${stackdriver-debugger-agent-url} + true + ${agent-extraction-root}/${stackdriver-debugger-agent-location} + + + + + + + diff --git a/jib-core/src/test/resources/application/dependencies/libraryA.jar b/examples/java-agent/settings.gradle similarity index 100% rename from jib-core/src/test/resources/application/dependencies/libraryA.jar rename to examples/java-agent/settings.gradle diff --git a/examples/java-agent/src/main/java/example/HelloWorld.java b/examples/java-agent/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..fdc61eb94f --- /dev/null +++ b/examples/java-agent/src/main/java/example/HelloWorld.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import static spark.Spark.get; +import static spark.Spark.port; + +public class HelloWorld { + public static void main(String[] args) { + // Allow use with Cloud Run which requires listening on the value in PORT + String portEnv = System.getenv("PORT"); + if (portEnv != null) { + port(Integer.parseInt(portEnv)); + } + + get("/", (req, res) -> "Hello World"); + } +} diff --git a/jib-core/src/test/resources/application/resources/world b/examples/java-agent/src/main/resources/world similarity index 100% rename from jib-core/src/test/resources/application/resources/world rename to examples/java-agent/src/main/resources/world diff --git a/examples/ktor/README.md b/examples/ktor/README.md new file mode 100644 index 0000000000..b03a64432f --- /dev/null +++ b/examples/ktor/README.md @@ -0,0 +1,30 @@ +# Containerize a [Ktor] application with Jib + +This is an example of how to easily build a Docker image for a [Ktor] application with Jib. + +```shell +./gradlew jibDockerBuild + +docker run --rm -p 8080:8080 ktor-jib-example:1 +``` + +The application can also be ran outside of Jib's build process via `./gradlew run` + +## Defined environment variables + +A few variables have been added in the code-base to show case some of the unique features of Ktor. + +- `KTOR_APP_ID` - The name of the application in the logs + - Type: String +- `KTOR_METRICS_ENABLED` - Exposes JMX Metrics through the application port (`8080`) + - Type: Boolean (default: `false`) +- `KTOR_ROUTE_TRACING` - Enables verbose logging of route matches for debugging complex/nested routing tables + - Type: Boolean + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [Ktor]. + + [Ktor]: https://ktor.io diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts new file mode 100644 index 0000000000..fd2b5247d6 --- /dev/null +++ b/examples/ktor/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + application + kotlin("jvm") version "1.3.10" + id("com.google.cloud.tools.jib") version "3.5.4" +} + +group = "example" +version = "1" + +val ktor_version by extra("1.0.0") +val logback_version by extra("1.2.3") + +val main_class by extra("io.ktor.server.netty.EngineMain") + +application { + mainClassName = main_class + + applicationDefaultJvmArgs = listOf( + "-server", + "-Djava.awt.headless=true", + "-Xms128m", + "-Xmx256m", + "-XX:+UseG1GC", + "-XX:MaxGCPauseMillis=100" + ) +} + +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + implementation(kotlin("stdlib")) + + implementation("ch.qos.logback:logback-classic:$logback_version") + + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("io.ktor:ktor-metrics:$ktor_version") + implementation("io.ktor:ktor-html-builder:$ktor_version") +} + +jib { + container { + ports = listOf("8080") + mainClass = main_class + + // good defauls intended for Java 8 (>= 8u191) containers + jvmFlags = listOf( + "-server", + "-Djava.awt.headless=true", + "-XX:InitialRAMFraction=2", + "-XX:MinRAMFraction=2", + "-XX:MaxRAMFraction=2", + "-XX:+UseG1GC", + "-XX:MaxGCPauseMillis=100", + "-XX:+UseStringDeduplication" + ) + } +} + +repositories { + mavenCentral() +} diff --git a/examples/ktor/gradle.properties b/examples/ktor/gradle.properties new file mode 100644 index 0000000000..29e08e8ca8 --- /dev/null +++ b/examples/ktor/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.jar b/examples/ktor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/ktor/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.properties b/examples/ktor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..6b3851a8ad --- /dev/null +++ b/examples/ktor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/ktor/gradlew b/examples/ktor/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/ktor/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/ktor/gradlew.bat b/examples/ktor/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/ktor/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/ktor/settings.gradle.kts b/examples/ktor/settings.gradle.kts new file mode 100644 index 0000000000..c89c4b6286 --- /dev/null +++ b/examples/ktor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ktor-jib-example" diff --git a/examples/ktor/src/main/kotlin/example/ktor/App.kt b/examples/ktor/src/main/kotlin/example/ktor/App.kt new file mode 100644 index 0000000000..2cf7b70e6b --- /dev/null +++ b/examples/ktor/src/main/kotlin/example/ktor/App.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.ktor + +import com.codahale.metrics.JmxReporter +import io.ktor.application.* +import io.ktor.features.CallLogging +import io.ktor.features.DefaultHeaders +import io.ktor.features.StatusPages +import io.ktor.html.respondHtml +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.metrics.Metrics +import io.ktor.request.receiveOrNull +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.route +import io.ktor.routing.routing +import kotlinx.html.* +import java.util.concurrent.TimeUnit + +/** + * Ktor Example Application + * + * See documentation at https://ktor.io/servers/index.html + */ +fun Application.main() { + init() + router() +} + +// Variable for maintaining health-check status +var healthy = true + +/** + * For enabling Metrics - https://ktor.io/servers/features/metrics.html + */ +val metricsEnabled = (true == System.getenv("KTOR_METRICS_ENABLED")?.toBoolean()) + +/** + * For enabling route tracing - https://ktor.io/servers/features/routing.html#tracing + */ +val routeTracing = (true == System.getenv("KTOR_ROUTE_TRACING")?.toBoolean()) + +/** + * A method for defining the routes - https://ktor.io/servers/structure.html#extracting-routes + */ +private fun Application.router() { + log.info("Route tracing enabled? $routeTracing") + routing { + // If enabled, will print out a tree of the routes that were matched while trying to process the URI + if (routeTracing) { + trace { application.log.trace(it.buildText()) } + } + + get("/") { + call.respondText("Hello World!") + } + + get("/info") { + // The `respondHtml` extension method is available at the `ktor-html-builder` artifact. + // It provides a DSL for building HTML to a Writer, potentially in a chunked way. + // More information about this DSL: https://ktor.io/servers/features/templates/html-dsl.html + call.respondHtml { + head { + title { +"Ktor: Jib" } + } + body { + val runtime = Runtime.getRuntime() + h1 { +"Hello from Ktor sample application built with Jib" } + val runtimeString = "Runtime.getRuntime()" + p { +"$runtimeString.availableProcessors(): ${runtime.availableProcessors()}" } + p { +"$runtimeString.freeMemory(): ${runtime.freeMemory()}" } + p { +"$runtimeString.totalMemory(): ${runtime.totalMemory()}" } + p { +"$runtimeString.maxMemory(): ${runtime.maxMemory()}" } + p { +"System.getProperty(\"user.name\"): ${System.getProperty("user.name")}" } + } + } + } + + route("/healthz") { + // Some simple toy health check example + post("/") { + when(call.receiveOrNull()) { + "infect" -> { + healthy = false + call.respond(HttpStatusCode.Accepted) + } + "heal" -> { + healthy = true + call.respond(HttpStatusCode.Accepted) + } + else -> { + // Showcase the StatusPage installation feature + throw IllegalAccessException("POST /healthz only accepts 'infect' or 'heal'") + } + } + } + get("/") { + var txt = "ok" + var status = HttpStatusCode.OK + if (!healthy) { + txt = "failure" + status = HttpStatusCode.ServiceUnavailable + } + call.respondText(txt.toUpperCase(), status = status) + } + } + } +} + +/** + * A method for installing features - https://ktor.io/servers/features.html#installing + */ +private fun Application.init() { + // This adds automatically Date and Server headers to each response, and would allow you to configure + // additional headers served to each response. + install(DefaultHeaders) + + // This allows handling exceptions and status codes in a specific way. + install(StatusPages) { + // For all thrown Exceptions, log, and return the message of it as text + exception { e -> + val message = e.localizedMessage + application.log.error(message) + call.respondText(message, + ContentType.Text.Plain, HttpStatusCode.InternalServerError) + } + } + + // This uses use the logger to log every call (request/response) + install(CallLogging) + + // Conditionally enable the Metrics API + log.info("Metrics Registry enabled? $metricsEnabled") + if (metricsEnabled) { + install(Metrics) { + JmxReporter.forRegistry(registry) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build() + .start() + } + } +} \ No newline at end of file diff --git a/examples/ktor/src/main/resources/application.conf b/examples/ktor/src/main/resources/application.conf new file mode 100644 index 0000000000..42b1a9a79d --- /dev/null +++ b/examples/ktor/src/main/resources/application.conf @@ -0,0 +1,13 @@ +# Ktor configuration - https://ktor.io/servers/configuration.html +ktor { + deployment { + port = 8080 + } + + application { + # Defining values twice is way to define a default with substitutable value + id = "Ktor Jib Example" + id = ${?KTOR_APP_ID} + modules = [ example.ktor.AppKt.main ] + } +} \ No newline at end of file diff --git a/examples/ktor/src/main/resources/logback.xml b/examples/ktor/src/main/resources/logback.xml new file mode 100644 index 0000000000..9e78155525 --- /dev/null +++ b/examples/ktor/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/examples/micronaut/README.md b/examples/micronaut/README.md new file mode 100644 index 0000000000..adc873f434 --- /dev/null +++ b/examples/micronaut/README.md @@ -0,0 +1,52 @@ +# Containerize a [Micronaut](http://micronaut.io/) app with Jib + +This is an example of how to easily build a Docker image for a [Micronaut framework Groovy/Java application](http://guides.micronaut.io/creating-your-first-micronaut-app-groovy/guide/index.html) with Jib. + + +

+ + Dockerize Micronaut app with Jib + +

+ +## Quickstart + +### With Docker + +```shell +./gradlew jibDockerBuild + +docker run -d -p 8080:8080 micronaut-jib:0.1 +``` +```shell +curl localhost:8080/hello +> Hello World +``` + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Dockerize%20and%20run%20a%20%22Hello%20World%22%20%40Java%20%40micronautfw%20app%20with%20%23Jib%20in%20seconds&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/micronaut&hashtags=docker,kubernetes) + +### With Kubernetes + +```shell +IMAGE= + +./gradlew jib --image=$IMAGE + +kubectl run micronaut-jib --image=$IMAGE --port=8080 --restart=Never + +# Wait until pod is running +kubectl port-forward micronaut-jib 8080 > /dev/null 2>&1 & +``` +```shell +curl localhost:8080/hello +> Hello World +``` + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run%20a%20%22Hello%20World%22%20%40java%20%40micronautfw%20app%20on%20%23Kubernetes%20with%20%23Jib%20in%20seconds&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/micronaut&hashtags=docker,kubernetes) + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). +Learn [more about Micronaut](https://micronaut.io). diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle new file mode 100644 index 0000000000..c21f517bd2 --- /dev/null +++ b/examples/micronaut/build.gradle @@ -0,0 +1,54 @@ +plugins { + id "groovy" + id "com.github.johnrengelman.shadow" version "5.2.0" + id "application" + id 'com.google.cloud.tools.jib' version '3.5.4' +} + +version "0.1" +group 'example.micronaut-jib' + +repositories { + mavenCentral() +} + +configurations { + // for dependencies that are needed for development only + developmentOnly +} + +dependencies { + compileOnly(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) + compileOnly("io.micronaut:micronaut-inject-groovy") + implementation(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) + implementation("io.micronaut:micronaut-inject") + implementation("io.micronaut:micronaut-validation") + implementation("io.micronaut:micronaut-runtime-groovy") + implementation("javax.annotation:javax.annotation-api") + implementation("io.micronaut:micronaut-http-server-netty") + implementation("io.micronaut:micronaut-http-client") + runtimeOnly("ch.qos.logback:logback-classic:1.2.3") + testImplementation enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion") + testImplementation("io.micronaut:micronaut-inject-groovy") + testImplementation("org.spockframework:spock-core") { + exclude group: "org.codehaus.groovy", module: "groovy-all" + } + testImplementation("io.micronaut.test:micronaut-test-spock") +} + +test.classpath += configurations.developmentOnly + +mainClassName = "example.micronaut.Application" + +tasks.withType(GroovyCompile) { + groovyOptions.forkOptions.jvmArgs.add('-Dgroovy.parameters=true') +} + +shadowJar { + mergeServiceFiles() +} + +tasks.withType(JavaExec) { + classpath += configurations.developmentOnly + jvmArgs('-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote') +} diff --git a/examples/micronaut/dockerize-micronaut-jib.gif b/examples/micronaut/dockerize-micronaut-jib.gif new file mode 100644 index 0000000000..463ca21971 Binary files /dev/null and b/examples/micronaut/dockerize-micronaut-jib.gif differ diff --git a/examples/micronaut/gradle.properties b/examples/micronaut/gradle.properties new file mode 100644 index 0000000000..4e14bde9ae --- /dev/null +++ b/examples/micronaut/gradle.properties @@ -0,0 +1 @@ +micronautVersion=2.0.0.M3 \ No newline at end of file diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.jar b/examples/micronaut/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/micronaut/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.properties b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..6b3851a8ad --- /dev/null +++ b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/micronaut/gradlew b/examples/micronaut/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/micronaut/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/micronaut/gradlew.bat b/examples/micronaut/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/micronaut/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/micronaut/settings.gradle b/examples/micronaut/settings.gradle new file mode 100644 index 0000000000..3770a05315 --- /dev/null +++ b/examples/micronaut/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'micronaut-jib' diff --git a/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy b/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy new file mode 100644 index 0000000000..7db7c36e04 --- /dev/null +++ b/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy @@ -0,0 +1,12 @@ +package example.micronaut + +import groovy.transform.CompileStatic +import io.micronaut.runtime.Micronaut + +@CompileStatic +class Application { + + static void main(String[] args) { + Micronaut.run(Application.class) + } +} \ No newline at end of file diff --git a/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy new file mode 100644 index 0000000000..c3414a3051 --- /dev/null +++ b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy @@ -0,0 +1,18 @@ +package example.micronaut + +import groovy.transform.CompileStatic +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces + +@CompileStatic +@Controller("/hello") // <1> +class HelloController { + + @Produces(MediaType.TEXT_PLAIN) + @Get("/") // <2> + String index() { + "Hello World" // <3> + } +} diff --git a/examples/micronaut/src/main/resources/application.yml b/examples/micronaut/src/main/resources/application.yml new file mode 100644 index 0000000000..0a6f9ed238 --- /dev/null +++ b/examples/micronaut/src/main/resources/application.yml @@ -0,0 +1,3 @@ +micronaut: + application: + name: examples \ No newline at end of file diff --git a/examples/micronaut/src/main/resources/logback.xml b/examples/micronaut/src/main/resources/logback.xml new file mode 100644 index 0000000000..afaebf8e17 --- /dev/null +++ b/examples/micronaut/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy new file mode 100644 index 0000000000..8517a5c5cc --- /dev/null +++ b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy @@ -0,0 +1,30 @@ +package example.micronaut + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.RxHttpClient +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class HelloControllerSpec extends Specification { + + @Shared + @AutoCleanup // <1> + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) // <2> + + @Shared + @AutoCleanup + RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) // <3> + + void "test hello world response"() { + when: + HttpRequest request = HttpRequest.GET('/hello') // <4> + String rsp = client.toBlocking().retrieve(request) + + then: + rsp == "Hello World" + } + +} \ No newline at end of file diff --git a/examples/multi-module/.mvn/wrapper/MavenWrapperDownloader.java b/examples/multi-module/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000000..d475a89ce1 --- /dev/null +++ b/examples/multi-module/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/multi-module/.mvn/wrapper/maven-wrapper.properties b/examples/multi-module/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000000..00d32aab1d --- /dev/null +++ b/examples/multi-module/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md new file mode 100644 index 0000000000..e47a5e9727 --- /dev/null +++ b/examples/multi-module/README.md @@ -0,0 +1,104 @@ +# Multi-module example + +This example shows how to build multiple containers for a multi-module project in both **Maven** and **Gradle**. + +# How the example is set up + +The project consists of two microservices and a library: + +1. `name-service` - responds with a name +1. `shared-library` - a project dependency used by `name-service` +1. `hello-service` - calls `name-service` and responds with a greeting + +The **Maven** project is set up with a parent POM ([`pom.xml`](pom.xml)) that defines most of the common build configuration. The module POMs ([`name-service/pom.xml`](name-service/pom.xml) and [`hello-service/pom.xml`](hello-service/pom.xml)) just define inheritance on the parent POM. However, if needed, the module POMs can define custom configuration on `jib-maven-plugin` specific to that module. + +The **Gradle** project is set up with a parent [`build.gradle`](build.gradle) that sets some common configuration up for all projects, with each sub-project containing its own `build.gradle` with some custom configuration. [`settings.gradle`](settings.gradle) defines which modules to include in the overall build. + +## Reproducibility of dependency module `shared-library` + +Since dependency module builds happen with the underlying build system +(maven/gradle), we must add some extra configuration to ensure that the +resulting `jar` that is built conforms to our reproducibility expectations. +The module [`shared-library`](shared-library) uses the [Reproducible Build Maven Plugin](https://zlika.github.io/reproducible-build-maven-plugin/) +for maven, and some special `Jar` properties ([`preserveFileTimestamps`](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:preserveFileTimestamps), +[`reproducibleFileOrder`](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:reproducibleFileOrder)) +in gradle to achieve this. This configuration can be seen in the +`shared-library`'s [`pom.xml`](shared-library/pom.xml) and [`build.gradle`](shared-library/build.gradle). + +Care must be taken when adding custom attributes to a `MANIFEST.MF`. +Attributes whose values change on every build can affect reproducibility even +with the modifications outlined above. + +# How to run + +Set the `PROJECT_ID` environment variable to your own Google Cloud Platform project: + +```shell +export PROJECT_ID=$(gcloud config list --format 'value(core.project)') +``` + +Run the **Maven** build: + +```shell +# build everything +./mvnw package jib:build + +# build just hello-service +./mvnw compile jib:build -pl hello-service + +# build name-service (with dependency on shared-library) +# you must use "package" for jib to correctly package "shared-library" with the +# "name-service" container +./mvnw package jib:build -pl name-service -am +``` + +Run the **Gradle** build: + +```shell +# build everything +./gradlew jib + +# build just hello-service +./gradlew :hello-service:jib + +# build name-service (with dependency on shared-library) +./gradlew :name-service:jib +``` + +You can also run `./maven-build.sh` or `./gradle-build.sh` as a shorthand. + +# Where are the containers + +The output of the build should have the container image references highlighted in cyan. You can expect them to be at: + +- `name-service`: `gcr.io/PROJECT_ID/name-service:0.1.0` +- `hello-service`: `gcr.io/PROJECT_ID/hello-service:0.1.0` + +# How to run on Kubernetes + +[`kubernetes.yaml`](kubernetes.yaml) defines the manifests for running the two microservices on Kubernetes. Make sure to open the file and change `PROJECT_ID` to your own Google Cloud Platform project. + +Create a Kubernetes cluster: + +```shell +gcloud container clusters create jib +``` + +Apply to your Kubernetes cluster: + +```shell +kubectl apply -f kubernetes.yaml +``` + +Find the `EXTERNAL-IP` of the `hello-service`. + +``` +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +svc/hello-service LoadBalancer 10.19.243.223 35.237.89.148 80:30196/TCP 1m +``` + +Visit the IP in your web browser and you should see: + +``` +Hello Jib Multimodule: A string from 'shared-library' +``` diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle new file mode 100644 index 0000000000..5cb2c31702 --- /dev/null +++ b/examples/multi-module/build.gradle @@ -0,0 +1,6 @@ +// Define plugin versions here, but only apply them where we need to +plugins { + id 'org.springframework.boot' version '2.0.3.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.6.RELEASE' apply false + id 'com.google.cloud.tools.jib' version '3.5.4' apply false +} diff --git a/examples/multi-module/gradle-build.sh b/examples/multi-module/gradle-build.sh new file mode 100755 index 0000000000..ef556aca3d --- /dev/null +++ b/examples/multi-module/gradle-build.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -ex + +export PROJECT_ID=$(gcloud config list --format 'value(core.project)') +./gradlew jib diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.jar b/examples/multi-module/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/multi-module/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..6b3851a8ad --- /dev/null +++ b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/multi-module/gradlew b/examples/multi-module/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/multi-module/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/multi-module/gradlew.bat b/examples/multi-module/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/multi-module/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/multi-module/hello-service/build.gradle b/examples/multi-module/hello-service/build.gradle new file mode 100644 index 0000000000..58fff228a2 --- /dev/null +++ b/examples/multi-module/hello-service/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +// IMPORTANT: Set the environment variable PROJECT_ID to your own Google Cloud Platform project. +jib.to.image = "gcr.io/${System.getenv('PROJECT_ID')}/${project.name}:${version}" diff --git a/examples/multi-module/hello-service/gradle.properties b/examples/multi-module/hello-service/gradle.properties new file mode 100644 index 0000000000..3f63f85a9b --- /dev/null +++ b/examples/multi-module/hello-service/gradle.properties @@ -0,0 +1 @@ +version = 0.1.0 diff --git a/examples/multi-module/hello-service/pom.xml b/examples/multi-module/hello-service/pom.xml new file mode 100644 index 0000000000..3e27259e22 --- /dev/null +++ b/examples/multi-module/hello-service/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + hello-service + 0.1.0 + + + + com.example + jib-multimodule + 0.1.0 + + diff --git a/examples/multi-module/hello-service/src/main/java/hello/Application.java b/examples/multi-module/hello-service/src/main/java/hello/Application.java new file mode 100644 index 0000000000..8c26fec153 --- /dev/null +++ b/examples/multi-module/hello-service/src/main/java/hello/Application.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/examples/multi-module/hello-service/src/main/java/hello/HelloController.java b/examples/multi-module/hello-service/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..bf49fc9b1d --- /dev/null +++ b/examples/multi-module/hello-service/src/main/java/hello/HelloController.java @@ -0,0 +1,15 @@ +package hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String sayHello() { + String name = new RestTemplate().getForEntity("http://name-service", String.class).getBody(); + return "Hello " + name; + } +} diff --git a/examples/multi-module/kubernetes.yaml b/examples/multi-module/kubernetes.yaml new file mode 100644 index 0000000000..2b5018fb6d --- /dev/null +++ b/examples/multi-module/kubernetes.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: name-service + name: name-service +spec: + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app: name-service +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: name-service + name: name-service +spec: + replicas: 1 + selector: + matchLabels: + app: name-service + template: + metadata: + labels: + app: name-service + spec: + containers: + - name: name-service + image: gcr.io/PROJECT_ID/name-service:0.1.0 + ports: + - name: http + containerPort: 8080 + imagePullPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: hello-service + name: hello-service +spec: + type: LoadBalancer + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app: hello-service +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: hello-service + name: hello-service +spec: + replicas: 1 + selector: + matchLabels: + app: hello-service + template: + metadata: + labels: + app: hello-service + spec: + containers: + - name: hello-service + image: gcr.io/PROJECT_ID/hello-service:0.1.0 + ports: + - name: http + containerPort: 8080 + imagePullPolicy: Always diff --git a/examples/multi-module/maven-build.sh b/examples/multi-module/maven-build.sh new file mode 100755 index 0000000000..7afa910f66 --- /dev/null +++ b/examples/multi-module/maven-build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -ex + +export PROJECT_ID=$(gcloud config list --format 'value(core.project)') +# if there are no intermodule dependencies, compile is enough to complete a jib build. +./mvnw compile jib:build -pl hello-service + +# multi module builds with dependencies on other modules in the build require that the +# "package" phase is executed as part of the build to correctly include module dependencies. +./mvnw package jib:build -pl name-service -am diff --git a/examples/multi-module/mvnw b/examples/multi-module/mvnw new file mode 100755 index 0000000000..961a825001 --- /dev/null +++ b/examples/multi-module/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/multi-module/mvnw.cmd b/examples/multi-module/mvnw.cmd new file mode 100755 index 0000000000..830073a17e --- /dev/null +++ b/examples/multi-module/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/multi-module/name-service/build.gradle b/examples/multi-module/name-service/build.gradle new file mode 100644 index 0000000000..977be7685b --- /dev/null +++ b/examples/multi-module/name-service/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation project(':shared-library') +} + +// IMPORTANT: Set the environment variable PROJECT_ID to your own Google Cloud Platform project. +jib.to.image = "gcr.io/${System.getenv('PROJECT_ID')}/${project.name}:${version}" diff --git a/examples/multi-module/name-service/gradle.properties b/examples/multi-module/name-service/gradle.properties new file mode 100644 index 0000000000..3f63f85a9b --- /dev/null +++ b/examples/multi-module/name-service/gradle.properties @@ -0,0 +1 @@ +version = 0.1.0 diff --git a/examples/multi-module/name-service/pom.xml b/examples/multi-module/name-service/pom.xml new file mode 100644 index 0000000000..8fa9dbe679 --- /dev/null +++ b/examples/multi-module/name-service/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + name-service + 0.1.0 + + + + com.example + jib-multimodule + 0.1.0 + + + + + + com.example + shared-library + 0.1.0 + + + diff --git a/examples/multi-module/name-service/src/main/java/name/Application.java b/examples/multi-module/name-service/src/main/java/name/Application.java new file mode 100644 index 0000000000..26a558778c --- /dev/null +++ b/examples/multi-module/name-service/src/main/java/name/Application.java @@ -0,0 +1,12 @@ +package name; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/examples/multi-module/name-service/src/main/java/name/NameController.java b/examples/multi-module/name-service/src/main/java/name/NameController.java new file mode 100644 index 0000000000..0ce97199d7 --- /dev/null +++ b/examples/multi-module/name-service/src/main/java/name/NameController.java @@ -0,0 +1,14 @@ +package name; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; +import common.SharedUtils; + +@RestController +public class NameController { + + @RequestMapping("/") + public String getText() { + return "Jib Multimodule: " + SharedUtils.getText(); + } +} diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml new file mode 100644 index 0000000000..c5eff663cf --- /dev/null +++ b/examples/multi-module/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + com.example + jib-multimodule + pom + 0.1.0 + Jib Multi-module Example + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.3.RELEASE + + + + + name-service + hello-service + shared-library + + + + + 1.8 + + + gcr.io/${env.PROJECT_ID}/${project.artifactId}:${project.version} + + + + + + + + com.google.cloud.tools + jib-maven-plugin + 3.5.2 + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + diff --git a/examples/multi-module/settings.gradle b/examples/multi-module/settings.gradle new file mode 100644 index 0000000000..d37f386743 --- /dev/null +++ b/examples/multi-module/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'jib-multimodule' +include 'name-service', 'hello-service', 'shared-library' diff --git a/examples/multi-module/shared-library/build.gradle b/examples/multi-module/shared-library/build.gradle new file mode 100644 index 0000000000..990d8c9a27 --- /dev/null +++ b/examples/multi-module/shared-library/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +// Since this library is included as a jar in our jib projects, we want the +// jar to built reproducibly. +jar { + preserveFileTimestamps false + reproducibleFileOrder true +} diff --git a/examples/multi-module/shared-library/gradle.properties b/examples/multi-module/shared-library/gradle.properties new file mode 100644 index 0000000000..3f63f85a9b --- /dev/null +++ b/examples/multi-module/shared-library/gradle.properties @@ -0,0 +1 @@ +version = 0.1.0 diff --git a/examples/multi-module/shared-library/pom.xml b/examples/multi-module/shared-library/pom.xml new file mode 100644 index 0000000000..ff5de91880 --- /dev/null +++ b/examples/multi-module/shared-library/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + shared-library + 0.1.0 + + + + com.example + jib-multimodule + 0.1.0 + + + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + io.github.zlika + reproducible-build-maven-plugin + 0.11 + + + run-when-packaged + + strip-jar + + package + + + + + + diff --git a/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java b/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java new file mode 100644 index 0000000000..fd3ad5202f --- /dev/null +++ b/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java @@ -0,0 +1,8 @@ +package common; + +public class SharedUtils { + + public static String getText() { + return "A string from 'shared-library'"; + } +} \ No newline at end of file diff --git a/examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java b/examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000000..d475a89ce1 --- /dev/null +++ b/examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/spring-boot/.mvn/wrapper/maven-wrapper.properties b/examples/spring-boot/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000000..00d32aab1d --- /dev/null +++ b/examples/spring-boot/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/examples/spring-boot/README.md b/examples/spring-boot/README.md new file mode 100644 index 0000000000..822a67a761 --- /dev/null +++ b/examples/spring-boot/README.md @@ -0,0 +1,56 @@ +# Dockerize a Spring Boot application using Jib + +This is an example of how to easily build a Docker image for a Spring Boot application with Jib. + +## Try it yourself + +You can containerize the application with one of the following commands. + +**Maven:** +```shell +./mvnw compile jib:build -Dimage= +``` + +**Gradle:** +```shell +./gradlew jib --image= +``` + +## Deploying to Kubernetes using `kubectl` + + +

+ + Dockerize Spring Boot app with Jib and deploy to Kubernetes + +

+ +*Make sure you have `kubectl` installed and [configured with a cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster).* + +```shell +IMAGE= + +./mvnw compile jib:build -Dimage=$IMAGE + +kubectl run spring-boot-jib --image=$IMAGE --port=8080 --restart=Never + +# Wait until pod is running +kubectl port-forward spring-boot-jib 8080 +``` +```shell +curl localhost:8080 +> Greetings from Spring Boot and Jib! +``` + +\* If you are using Gradle, use `./gradlew jib --image=$IMAGE` instead of the `./mvnw` command + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run+a+%40springboot+app+on+%23Kubernetes+in+seconds+%40kubernetesio+%23jib+%23java&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot&hashtags=docker) + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/spring-boot) diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle new file mode 100644 index 0000000000..729f92a1a4 --- /dev/null +++ b/examples/spring-boot/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' version '2.1.6.RELEASE' + id 'io.spring.dependency-management' version '1.0.6.RELEASE' + id 'com.google.cloud.tools.jib' version '3.5.4' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} diff --git a/examples/spring-boot/dockerize-spring-boot-jib.gif b/examples/spring-boot/dockerize-spring-boot-jib.gif new file mode 100644 index 0000000000..0b9db9cd5e Binary files /dev/null and b/examples/spring-boot/dockerize-spring-boot-jib.gif differ diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..6b3851a8ad --- /dev/null +++ b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/spring-boot/gradlew b/examples/spring-boot/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/spring-boot/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/spring-boot/gradlew.bat b/examples/spring-boot/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/spring-boot/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/spring-boot/mvnw b/examples/spring-boot/mvnw new file mode 100755 index 0000000000..961a825001 --- /dev/null +++ b/examples/spring-boot/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/spring-boot/mvnw.cmd b/examples/spring-boot/mvnw.cmd new file mode 100755 index 0000000000..830073a17e --- /dev/null +++ b/examples/spring-boot/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml new file mode 100644 index 0000000000..f36414fe25 --- /dev/null +++ b/examples/spring-boot/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + example + spring-boot-k8s-example + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + + 1.8 + + + + + + com.google.cloud.tools + jib-maven-plugin + 3.5.2 + + + + diff --git a/jib-core/src/test/resources/application/dependencies/libraryB.jar b/examples/spring-boot/settings.gradle similarity index 100% rename from jib-core/src/test/resources/application/dependencies/libraryB.jar rename to examples/spring-boot/settings.gradle diff --git a/examples/spring-boot/src/main/java/hello/Application.java b/examples/spring-boot/src/main/java/hello/Application.java new file mode 100644 index 0000000000..096bd4c9d8 --- /dev/null +++ b/examples/spring-boot/src/main/java/hello/Application.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/examples/spring-boot/src/main/java/hello/HelloController.java b/examples/spring-boot/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..8052d26866 --- /dev/null +++ b/examples/spring-boot/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Greetings from Spring Boot and Jib!"; + } +} diff --git a/examples/vertx/README.md b/examples/vertx/README.md new file mode 100644 index 0000000000..0f25429920 --- /dev/null +++ b/examples/vertx/README.md @@ -0,0 +1,15 @@ +# Containerize a [Eclipse Vert.x](https://vertx.io/) application with Jib + +This is an example of how to easily build a Docker image for a [Eclipse Vert.x application](https://vertx.io/) with Jib. + +```shell +./gradlew jibDockerBuild + +docker run -d --rm -p 8080:8080 vertx-jib-example +``` + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [more about Eclipse Vert.x](https://vertx.io). diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle new file mode 100644 index 0000000000..0ae08b7987 --- /dev/null +++ b/examples/vertx/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'io.vertx.vertx-plugin' version '0.1.0' + id 'com.google.cloud.tools.jib' version '3.5.4' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'io.vertx:vertx-web' + implementation 'ch.qos.logback:logback-classic:1.2.3' +} + +vertx { + vertxVersion = '3.6.0.CR1' + mainVerticle = 'example.vertx.MainVerticle' + jvmArgs = ['-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory'] +} + +jib { + container { + ports = ['8080'] + } +} diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.jar b/examples/vertx/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/examples/vertx/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.properties b/examples/vertx/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..6b3851a8ad --- /dev/null +++ b/examples/vertx/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/vertx/gradlew b/examples/vertx/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/vertx/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/vertx/gradlew.bat b/examples/vertx/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/vertx/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/vertx/settings.gradle b/examples/vertx/settings.gradle new file mode 100644 index 0000000000..268b013937 --- /dev/null +++ b/examples/vertx/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'vertx-jib-example' diff --git a/examples/vertx/src/main/java/example/vertx/MainVerticle.java b/examples/vertx/src/main/java/example/vertx/MainVerticle.java new file mode 100644 index 0000000000..c74eb02863 --- /dev/null +++ b/examples/vertx/src/main/java/example/vertx/MainVerticle.java @@ -0,0 +1,56 @@ +package example.vertx; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MainVerticle extends AbstractVerticle { + + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new MainVerticle()); + } + + private final Logger logger = LoggerFactory.getLogger(MainVerticle.class); + + @Override + public void start(Future startFuture) { + Router router = Router.router(vertx); + + router.get("/").handler(this::hello); + router.get("/time").handler(this::now); + + vertx.createHttpServer() + .requestHandler(router) + .listen(8080, asyncStart -> { + if (asyncStart.succeeded()) { + startFuture.complete(); + logger.info("HTTP server running on port 8080"); + } else { + logger.error("Woops", asyncStart.cause()); + startFuture.fail(asyncStart.cause()); + } + }); + } + + private void hello(RoutingContext context) { + logger.info("Hello request from {}", context.request().remoteAddress()); + context.response() + .putHeader("Content-Type", "text/plain") + .end("Hello from Vert.x!"); + } + + private void now(RoutingContext context) { + logger.info("Time request from {}", context.request().remoteAddress()); + JsonObject data = new JsonObject() + .put("powered-by", "vertx") + .put("current-time", System.currentTimeMillis()); + context.response() + .putHeader("Content-Type", "application/json") + .end(data.encode()); + } +} diff --git a/examples/vertx/src/main/resources/logback.xml b/examples/vertx/src/main/resources/logback.xml new file mode 100644 index 0000000000..e60adb276e --- /dev/null +++ b/examples/vertx/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..7fe7ea26ee --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1024m +org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ec991f9aa1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-build-plan/CHANGELOG.md b/jib-build-plan/CHANGELOG.md new file mode 100644 index 0000000000..8695aa821e --- /dev/null +++ b/jib-build-plan/CHANGELOG.md @@ -0,0 +1,39 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +- Added explicit `toString()` methods to `FileEntry` and `FilePermissions`. ([#2714](https://github.com/GoogleContainerTools/jib/pull/2714)) + +### Changed + +### Fixed + +## 0.4.0 + +### Changed + +- Replaced `BiFunction` usage with `FilePermissionsProvider`, `ModificationTimeProvider` and `OwnershipProvider`. ([#2638](https://github.com/GoogleContainerTools/jib/issues/2638)) + +## 0.3.1 + +### Added + +- Added `Platform` class representing an image platform. ([#2584](https://github.com/GoogleContainerTools/jib/pull/2584)) +- Added `get/setPlatforms()` and `addPlatform()` to `ContainerBuildPlan` for setting and getting image platforms. ([#2584](https://github.com/GoogleContainerTools/jib/pull/2584)) + +### Changed + +- Removed `get/setOsHint()` and `get/setArchitectureHint()` in favor of `get/setPlatforms()` and `addPlatform()`. ([#2584](https://github.com/GoogleContainerTools/jib/pull/2584)) + +### Fixed + +- Fixed the critical bug that the default `Platform` in `ContainerBuildPlan` has OS and architecture values switched with each other. ([#2597](https://github.com/GoogleContainerTools/jib/pull/2597)) + +## 0.2.0 + +### Added + +- Added file ownership information in `FileEntry` and `FileEntriesLayer`. ([#2494](https://github.com/GoogleContainerTools/jib/pull/2494)) diff --git a/jib-build-plan/build.gradle b/jib-build-plan/build.gradle new file mode 100644 index 0000000000..6adb928055 --- /dev/null +++ b/jib-build-plan/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' +} + +dependencies { + compileOnly dependencyStrings.JSR305 + + testImplementation dependencyStrings.GUAVA + testImplementation dependencyStrings.JUNIT + testImplementation dependencyStrings.MOCKITO_CORE + testImplementation dependencyStrings.SLF4J_API + testImplementation dependencyStrings.SYSTEM_RULES +} + +jar { + manifest { + attributes 'Implementation-Version': archiveVersion + attributes 'Automatic-Module-Name': 'com.google.cloud.tools.jib.api.buildplan' + + // OSGi metadata + attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib.api.buildplan' + attributes 'Bundle-Name': 'Jib Container Build Plan API' + attributes 'Bundle-Vendor': 'Google LLC' + attributes 'Bundle-DocURL': 'https://github.com/GoogleContainerTools/jib' + attributes 'Bundle-License': 'https://www.apache.org/licenses/LICENSE-2.0' + attributes 'Export-Package': 'com.google.cloud.tools.jib.api.buildplan' + } +} + +/* RELEASE */ +configureMavenRelease() + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Jib Container Build Plan API' + description = 'Jib Container Build Plan API' + } + from components.java + } + } +} + +// Release plugin (git release commits and version updates) +release { + tagTemplate = 'v$version-build-plan' + git { + requireBranch = /^build-plan-release-v\d+.*$/ //regex + } +} +/* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +/* ECLIPSE */ diff --git a/jib-build-plan/gradle.properties b/jib-build-plan/gradle.properties new file mode 100644 index 0000000000..6b7e8ff821 --- /dev/null +++ b/jib-build-plan/gradle.properties @@ -0,0 +1 @@ +version = 0.4.1-SNAPSHOT diff --git a/jib-build-plan/kokoro/release_build.sh b/jib-build-plan/kokoro/release_build.sh new file mode 100755 index 0000000000..1cce405bf0 --- /dev/null +++ b/jib-build-plan/kokoro/release_build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Fail on any error. +set -o errexit +# Display commands to stderr. +set -o xtrace + +cd github/jib +./gradlew :jib-build-plan:prepareRelease diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPath.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPath.java new file mode 100644 index 0000000000..7c85a22662 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPath.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.cloud.tools.jib.buildplan.UnixPathParser; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import javax.annotation.concurrent.Immutable; + +/** + * Represents a Unix-style path in absolute form (containing all path components relative to the + * file system root {@code /}). + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class AbsoluteUnixPath { + + /** + * Gets a new {@link AbsoluteUnixPath} from a Unix-style path string. The path must begin with a + * forward slash ({@code /}). + * + * @param unixPath the Unix-style path string in absolute form + * @return a new {@link AbsoluteUnixPath} + */ + public static AbsoluteUnixPath get(String unixPath) { + if (!unixPath.startsWith("/")) { + throw new IllegalArgumentException("Path does not start with forward slash (/): " + unixPath); + } + + return new AbsoluteUnixPath(UnixPathParser.parse(unixPath)); + } + + /** + * Gets a new {@link AbsoluteUnixPath} from a {@link Path}. The {@code path} must be absolute + * (indicated by a non-null {@link Path#getRoot}). + * + * @param path the absolute {@link Path} to convert to an {@link AbsoluteUnixPath}. + * @return a new {@link AbsoluteUnixPath} + */ + public static AbsoluteUnixPath fromPath(Path path) { + if (path.getRoot() == null) { + throw new IllegalArgumentException( + "Cannot create AbsoluteUnixPath from non-absolute Path: " + path); + } + + List pathComponents = new ArrayList<>(path.getNameCount()); + path.forEach(component -> pathComponents.add(component.toString())); + return new AbsoluteUnixPath(pathComponents); + } + + /** Path components after the file system root. This should always match {@link #unixPath}. */ + private final List pathComponents; + + /** + * Unix-style path, in absolute form. Does not end with trailing slash, except for the file system + * root ({@code /}). This should always match {@link #pathComponents}. + */ + private final String unixPath; + + private AbsoluteUnixPath(List pathComponents) { + this.pathComponents = pathComponents; + + StringJoiner pathJoiner = new StringJoiner("/", "/", ""); + for (String pathComponent : pathComponents) { + pathJoiner.add(pathComponent); + } + unixPath = pathJoiner.toString(); + } + + /** + * Resolves this path against another relative path. + * + * @param relativeUnixPath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(RelativeUnixPath relativeUnixPath) { + int newSize = pathComponents.size() + relativeUnixPath.getRelativePathComponents().size(); + List newPathComponents = new ArrayList<>(newSize); + + newPathComponents.addAll(pathComponents); + newPathComponents.addAll(relativeUnixPath.getRelativePathComponents()); + return new AbsoluteUnixPath(newPathComponents); + } + + /** + * Resolves this path against another relative path (by the name elements of {@code + * relativePath}). + * + * @param relativePath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(Path relativePath) { + if (relativePath.getRoot() != null) { + throw new IllegalArgumentException("Cannot resolve against absolute Path: " + relativePath); + } + + return AbsoluteUnixPath.fromPath(Paths.get(unixPath).resolve(relativePath)); + } + + /** + * Resolves this path against another relative Unix path in string form. + * + * @param relativeUnixPath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(String relativeUnixPath) { + return resolve(RelativeUnixPath.get(relativeUnixPath)); + } + + /** + * Returns the string form of the absolute Unix-style path. + * + * @return the string form + */ + @Override + public String toString() { + return unixPath; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AbsoluteUnixPath)) { + return false; + } + AbsoluteUnixPath otherAbsoluteUnixPath = (AbsoluteUnixPath) other; + return unixPath.equals(otherAbsoluteUnixPath.unixPath); + } + + @Override + public int hashCode() { + return unixPath.hashCode(); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlan.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlan.java new file mode 100644 index 0000000000..e11e29430e --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlan.java @@ -0,0 +1,507 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** Describes a plan to build a container. */ +@Immutable +public class ContainerBuildPlan { + + /** Builder for {@link ContainerBuildPlan}. */ + public static class Builder { + + private String baseImage = "scratch"; + private Instant creationTime = Instant.EPOCH; + private ImageFormat format = ImageFormat.Docker; + + // LinkedHashSet to preserve the order + private Set platforms = + new LinkedHashSet<>(Collections.singleton(new Platform("amd64", "linux"))); + + // image execution parameters + private Map environment = new HashMap<>(); + private Map labels = new HashMap<>(); + private Set volumes = new HashSet<>(); + private Set exposedPorts = new HashSet<>(); + @Nullable private String user; + @Nullable private AbsoluteUnixPath workingDirectory; + @Nullable private List entrypoint; + @Nullable private List cmd; + + private List layers = new ArrayList<>(); + + private Builder() {} + + /** + * Image reference to a base image. The default is {@code scratch}. + * + * @param baseImage image reference to a base image + * @return this + */ + public Builder setBaseImage(String baseImage) { + this.baseImage = baseImage; + return this; + } + + /** + * Adds a desired image platform (OS and architecture pair). If the base image reference is a + * Docker manifest list or an OCI image index, an image builder may select the base image + * matching the given platform. If the base image reference is an image manifest, an image + * builder may ignore the given platform and use the platform of the base image or may decide to + * raise on error. + * + *

Note that a new build plan starts with "amd64/linux" as the default platform. If you want + * to reset the default platform instead of adding a new one, use {@link #setPlatforms(Set)}. + * + * @param architecture architecture (for example, {@code amd64}) to select a base image in case + * of a manifest list + * @param os OS (for example, {@code linux}) to select a base image in case of a manifest list + * @return this + */ + public Builder addPlatform(String architecture, String os) { + platforms.add(new Platform(architecture, os)); + return this; + } + + /** + * Sets a desired platform (properties including OS and architecture) list. If the base image + * reference is a Docker manifest list or an OCI image index, an image builder may select the + * base images matching the given platforms. If the base image reference is an image manifest, + * an image builder may ignore the given platforms and use the platform of the base image or may + * decide to raise on error. + * + *

Note that a new build plan starts with "amd64/linux" as the default platform. + * + * @param platforms list of platforms to select base images in case of a manifest list + * @return this + */ + public Builder setPlatforms(Set platforms) { + if (platforms.isEmpty()) { + throw new IllegalArgumentException("platforms set cannot be empty"); + } + this.platforms = new LinkedHashSet<>(platforms); + return this; + } + + /** + * Sets the container image creation time. The default is {@link Instant#EPOCH}. + * + * @param creationTime the container image creation time + * @return this + */ + public Builder setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + + /** + * Sets the format to build the container image as. Use {@link ImageFormat#Docker} for Docker + * V2.2 or {@link ImageFormat#OCI} for OCI. + * + * @param format the {@link ImageFormat} + * @return this + */ + public Builder setFormat(ImageFormat format) { + this.format = format; + return this; + } + + /** + * Sets the container environment. These environment variables are available to the program + * launched by the container entrypoint command. This replaces any previously-set environment + * variables. Note that these values are added to the base image values. + * + *

This is similar to {@code + * ENV} in Dockerfiles or {@code env} in the Kubernetes + * Container spec. + * + * @param environment a map of environment variable names to values + * @return this + */ + public Builder setEnvironment(Map environment) { + this.environment = new HashMap<>(environment); + return this; + } + + /** + * Adds a variable in the container environment. + * + * @param name the environment variable name + * @param value the environment variable value + * @return this + * @see #setEnvironment + */ + public Builder addEnvironmentVariable(String name, String value) { + environment.put(name, value); + return this; + } + + /** + * Sets the directories that may hold externally mounted volumes. Note that these values are + * added to the base image values. + * + *

This is similar to {@code VOLUME} in + * Dockerfiles. + * + * @param volumes the directory paths on the container filesystem to set as volumes + * @return this + */ + public Builder setVolumes(Set volumes) { + this.volumes = new HashSet<>(volumes); + return this; + } + + /** + * Adds a directory that may hold an externally mounted volume. + * + * @param volume a directory path on the container filesystem to represent a volume + * @return this + * @see #setVolumes(Set) + */ + public Builder addVolume(AbsoluteUnixPath volume) { + volumes.add(volume); + return this; + } + + /** + * Sets the labels for the container. This replaces any previously-set labels. Note that these + * values are added to the base image values. + * + *

This is similar to {@code LABEL} in + * Dockerfiles. + * + * @param labels a map of label keys to values + * @return this + */ + public Builder setLabels(Map labels) { + this.labels = new HashMap<>(labels); + return this; + } + + /** + * Sets a label for the container. + * + * @param key the label key + * @param value the label value + * @return this + */ + public Builder addLabel(String key, String value) { + labels.put(key, value); + return this; + } + + /** + * Sets the ports to expose from the container. Ports exposed will allow ingress traffic. This + * replaces any previously-set exposed ports. Note that these values are added to the base image + * values. + * + *

Use {@link Port#tcp} to expose a port for TCP traffic and {@link Port#udp} to expose a + * port for UDP traffic. + * + *

This is similar to {@code EXPOSE} in + * Dockerfiles or {@code ports} in the Kubernetes + * Container spec. + * + * @param exposedPorts the ports to expose + * @return this + */ + public Builder setExposedPorts(Set exposedPorts) { + this.exposedPorts = new HashSet<>(exposedPorts); + return this; + } + + /** + * Adds a port to expose from the container. + * + * @param exposedPort the port to expose + * @return this + * @see #setExposedPorts(Set) + */ + public Builder addExposedPort(Port exposedPort) { + exposedPorts.add(exposedPort); + return this; + } + + /** + * Sets the user and group to run the container as. {@code user} can be a username or UID along + * with an optional groupname or GID. {@code null} signals to use the base image value. + * + *

The following are valid formats for {@code user} + * + *

    + *
  • {@code user} + *
  • {@code uid} + *
  • {@code :group} + *
  • {@code :gid} + *
  • {@code user:group} + *
  • {@code uid:gid} + *
  • {@code uid:group} + *
  • {@code user:gid} + *
+ * + * @param user the user to run the container as + * @return this + */ + public Builder setUser(@Nullable String user) { + this.user = user; + return this; + } + + /** + * Sets the working directory in the container. {@code null} signals to use the base image + * value. + * + * @param workingDirectory the working directory + * @return this + */ + public Builder setWorkingDirectory(@Nullable AbsoluteUnixPath workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + /** + * Sets the container entrypoint. This is the beginning of the command that is run when the + * container starts. {@link #setCmd} sets additional tokens. {@code null} signals to use the + * base image value. + * + *

This is similar to {@code + * ENTRYPOINT} in Dockerfiles or {@code command} in the Kubernetes + * Container spec. + * + * @param entrypoint a list of the entrypoint command + * @return this + */ + public Builder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint == null) { + this.entrypoint = null; + } else { + this.entrypoint = new ArrayList<>(entrypoint); + } + return this; + } + + /** + * Sets the container entrypoint program arguments. These are additional tokens added to the end + * of the entrypoint command. {@code null} signals to use the base image value (only when + * entrypoint is also {@code null}). + * + *

This is similar to {@code + * CMD} in Dockerfiles or {@code args} in the Kubernetes + * Container spec. + * + *

For example, if the entrypoint was {@code myprogram --flag subcommand} and program + * arguments were {@code hello world}, then the command that run when the container starts is + * {@code myprogram --flag subcommand hello world}. + * + * @param cmd a list of program argument tokens + * @return this + */ + public Builder setCmd(@Nullable List cmd) { + if (cmd == null) { + this.cmd = null; + } else { + this.cmd = new ArrayList<>(cmd); + } + return this; + } + + public Builder addLayer(LayerObject layer) { + layers.add(layer); + return this; + } + + public Builder setLayers(List layer) { + layers = new ArrayList<>(layer); + return this; + } + + /** + * Returns the built {@link ContainerBuildPlan}. + * + * @return container build plan + */ + public ContainerBuildPlan build() { + return new ContainerBuildPlan( + baseImage, + platforms, + creationTime, + format, + environment, + labels, + volumes, + exposedPorts, + user, + workingDirectory, + entrypoint, + cmd, + layers); + } + } + + public static Builder builder() { + return new Builder(); + } + + private final String baseImage; + private final Set platforms; + private final Instant creationTime; + private final ImageFormat format; + + // image execution parameters + private final Map environment; + private final Map labels; + private final Set volumes; + private final Set exposedPorts; + @Nullable private final String user; + @Nullable private final AbsoluteUnixPath workingDirectory; + @Nullable private final List entrypoint; + @Nullable private final List cmd; + + private final List layers; + + private ContainerBuildPlan( + String baseImage, + Set platforms, + Instant creationTime, + ImageFormat format, + Map environment, + Map labels, + Set volumes, + Set exposedPorts, + @Nullable String user, + @Nullable AbsoluteUnixPath workingDirectory, + @Nullable List entrypoint, + @Nullable List cmd, + List layers) { + this.baseImage = baseImage; + this.platforms = platforms; + this.creationTime = creationTime; + this.format = format; + this.environment = environment; + this.labels = labels; + this.volumes = volumes; + this.exposedPorts = exposedPorts; + this.user = user; + this.workingDirectory = workingDirectory; + this.entrypoint = entrypoint; + this.cmd = cmd; + this.layers = layers; + } + + public String getBaseImage() { + return baseImage; + } + + /** + * Creates and returns a default platform if the user hasn't added or set any platforms ,else + * returns a list of user specified platforms . + * + * @return platforms a list of user specified platforms. + */ + public Set getPlatforms() { + return new LinkedHashSet<>(platforms); + } + + public ImageFormat getFormat() { + return format; + } + + public Instant getCreationTime() { + return creationTime; + } + + public Map getEnvironment() { + return new HashMap<>(environment); + } + + public Set getVolumes() { + return new HashSet<>(volumes); + } + + public Map getLabels() { + return new HashMap<>(labels); + } + + public Set getExposedPorts() { + return new HashSet<>(exposedPorts); + } + + @Nullable + public String getUser() { + return user; + } + + @Nullable + public AbsoluteUnixPath getWorkingDirectory() { + return workingDirectory; + } + + @Nullable + public List getEntrypoint() { + return entrypoint == null ? null : new ArrayList<>(entrypoint); + } + + @Nullable + public List getCmd() { + return cmd == null ? null : new ArrayList<>(cmd); + } + + public List getLayers() { + return new ArrayList<>(layers); + } + + /** + * Creates a builder configured with the current values. + * + * @return {@link Builder} configured with the current values. + */ + public Builder toBuilder() { + return builder() + .setBaseImage(baseImage) + .setPlatforms(platforms) + .setCreationTime(creationTime) + .setFormat(format) + .setEnvironment(environment) + .setLabels(labels) + .setVolumes(volumes) + .setExposedPorts(exposedPorts) + .setUser(user) + .setWorkingDirectory(workingDirectory) + .setEntrypoint(entrypoint) + .setCmd(cmd) + .setLayers(layers); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayer.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayer.java new file mode 100644 index 0000000000..9cab9e3b4a --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayer.java @@ -0,0 +1,388 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.concurrent.Immutable; + +/** Configures how to build a layer in the container image. Instantiate with {@link #builder}. */ +@Immutable +public class FileEntriesLayer implements LayerObject { + + /** Builds a {@link FileEntriesLayer}. */ + public static class Builder { + + private String name = ""; + private List entries = new ArrayList<>(); + + private Builder() {} + + /** + * Sets a name for this layer. This name does not affect the contents of the layer. + * + * @param name the name + * @return this + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * Sets entries for the layer. + * + * @param entries file entries in the layer + * @return this + */ + public Builder setEntries(List entries) { + this.entries = new ArrayList<>(entries); + return this; + } + + /** + * Adds an entry to the layer. + * + * @param entry the layer entry to add + * @return this + */ + public Builder addEntry(FileEntry entry) { + entries.add(entry); + return this; + } + + /** + * Adds an entry to the layer. Only adds the single source file to the exact path in the + * container file system. + * + *

For example, {@code addEntry(Paths.get("myfile"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a file {@code myfile} to the container file + * system at {@code /path/in/container}. + * + *

For example, {@code addEntry(Paths.get("mydirectory"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a directory {@code mydirectory/} to the + * container file system at {@code /path/in/container/}. This does not add the contents + * of {@code mydirectory}. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + */ + public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { + return addEntry( + sourceFile, + pathInContainer, + DEFAULT_FILE_PERMISSIONS_PROVIDER.get(sourceFile, pathInContainer)); + } + + /** + * Adds an entry to the layer with the given permissions. Only adds the single source file to + * the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, FilePermissions permissions) { + return addEntry(sourceFile, pathInContainer, permissions, DEFAULT_MODIFICATION_TIME); + } + + /** + * Adds an entry to the layer with the given file modification time. Only adds the single source + * file to the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param modificationTime the file modification time + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, Instant modificationTime) { + return addEntry( + sourceFile, + pathInContainer, + DEFAULT_FILE_PERMISSIONS_PROVIDER.get(sourceFile, pathInContainer), + modificationTime); + } + + /** + * Adds an entry to the layer with the given permissions and file modification time. Only adds + * the single source file to the exact path in the container file system. See {@link + * Builder#addEntry(Path, AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS + */ + public Builder addEntry( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissions permissions, + Instant modificationTime) { + return addEntry(new FileEntry(sourceFile, pathInContainer, permissions, modificationTime)); + } + + /** + * Adds an entry to the layer with the given permissions and file modification time. Only adds + * the single source file to the exact path in the container file system. See {@link + * Builder#addEntry(Path, AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + * @param ownership file ownership. For example, "1234", "user", ":5678", ":group", "1234:5678", + * and "user:group". Note that "" (empty string), ":" (single colon), "0:", ":0" are allowed + * and representative of "0:0" or "root:root", but prefer an empty string for "0:0". + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS + */ + public Builder addEntry( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissions permissions, + Instant modificationTime, + String ownership) { + return addEntry( + new FileEntry(sourceFile, pathInContainer, permissions, modificationTime, ownership)); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + *

For example, {@code addEntryRecursive(Paths.get("mydirectory", + * AbsoluteUnixPath.get("/path/in/container"))} adds {@code mydirectory} to the container file + * system at {@code /path/in/container} such that {@code mydirectory/subfile} is found at {@code + * /path/in/container/subfile}. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive(Path sourceFile, AbsoluteUnixPath pathInContainer) + throws IOException { + return addEntryRecursive(sourceFile, pathInContainer, DEFAULT_FILE_PERMISSIONS_PROVIDER); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissionsProvider filePermissionProvider) + throws IOException { + return addEntryRecursive( + sourceFile, pathInContainer, filePermissionProvider, DEFAULT_MODIFICATION_TIME_PROVIDER); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @param modificationTimeProvider a provider that takes a source path and destination path on + * the container and returns the file modification time that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissionsProvider filePermissionProvider, + ModificationTimeProvider modificationTimeProvider) + throws IOException { + return addEntryRecursive( + sourceFile, + pathInContainer, + filePermissionProvider, + modificationTimeProvider, + DEFAULT_OWNERSHIP_PROVIDER); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @param modificationTimeProvider a provider that takes a source path and destination path on + * the container and returns the file modification time that should be set for that path + * @param ownershipProvider a provider that takes a source path and destination path on the + * container and returns the ownership that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissionsProvider filePermissionProvider, + ModificationTimeProvider modificationTimeProvider, + OwnershipProvider ownershipProvider) + throws IOException { + FilePermissions permissions = filePermissionProvider.get(sourceFile, pathInContainer); + Instant modificationTime = modificationTimeProvider.get(sourceFile, pathInContainer); + String ownership = ownershipProvider.get(sourceFile, pathInContainer); + addEntry(sourceFile, pathInContainer, permissions, modificationTime, ownership); + if (!Files.isDirectory(sourceFile)) { + return this; + } + try (Stream files = Files.list(sourceFile)) { + for (Path file : files.collect(Collectors.toList())) { + addEntryRecursive( + file, + pathInContainer.resolve(file.getFileName()), + filePermissionProvider, + modificationTimeProvider, + ownershipProvider); + } + } + return this; + } + + /** + * Returns the built {@link FileEntriesLayer}. + * + * @return the built {@link FileEntriesLayer} + */ + public FileEntriesLayer build() { + return new FileEntriesLayer(name, entries); + } + } + + /** Provider that returns default file permissions (644 for files, 755 for directories). */ + public static final FilePermissionsProvider DEFAULT_FILE_PERMISSIONS_PROVIDER = + (sourcePath, destinationPath) -> + Files.isDirectory(sourcePath) + ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS + : FilePermissions.DEFAULT_FILE_PERMISSIONS; + + /** Default file modification time (EPOCH + 1 second). */ + public static final Instant DEFAULT_MODIFICATION_TIME = Instant.ofEpochSecond(1); + + /** Provider that returns default file modification time (EPOCH + 1 second). */ + public static final ModificationTimeProvider DEFAULT_MODIFICATION_TIME_PROVIDER = + (sourcePath, destinationPath) -> DEFAULT_MODIFICATION_TIME; + + /** + * Provider that returns default file ownership (an empty string "" effectively representing + * "0:0"). + */ + public static final OwnershipProvider DEFAULT_OWNERSHIP_PROVIDER = + (sourcePath, destinationPath) -> ""; + + /** + * Gets a new {@link Builder} for {@link FileEntriesLayer}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + private final String name; + private final List entries; + + /** + * Use {@link #builder} to instantiate. + * + * @param name an optional name for the layer + * @param entries the list of {@link FileEntry}s + */ + private FileEntriesLayer(String name, List entries) { + this.name = name; + this.entries = entries; + } + + @Override + public Type getType() { + return Type.FILE_ENTRIES; + } + + /** + * Gets the name. + * + * @return the name + */ + @Override + public String getName() { + return name; + } + + /** + * Gets the list of entries. + * + * @return the list of entries + */ + public List getEntries() { + return new ArrayList<>(entries); + } + + /** + * Creates a builder configured with the current values. + * + * @return {@link Builder} configured with the current values + */ + public Builder toBuilder() { + return builder().setName(name).setEntries(entries); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntry.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntry.java new file mode 100644 index 0000000000..36c8c139a2 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntry.java @@ -0,0 +1,186 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.Objects; +import javax.annotation.concurrent.Immutable; + +/** + * Represents an entry in the layer. A layer consists of many entries that can be converted into tar + * archive entries. + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class FileEntry { + + private final Path sourceFile; + private final AbsoluteUnixPath extractionPath; + private final FilePermissions permissions; + private final Instant modificationTime; + private final String ownership; + + /** + * Instantiates with a source file and the path to place the source file in the container file + * system. + * + *

For example, {@code new FileEntry(Paths.get("HelloWorld.class"), + * AbsoluteUnixPath.get("/app/classes/HelloWorld.class"))} adds a file {@code HelloWorld.class} to + * the container file system at {@code /app/classes/HelloWorld.class}. + * + *

For example, {@code new FileEntry(Paths.get("com"), + * AbsoluteUnixPath.get("/app/classes/com"))} adds a directory to the container file system at + * {@code /app/classes/com}. This does not add the contents of {@code com/}. + * + *

Note that: + * + *

    + *
  • Entry source files can be either files or directories. + *
  • Adding a directory does not include the contents of the directory. Each file under a + * directory must be added as a separate {@link FileEntry}. + *
+ * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + */ + public FileEntry( + Path sourceFile, + AbsoluteUnixPath extractionPath, + FilePermissions permissions, + Instant modificationTime) { + this.sourceFile = sourceFile; + this.extractionPath = extractionPath; + this.permissions = permissions; + this.modificationTime = modificationTime; + ownership = ""; + } + + /** + * Instantiates with a source file and the path to place the source file in the container file + * system. See {@link #FileEntry(Path, AbsoluteUnixPath, FilePermissions, Instant)} for more + * information. + * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + * @param ownership file ownership. For example, "1234", "user", ":5678", ":group", "1234:5678", + * and "user:group". Note that "" (empty string), ":" (single colon), "0:", ":0" are allowed + * and representative of "0:0" or "root:root", but prefer an empty string for "0:0". + */ + public FileEntry( + Path sourceFile, + AbsoluteUnixPath extractionPath, + FilePermissions permissions, + Instant modificationTime, + String ownership) { + this.sourceFile = sourceFile; + this.extractionPath = extractionPath; + this.permissions = permissions; + this.modificationTime = modificationTime; + this.ownership = ownership; + } + + /** + * Returns the modification time of the file in the entry. + * + * @return the modification time + */ + public Instant getModificationTime() { + return modificationTime; + } + + /** + * Gets the source file. The source file may be relative or absolute, so the caller should use + * {@code getSourceFile().toAbsolutePath().toString()} for the serialized form since the + * serialization could change independently of the path representation. + * + * @return the source file + */ + public Path getSourceFile() { + return sourceFile; + } + + /** + * Gets the extraction path. + * + * @return the extraction path + */ + public AbsoluteUnixPath getExtractionPath() { + return extractionPath; + } + + /** + * Gets the file permissions on the container. + * + * @return the file permissions on the container + */ + public FilePermissions getPermissions() { + return permissions; + } + + /** + * Gets the file ownership on the container. + * + * @return the file ownership on the container + */ + public String getOwnership() { + return ownership; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FileEntry)) { + return false; + } + FileEntry otherFileEntry = (FileEntry) other; + return sourceFile.equals(otherFileEntry.sourceFile) + && extractionPath.equals(otherFileEntry.extractionPath) + && permissions.equals(otherFileEntry.permissions) + && modificationTime.equals(otherFileEntry.modificationTime) + && ownership.equals(otherFileEntry.ownership); + } + + @Override + public int hashCode() { + return Objects.hash(sourceFile, extractionPath, permissions, modificationTime, ownership); + } + + @Override + public String toString() { + return "{" + + sourceFile + + "," + + extractionPath + + "," + + permissions + + "," + + modificationTime + + "," + + ownership + + "}"; + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissions.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissions.java new file mode 100644 index 0000000000..235ea8254f --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissions.java @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.attribute.PosixFilePermission; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import javax.annotation.concurrent.Immutable; + +/** + * Represents read/write/execute file permissions for owner, group, and others. + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class FilePermissions { + + /** Default permissions for files added to the container. */ + public static final FilePermissions DEFAULT_FILE_PERMISSIONS = new FilePermissions(0644); + + /** Default permissions for folders added to the container. */ + public static final FilePermissions DEFAULT_FOLDER_PERMISSIONS = new FilePermissions(0755); + + /** + * Matches an octal string representation of file permissions. From left to right, each digit + * represents permissions for owner, group, and other. + */ + private static final String OCTAL_PATTERN = "[0-7][0-7][0-7]"; + + /** Maps from a {@link PosixFilePermission} to its corresponding file permission bit. */ + private static final Map PERMISSION_MAP; + + static { + Map map = new HashMap<>(9); + map.put(PosixFilePermission.OWNER_READ, 0400); + map.put(PosixFilePermission.OWNER_WRITE, 0200); + map.put(PosixFilePermission.OWNER_EXECUTE, 0100); + map.put(PosixFilePermission.GROUP_READ, 040); + map.put(PosixFilePermission.GROUP_WRITE, 020); + map.put(PosixFilePermission.GROUP_EXECUTE, 010); + map.put(PosixFilePermission.OTHERS_READ, 04); + map.put(PosixFilePermission.OTHERS_WRITE, 02); + map.put(PosixFilePermission.OTHERS_EXECUTE, 01); + PERMISSION_MAP = Collections.unmodifiableMap(map); + } + + /** + * Creates a new {@link FilePermissions} from an octal string representation (e.g. "123", "644", + * "755", etc). + * + * @param octalPermissions the octal string representation of the permissions + * @return a new {@link FilePermissions} with the given permissions + */ + public static FilePermissions fromOctalString(String octalPermissions) { + if (!octalPermissions.matches(OCTAL_PATTERN)) { + throw new IllegalArgumentException( + "octalPermissions must be a 3-digit octal number (000-777)"); + } + return new FilePermissions(Integer.parseInt(octalPermissions, 8)); + } + + /** + * Creates a new {@link FilePermissions} from a set of {@link PosixFilePermission}. + * + * @param posixFilePermissions the set of {@link PosixFilePermission} + * @return a new {@link FilePermissions} with the given permissions + */ + public static FilePermissions fromPosixFilePermissions( + Set posixFilePermissions) { + int permissionBits = 0; + for (PosixFilePermission permission : posixFilePermissions) { + permissionBits |= Objects.requireNonNull(PERMISSION_MAP.get(permission)); + } + return new FilePermissions(permissionBits); + } + + private final int permissionBits; + + // VisibleForTesting + FilePermissions(int permissionBits) { + this.permissionBits = permissionBits; + } + + /** + * Gets the corresponding permissions bits specified by the {@link FilePermissions}. + * + * @return the permission bits + */ + public int getPermissionBits() { + return permissionBits; + } + + /** + * Gets the octal string representation of the permissions. + * + * @return the octal string representation of the permissions + */ + public String toOctalString() { + return Integer.toString(permissionBits, 8); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FilePermissions)) { + return false; + } + FilePermissions otherFilePermissions = (FilePermissions) other; + return permissionBits == otherFilePermissions.permissionBits; + } + + @Override + public int hashCode() { + return permissionBits; + } + + @Override + public String toString() { + return toOctalString(); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsProvider.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsProvider.java new file mode 100644 index 0000000000..b013f540ec --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.Path; + +/** Interface for providing rules to determine file permissions on a container. */ +@FunctionalInterface +public interface FilePermissionsProvider { + + /** + * Returns the file permissions that should be set for a path, given the source path and + * destination path on a container. + * + * @param sourcePath the source file. + * @param destinationPath the destination path. The path in the container file system + * corresponding to sourcePath. + * @return the permissions to be set for the file. + */ + public FilePermissions get(Path sourcePath, AbsoluteUnixPath destinationPath); +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ImageFormat.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ImageFormat.java new file mode 100644 index 0000000000..096945d52d --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ImageFormat.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +/** Indicates the format of the image. */ +public enum ImageFormat { + + /** See Docker V2.2. */ + Docker, + + /** See OCI. */ + OCI +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/LayerObject.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/LayerObject.java new file mode 100644 index 0000000000..d32067aa9c --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/LayerObject.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import javax.annotation.concurrent.Immutable; + +/** + * Serves as a base class for the "layers" property in the build plan specification. + * + *

    + *
  • {@link Type#FILE_ENTRIES} indicates {@link FileEntriesLayer}. + *
+ */ +@Immutable +public interface LayerObject { + + public static enum Type { + FILE_ENTRIES, + } + + public Type getType(); + + public String getName(); +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ModificationTimeProvider.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ModificationTimeProvider.java new file mode 100644 index 0000000000..bb724317d3 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ModificationTimeProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.Path; +import java.time.Instant; + +/** Interface for providing the file modification time on a container. */ +@FunctionalInterface +public interface ModificationTimeProvider { + + /** + * Returns the file modification time that should be set on a path, given the source path and + * destination path. + * + * @param sourcePath the source file. + * @param destinationPath the destination path. The path in the container file system + * corresponding to sourcePath. + * @return the file modification time. + */ + public Instant get(Path sourcePath, AbsoluteUnixPath destinationPath); +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/OwnershipProvider.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/OwnershipProvider.java new file mode 100644 index 0000000000..2d92f117b9 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/OwnershipProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.Path; + +/** Interface for providing the file ownership on a container. */ +@FunctionalInterface +public interface OwnershipProvider { + + /** + * Returns the file ownership that should be set for a path, given the source path and destination + * path on a container. + * + * @param sourcePath the source file. + * @param destinationPath the destination path. The path in the container file system + * corresponding to sourcePath. + * @return the ownership to be set for the file. + */ + public String get(Path sourcePath, AbsoluteUnixPath destinationPath); +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Platform.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Platform.java new file mode 100644 index 0000000000..eb5550faa1 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Platform.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.util.Objects; +import javax.annotation.concurrent.Immutable; + +/** Represents an image platform (for example, "amd64/linux"). */ +@Immutable +public class Platform { + private final String architecture; + private final String os; + + public Platform(String architecture, String os) { + this.architecture = architecture; + this.os = os; + } + + public String getArchitecture() { + return architecture; + } + + public String getOs() { + return os; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Platform)) { + return false; + } + Platform otherPlatform = (Platform) other; + return architecture.equals(otherPlatform.getArchitecture()) && os.equals(otherPlatform.getOs()); + } + + @Override + public int hashCode() { + return Objects.hash(architecture, os); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Port.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Port.java new file mode 100644 index 0000000000..7bd1e91f7f --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Port.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.util.Objects; +import javax.annotation.concurrent.Immutable; + +/** Represents a port number with a protocol (TCP or UDP). */ +@Immutable +public class Port { + + private static final String TCP_PROTOCOL = "tcp"; + private static final String UDP_PROTOCOL = "udp"; + + /** + * Create a new {@link Port} with TCP protocol. + * + * @param port the port number + * @return the new {@link Port} + */ + public static Port tcp(int port) { + return new Port(port, TCP_PROTOCOL); + } + + /** + * Create a new {@link Port} with UDP protocol. + * + * @param port the port number + * @return the new {@link Port} + */ + public static Port udp(int port) { + return new Port(port, UDP_PROTOCOL); + } + + /** + * Gets a {@link Port} with protocol parsed from the string form {@code protocolString}. Unknown + * protocols will default to TCP. + * + * @param port the port number + * @param protocolString the case insensitive string (e.g. "tcp", "udp") + * @return the {@link Port} + */ + public static Port parseProtocol(int port, String protocolString) { + String protocol = UDP_PROTOCOL.equalsIgnoreCase(protocolString) ? UDP_PROTOCOL : TCP_PROTOCOL; + return new Port(port, protocol); + } + + private final int portNumber; + private final String protocol; + + private Port(int portNumber, String protocol) { + this.portNumber = portNumber; + this.protocol = protocol; + } + + /** + * Gets the port number. + * + * @return the port number + */ + public int getPort() { + return portNumber; + } + + /** + * Gets the protocol. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Port)) { + return false; + } + Port otherPort = (Port) other; + return portNumber == otherPort.portNumber && protocol.equals(otherPort.protocol); + } + + @Override + public int hashCode() { + return Objects.hash(portNumber, protocol); + } + + /** + * Stringifies the port with protocol, in the form {@code /}. For example: {@code + * 1337/TCP}. + * + * @return the string form of the port with protocol + */ + @Override + public String toString() { + return portNumber + "/" + protocol; + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPath.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPath.java new file mode 100644 index 0000000000..5a2ef023ef --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPath.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.cloud.tools.jib.buildplan.UnixPathParser; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * Represents a Unix-style path in relative form (does not start at the file system root {@code /}). + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class RelativeUnixPath { + + /** + * Gets a new {@link RelativeUnixPath} from a Unix-style path in relative form. The {@code path} + * must be relative (does not begin with a leading slash {@code /}). + * + * @param relativePath the relative path + * @return a new {@link RelativeUnixPath} + */ + public static RelativeUnixPath get(String relativePath) { + if (relativePath.startsWith("/")) { + throw new IllegalArgumentException("Path starts with forward slash (/): " + relativePath); + } + + return new RelativeUnixPath(UnixPathParser.parse(relativePath)); + } + + private final List pathComponents; + + /** Instantiate with {@link #get}. */ + private RelativeUnixPath(List pathComponents) { + this.pathComponents = pathComponents; + } + + /** + * Gets the relative Unix path this represents, in a list of components. + * + * @return the relative path this represents, in a list of components + */ + List getRelativePathComponents() { + return new ArrayList<>(pathComponents); + } +} diff --git a/jib-build-plan/src/main/java/com/google/cloud/tools/jib/buildplan/UnixPathParser.java b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/buildplan/UnixPathParser.java new file mode 100644 index 0000000000..7a03087483 --- /dev/null +++ b/jib-build-plan/src/main/java/com/google/cloud/tools/jib/buildplan/UnixPathParser.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.buildplan; + +import java.util.ArrayList; +import java.util.List; + +/** Parses Unix-style paths. */ +public class UnixPathParser { + + /** + * Parses a Unix-style path into a list of path components. + * + * @param unixPath the Unix-style path + * @return a list of path components + */ + public static List parse(String unixPath) { + List pathComponents = new ArrayList<>(); + // -1 limit for Guava Splitter behavior: https://errorprone.info/bugpattern/StringSplitter + for (String component : unixPath.split("/", -1)) { + if (!component.isEmpty()) { + pathComponents.add(component); + } + } + return pathComponents; + } + + private UnixPathParser() {} +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPathTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPathTest.java new file mode 100644 index 0000000000..c7083e07ba --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPathTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +/** Test for {@link AbsoluteUnixPath}. */ +public class AbsoluteUnixPathTest { + + @Test + public void testGet_notAbsolute() { + try { + AbsoluteUnixPath.get("not/absolute"); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "Path does not start with forward slash (/): not/absolute", ex.getMessage()); + } + } + + @Test + public void testFromPath() { + Assert.assertEquals( + "/absolute/path", AbsoluteUnixPath.fromPath(Paths.get("/absolute/path")).toString()); + } + + @Test + public void testFromPath_windows() { + Assume.assumeTrue(System.getProperty("os.name").startsWith("Windows")); + + Assert.assertEquals( + "/absolute/path", AbsoluteUnixPath.fromPath(Paths.get("T:\\absolute\\path")).toString()); + } + + @Test + public void testEquals() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/absolute/path"); + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/absolute/path/"); + AbsoluteUnixPath absoluteUnixPath3 = AbsoluteUnixPath.get("/another/path"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath2); + Assert.assertNotEquals(absoluteUnixPath1, absoluteUnixPath3); + } + + @Test + public void testResolve_relativeUnixPath() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve("")); + Assert.assertEquals("/file", absoluteUnixPath1.resolve("file").toString()); + Assert.assertEquals("/relative/path", absoluteUnixPath1.resolve("relative/path").toString()); + + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/some/path"); + Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve("")); + Assert.assertEquals("/some/path/file", absoluteUnixPath2.resolve("file").toString()); + Assert.assertEquals( + "/some/path/relative/path", absoluteUnixPath2.resolve("relative/path").toString()); + } + + @Test + public void testResolve_Path_notRelative() { + AbsoluteUnixPath absoluteUnixPath = AbsoluteUnixPath.get("/"); + Path path = Paths.get("/not/relative"); + IllegalArgumentException exception = + Assert.assertThrows(IllegalArgumentException.class, () -> absoluteUnixPath.resolve(path)); + Assert.assertEquals("Cannot resolve against absolute Path: " + path, exception.getMessage()); + } + + @Test + public void testResolve_Path() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve(Paths.get(""))); + Assert.assertEquals("/file", absoluteUnixPath1.resolve(Paths.get("file")).toString()); + Assert.assertEquals( + "/relative/path", absoluteUnixPath1.resolve(Paths.get("relative/path")).toString()); + + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/some/path"); + Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve(Paths.get(""))); + Assert.assertEquals( + "/some/path/file", absoluteUnixPath2.resolve(Paths.get("file///")).toString()); + Assert.assertEquals( + "/some/path/relative/path", + absoluteUnixPath2.resolve(Paths.get("relative//path/")).toString()); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlanTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlanTest.java new file mode 100644 index 0000000000..ac159708b3 --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlanTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ContainerBuildPlanTest}. */ +public class ContainerBuildPlanTest { + + @Test + public void testDefaults() { + ContainerBuildPlan plan = ContainerBuildPlan.builder().build(); + + Assert.assertEquals("scratch", plan.getBaseImage()); + Assert.assertEquals(ImmutableSet.of(new Platform("amd64", "linux")), plan.getPlatforms()); + Assert.assertEquals(ImageFormat.Docker, plan.getFormat()); + Assert.assertEquals(Instant.EPOCH, plan.getCreationTime()); + Assert.assertEquals(Collections.emptyMap(), plan.getEnvironment()); + Assert.assertEquals(Collections.emptySet(), plan.getVolumes()); + Assert.assertEquals(Collections.emptyMap(), plan.getLabels()); + Assert.assertEquals(Collections.emptySet(), plan.getExposedPorts()); + Assert.assertNull(plan.getUser()); + Assert.assertNull(plan.getWorkingDirectory()); + Assert.assertNull(plan.getEntrypoint()); + Assert.assertNull(plan.getCmd()); + Assert.assertEquals(Collections.emptyList(), plan.getLayers()); + } + + @Test + public void testBuilder() { + ContainerBuildPlan plan = createSamplePlan(); + + Assert.assertEquals("base/image", plan.getBaseImage()); + Assert.assertEquals( + ImmutableSet.of(new Platform("testOs", "testArchitecture")), plan.getPlatforms()); + Assert.assertEquals(ImageFormat.OCI, plan.getFormat()); + Assert.assertEquals(Instant.ofEpochMilli(30), plan.getCreationTime()); + Assert.assertEquals(ImmutableMap.of("env", "var"), plan.getEnvironment()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/mnt/foo"), AbsoluteUnixPath.get("/bar")), + plan.getVolumes()); + Assert.assertEquals(ImmutableMap.of("com.example.label", "cool"), plan.getLabels()); + Assert.assertEquals(ImmutableSet.of(Port.tcp(443)), plan.getExposedPorts()); + Assert.assertEquals(":", plan.getUser()); + Assert.assertEquals(AbsoluteUnixPath.get("/workspace"), plan.getWorkingDirectory()); + Assert.assertEquals(Arrays.asList("foo", "entrypoint"), plan.getEntrypoint()); + Assert.assertEquals(Arrays.asList("bar", "cmd"), plan.getCmd()); + + Assert.assertEquals(1, plan.getLayers().size()); + MatcherAssert.assertThat( + plan.getLayers().get(0), CoreMatchers.instanceOf(FileEntriesLayer.class)); + Assert.assertEquals( + Arrays.asList( + new FileEntry( + Paths.get("/src/file/foo"), + AbsoluteUnixPath.get("/path/in/container"), + FilePermissions.fromOctalString("644"), + Instant.ofEpochSecond(1))), + ((FileEntriesLayer) plan.getLayers().get(0)).getEntries()); + } + + @Test + public void testToBuilder() { + ContainerBuildPlan plan = createSamplePlan().toBuilder().build(); + + Assert.assertEquals("base/image", plan.getBaseImage()); + Assert.assertEquals( + ImmutableSet.of(new Platform("testOs", "testArchitecture")), plan.getPlatforms()); + Assert.assertEquals(ImageFormat.OCI, plan.getFormat()); + Assert.assertEquals(Instant.ofEpochMilli(30), plan.getCreationTime()); + Assert.assertEquals(ImmutableMap.of("env", "var"), plan.getEnvironment()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/mnt/foo"), AbsoluteUnixPath.get("/bar")), + plan.getVolumes()); + Assert.assertEquals(ImmutableMap.of("com.example.label", "cool"), plan.getLabels()); + Assert.assertEquals(ImmutableSet.of(Port.tcp(443)), plan.getExposedPorts()); + Assert.assertEquals(":", plan.getUser()); + Assert.assertEquals(AbsoluteUnixPath.get("/workspace"), plan.getWorkingDirectory()); + Assert.assertEquals(Arrays.asList("foo", "entrypoint"), plan.getEntrypoint()); + Assert.assertEquals(Arrays.asList("bar", "cmd"), plan.getCmd()); + + Assert.assertEquals(1, plan.getLayers().size()); + MatcherAssert.assertThat( + plan.getLayers().get(0), CoreMatchers.instanceOf(FileEntriesLayer.class)); + Assert.assertEquals( + Arrays.asList( + new FileEntry( + Paths.get("/src/file/foo"), + AbsoluteUnixPath.get("/path/in/container"), + FilePermissions.fromOctalString("644"), + Instant.ofEpochSecond(1))), + ((FileEntriesLayer) plan.getLayers().get(0)).getEntries()); + } + + @Test + public void testAddPlatform_duplicatePlatforms() { + ContainerBuildPlan plan = + ContainerBuildPlan.builder() + .addPlatform("testOS", "testArchitecture") + .addPlatform("testOS", "testArchitecture") + .build(); + Assert.assertEquals( + ImmutableSet.of(new Platform("amd64", "linux"), new Platform("testOS", "testArchitecture")), + plan.getPlatforms()); + } + + @Test + public void testSetPlatforms_emptyPlatformsSet() { + try { + ContainerBuildPlan.builder().setPlatforms(Collections.emptySet()); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("platforms set cannot be empty", ex.getMessage()); + } + } + + private ContainerBuildPlan createSamplePlan() { + FileEntriesLayer layer = + FileEntriesLayer.builder() + .addEntry(Paths.get("/src/file/foo"), AbsoluteUnixPath.get("/path/in/container")) + .build(); + + return ContainerBuildPlan.builder() + .setBaseImage("base/image") + .setPlatforms(ImmutableSet.of(new Platform("testOs", "testArchitecture"))) + .setFormat(ImageFormat.OCI) + .setCreationTime(Instant.ofEpochMilli(30)) + .setEnvironment(ImmutableMap.of("env", "var")) + .setVolumes(ImmutableSet.of(AbsoluteUnixPath.get("/mnt/foo"), AbsoluteUnixPath.get("/bar"))) + .setLabels(ImmutableMap.of("com.example.label", "cool")) + .setExposedPorts(ImmutableSet.of(Port.tcp(443))) + .setLayers(Arrays.asList(layer)) + .setUser(":") + .setWorkingDirectory(AbsoluteUnixPath.get("/workspace")) + .setEntrypoint(Arrays.asList("foo", "entrypoint")) + .setCmd(Arrays.asList("bar", "cmd")) + .build(); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayerTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayerTest.java new file mode 100644 index 0000000000..fe05723efd --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link FileEntriesLayer}. */ +public class FileEntriesLayerTest { + + private static FileEntry defaultFileEntry(Path source, AbsoluteUnixPath destination) { + return new FileEntry( + source, + destination, + FileEntriesLayer.DEFAULT_FILE_PERMISSIONS_PROVIDER.get(source, destination), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + } + + @Test + public void testAddEntryRecursive_defaults() throws IOException, URISyntaxException { + Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); + Path testFile = Paths.get(Resources.getResource("core/fileA").toURI()); + + FileEntriesLayer layer = + FileEntriesLayer.builder() + .addEntryRecursive(testDirectory, AbsoluteUnixPath.get("/app/layer/")) + .addEntryRecursive(testFile, AbsoluteUnixPath.get("/app/fileA")) + .build(); + + ImmutableSet expectedLayerEntries = + ImmutableSet.of( + defaultFileEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), + defaultFileEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), + defaultFileEntry(testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), + defaultFileEntry( + testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/")), + defaultFileEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), + defaultFileEntry( + testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat")), + defaultFileEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), + defaultFileEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); + + Assert.assertEquals(expectedLayerEntries, ImmutableSet.copyOf(layer.getEntries())); + } + + @Test + public void testAddEntryRecursive_otherFileEntryProperties() + throws IOException, URISyntaxException { + Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); + Path testFile = Paths.get(Resources.getResource("core/fileA").toURI()); + + FilePermissions permissions1 = FilePermissions.fromOctalString("111"); + FilePermissions permissions2 = FilePermissions.fromOctalString("777"); + Instant timestamp1 = Instant.ofEpochSecond(123); + Instant timestamp2 = Instant.ofEpochSecond(987); + String ownership1 = "root"; + String ownership2 = "nobody:65432"; + + FilePermissionsProvider permissionsProvider = + (source, destination) -> + destination.toString().startsWith("/app/layer/a") ? permissions1 : permissions2; + ModificationTimeProvider timestampProvider = + (source, destination) -> + destination.toString().startsWith("/app/layer/a") ? timestamp1 : timestamp2; + OwnershipProvider ownershipProvider = + (source, destination) -> + destination.toString().startsWith("/app/layer/a") ? ownership1 : ownership2; + + FileEntriesLayer layer = + FileEntriesLayer.builder() + .addEntry( + new FileEntry( + Paths.get("foo"), AbsoluteUnixPath.get("/foo"), permissions1, timestamp1)) + .addEntryRecursive( + testDirectory, + AbsoluteUnixPath.get("/app/layer/"), + permissionsProvider, + timestampProvider, + ownershipProvider) + .addEntryRecursive( + testFile, + AbsoluteUnixPath.get("/app/fileA"), + permissionsProvider, + timestampProvider, + ownershipProvider) + .build(); + + ImmutableSet expectedLayerEntries = + ImmutableSet.of( + new FileEntry( + Paths.get("foo"), AbsoluteUnixPath.get("/foo"), permissions1, timestamp1, ""), + new FileEntry( + testDirectory, + AbsoluteUnixPath.get("/app/layer/"), + permissions2, + timestamp2, + ownership2), + new FileEntry( + testDirectory.resolve("a"), + AbsoluteUnixPath.get("/app/layer/a/"), + permissions1, + timestamp1, + ownership1), + new FileEntry( + testDirectory.resolve("a/b"), + AbsoluteUnixPath.get("/app/layer/a/b/"), + permissions1, + timestamp1, + ownership1), + new FileEntry( + testDirectory.resolve("a/b/bar"), + AbsoluteUnixPath.get("/app/layer/a/b/bar/"), + permissions1, + timestamp1, + ownership1), + new FileEntry( + testDirectory.resolve("c/"), + AbsoluteUnixPath.get("/app/layer/c"), + permissions2, + timestamp2, + ownership2), + new FileEntry( + testDirectory.resolve("c/cat/"), + AbsoluteUnixPath.get("/app/layer/c/cat"), + permissions2, + timestamp2, + ownership2), + new FileEntry( + testDirectory.resolve("foo"), + AbsoluteUnixPath.get("/app/layer/foo"), + permissions2, + timestamp2, + ownership2), + new FileEntry( + testFile, + AbsoluteUnixPath.get("/app/fileA"), + permissions2, + timestamp2, + ownership2)); + + Assert.assertEquals(expectedLayerEntries, ImmutableSet.copyOf(layer.getEntries())); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntryTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntryTest.java new file mode 100644 index 0000000000..184a7045af --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntryTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import java.io.File; +import java.nio.file.Paths; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; + +public class FileEntryTest { + + @Test + public void testToString() { + Assert.assertEquals( + "{a" + File.separator + "path,/an/absolute/unix/path,333,1970-01-01T00:00:00Z,0:0}", + new FileEntry( + Paths.get("a/path"), + AbsoluteUnixPath.get("/an/absolute/unix/path"), + FilePermissions.fromOctalString("333"), + Instant.EPOCH, + "0:0") + .toString()); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsTest.java new file mode 100644 index 0000000000..45165840f3 --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.nio.file.attribute.PosixFilePermission; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link FilePermissions}. */ +public class FilePermissionsTest { + + @Test + public void testFromOctalString() { + Assert.assertEquals(new FilePermissions(0777), FilePermissions.fromOctalString("777")); + Assert.assertEquals(new FilePermissions(0000), FilePermissions.fromOctalString("000")); + Assert.assertEquals(new FilePermissions(0123), FilePermissions.fromOctalString("123")); + Assert.assertEquals(new FilePermissions(0755), FilePermissions.fromOctalString("755")); + Assert.assertEquals(new FilePermissions(0644), FilePermissions.fromOctalString("644")); + + ImmutableList badStrings = ImmutableList.of("abc", "-123", "777444333", "987", "3"); + for (String badString : badStrings) { + try { + FilePermissions.fromOctalString(badString); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "octalPermissions must be a 3-digit octal number (000-777)", ex.getMessage()); + } + } + } + + @Test + public void testFromPosixFilePermissions() { + Assert.assertEquals( + new FilePermissions(0000), FilePermissions.fromPosixFilePermissions(ImmutableSet.of())); + Assert.assertEquals( + new FilePermissions(0110), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_EXECUTE))); + Assert.assertEquals( + new FilePermissions(0202), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.OWNER_WRITE, PosixFilePermission.OTHERS_WRITE))); + Assert.assertEquals( + new FilePermissions(0044), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.OTHERS_READ))); + Assert.assertEquals( + new FilePermissions(0777), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.copyOf(PosixFilePermission.values()))); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/PortTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/PortTest.java new file mode 100644 index 0000000000..6a0701103f --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/PortTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Port}. */ +public class PortTest { + + @Test + public void testTcp() { + Port port = Port.tcp(5555); + Assert.assertEquals(5555, port.getPort()); + Assert.assertEquals("5555/tcp", port.toString()); + } + + @Test + public void testUdp() { + Port port = Port.udp(6666); + Assert.assertEquals(6666, port.getPort()); + Assert.assertEquals("6666/udp", port.toString()); + } + + @Test + public void testParseProtocol() { + Assert.assertEquals(Port.tcp(1111), Port.parseProtocol(1111, "tcp")); + Assert.assertEquals(Port.udp(2222), Port.parseProtocol(2222, "udp")); + Assert.assertEquals(Port.tcp(3333), Port.parseProtocol(3333, "")); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPathTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPathTest.java new file mode 100644 index 0000000000..16ffb5002c --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPathTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api.buildplan; + +import com.google.common.collect.ImmutableList; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RelativeUnixPath}. */ +public class RelativeUnixPathTest { + + @Test + public void testGet_absolute() { + try { + RelativeUnixPath.get("/absolute"); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Path starts with forward slash (/): /absolute", ex.getMessage()); + } + } + + @Test + public void testGet() { + Assert.assertEquals( + ImmutableList.of("some", "relative", "path"), + RelativeUnixPath.get("some/relative///path").getRelativePathComponents()); + } +} diff --git a/jib-build-plan/src/test/java/com/google/cloud/tools/jib/buildplan/UnixPathParserTest.java b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/buildplan/UnixPathParserTest.java new file mode 100644 index 0000000000..5baa64fecb --- /dev/null +++ b/jib-build-plan/src/test/java/com/google/cloud/tools/jib/buildplan/UnixPathParserTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.buildplan; + +import com.google.common.collect.ImmutableList; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link UnixPathParser}. */ +public class UnixPathParserTest { + + @Test + public void testParse() { + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("/some/path")); + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("some/path/")); + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("some///path///")); + // Windows-style paths are resolved in Unix semantics. + Assert.assertEquals( + ImmutableList.of("\\windows\\path"), UnixPathParser.parse("\\windows\\path")); + Assert.assertEquals(ImmutableList.of("T:\\dir"), UnixPathParser.parse("T:\\dir")); + Assert.assertEquals( + ImmutableList.of("T:\\dir", "real", "path"), UnixPathParser.parse("T:\\dir/real/path")); + } +} diff --git a/jib-build-plan/src/test/resources/core/fileA b/jib-build-plan/src/test/resources/core/fileA new file mode 100644 index 0000000000..d9ef1a6b36 --- /dev/null +++ b/jib-build-plan/src/test/resources/core/fileA @@ -0,0 +1 @@ +Crepe cakes are good. \ No newline at end of file diff --git a/jib-core/src/test/resources/layer/a/b/bar b/jib-build-plan/src/test/resources/core/layer/a/b/bar similarity index 100% rename from jib-core/src/test/resources/layer/a/b/bar rename to jib-build-plan/src/test/resources/core/layer/a/b/bar diff --git a/jib-core/src/test/resources/layer/c/cat b/jib-build-plan/src/test/resources/core/layer/c/cat similarity index 100% rename from jib-core/src/test/resources/layer/c/cat rename to jib-build-plan/src/test/resources/core/layer/c/cat diff --git a/jib-core/src/test/resources/layer/foo b/jib-build-plan/src/test/resources/core/layer/foo similarity index 100% rename from jib-core/src/test/resources/layer/foo rename to jib-build-plan/src/test/resources/core/layer/foo diff --git a/jib-cli/CHANGELOG.md b/jib-cli/CHANGELOG.md new file mode 100644 index 0000000000..aa7261fb61 --- /dev/null +++ b/jib-cli/CHANGELOG.md @@ -0,0 +1,103 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +### Changed + +### Fixed + +## 0.13.0 + +### Added + +### Changed + +### Fixed +- fix: support parsing manifest JSON containing `LayerSources:` from latest Docker. ([#4171](https://github.com/GoogleContainerTools/jib/pull/4171)) +- fix: (WAR Containerization) modify default entrypoint to `java -jar /usr/local/jetty/start.jar --module=ee10-deploy` for Jetty 12+ compatibility ([#4216](https://github.com/GoogleContainerTools/jib/pull/4216)) + +## 0.12.0 + +### Changed +- Upgraded Google HTTP libraries to 1.42.2 ([#3745](https://github.com/GoogleContainerTools/jib/pull/3745)) +- Re-synchronized jackson dependencies with BOM to use latest versions ([#3768](https://github.com/GoogleContainerTools/jib/pull/3768)) + +## 0.11.0 + +### Added +- Included `imagePushed` field to image metadata json output file which provides information on whether an image was pushed by Jib. ([#3641](https://github.com/GoogleContainerTools/jib/pull/3641)) +- Better error messaging when environment map in `container.environment` contains null values ([#3672](https://github.com/GoogleContainerTools/jib/pull/3672)). +- Starting with jib-cli 0.11.0, [SLSA 3 signatures](https://slsa.dev/) will be generated with every release. ([#3762](https://github.com/GoogleContainerTools/jib/pull/3726)). + +### Changed +- Upgraded slf4j-api to 2.0.0 ([#3735](https://github.com/GoogleContainerTools/jib/pull/3735)). +- Upgraded nullaway to 0.9.9 ([#3720](https://github.com/GoogleContainerTools/jib/pull/3720)). + +Thanks to our community contributors @wwadge @oliver-brm and @laurentsimon! + +## 0.10.0 + +### Changed +- Upgraded jackson-databind to 2.13.2.2 ([#3612](https://github.com/GoogleContainerTools/jib/issues/3612)). + +### Fixed + +- Incorrect release sha256 file for jib-cli. ([#3584](https://github.com/GoogleContainerTools/jib/issues/3584)) + +## 0.9.0 + +### Changed + +- For Java 17, changed the default base image of the Jib CLI `jar` command from the `azul/zulu-openjdk` to [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin). ([#3483](https://github.com/GoogleContainerTools/jib/issues/3483)) + +## 0.8.0 + +### Added + +- Increased robustness in registry communications by retrying HTTP requests (to the effect of retrying image pushes or pulls) on I/O exceptions with exponential backoffs. ([#3351](https://github.com/GoogleContainerTools/jib/pull/3351)) +- Now also supports `username` and `password` properties for the `auths` section in a Docker config (`~/.docker/config.json`). (Previously, only supported was a base64-encoded username and password string of the `auth` property.) ([#3365](https://github.com/GoogleContainerTools/jib/pull/3365)) + +### Changed + +- Downgraded Google HTTP libraries to 1.34.0 to resolve network issues. ([#3415](https://github.com/GoogleContainerTools/jib/pull/3415), [#3058](https://github.com/GoogleContainerTools/jib/issues/3058), [#3409](https://github.com/GoogleContainerTools/jib/issues/3409)) +- Changed the default base image of the Jib CLI `jar` command from the `adoptopenjdk` images to the [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin) (for Java 8 and 11) and [`azul/zulu-openjdk`](https://hub.docker.com/r/azul/zulu-openjdk) (for Java 17) images on Docker Hub. Note that Temurin (by Adoptium) is the new name of AdoptOpenJDK. ([#3491](https://github.com/GoogleContainerTools/jib/pull/3491)) + +## 0.7.0 + +### Added + +- Added the `war` command which can be used to containerize a standard WAR with `$ jib war --target ... my-app.war`. The command will explode out the contents of the WAR into optimized layers on the container. ([#3285](https://github.com/GoogleContainerTools/jib/pull/3285)) + +## 0.6.0 + +### Added + +- Added automatic update check. Jib CLI will now display a message if a new version is available. See the [privacy page](https://github.com/GoogleContainerTools/jib/blob/master/docs/privacy.md) for more details. ([#3165](https://github.com/GoogleContainerTools/jib/pull/3165)) +- Added `--image-metadata-out` option to specify JSON output file that should contain image metadata (image ID, digest, and tags) after build is complete. ([#3187](https://github.com/GoogleContainerTools/jib/pull/3187)) + +## 0.5.0 + +### Fixed + +- Fixed an issue where critical error messages (for example, unauthorized access from a registry) were erased by progress reporting and not shown. ([#3148](https://github.com/GoogleContainerTools/jib/issues/3148)) + +## 0.4.0 + +### Added + +- Added support for [configuring registry mirrors](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-am-hitting-docker-hub-rate-limits-how-can-i-configure-registry-mirrors) for base images. This is useful when hitting [Docker Hub rate limits](https://www.docker.com/increase-rate-limits). Only public mirrors (such as `mirror.gcr.io`) are supported. ([#3134](https://github.com/GoogleContainerTools/jib/pull/3134)) + +## 0.3.0 + +### Changed + +- Changed the default base image of the Jib CLI jar command from the `openjdk` images to the `adoptopenjdk` images on Docker Hub. ([#3108](https://github.com/GoogleContainerTools/jib/pull/3108)) + +## 0.2.0 + +### Added + +- Added the `jar` command which can be used to containerize a JAR with `$ jib jar --target ... my-app.jar`. By default, the command will add the contents of the JAR into optimized layers on the container. ([#11](https://github.com/GoogleContainerTools/jib/projects/11)) diff --git a/jib-cli/README.md b/jib-cli/README.md new file mode 100644 index 0000000000..a81b1d58a4 --- /dev/null +++ b/jib-cli/README.md @@ -0,0 +1,477 @@ +# Jib CLI + + + +[![Chocolatey](https://img.shields.io/chocolatey/v/jib.svg)](https://chocolatey.org/packages/jib) +[![Chocolatey](https://img.shields.io/chocolatey/dt/jib.svg)](https://chocolatey.org/packages/jib) +[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev) + +`jib` is a general-purpose command-line utility for building Docker or [OCI](https://github.com/opencontainers/image-spec) container images from file system content as well as JAR files. Jib CLI builds containers [fast and reproducibly without Docker](https://github.com/GoogleContainerTools/jib#goals) like [other Jib tools](https://github.com/GoogleContainerTools/jib#what-is-jib). + +```sh +# docker not required +$ docker +-bash: docker: command not found +# build and upload an image +$ jib build --target=my-registry.example.com/built-by-jib +``` + +Additionally, Jib CLI can directly build an optimized image for JAR files (including Spring Boot fat JAR). +```sh +$ jib jar --target=my-registry.example.com/jar-app myapp.jar +``` + +The CLI tool is powered by [Jib Core](https://github.com/GoogleContainerTools/jib/tree/master/jib-core), a Java library for building containers without Docker. + +## Table of Contents +* [Get the Jib CLI](#get-the-jib-cli) + * [Download a Java Application](#download-a-java-application) + * [Windows: Install with `choco`](#windows-install-with-choco) + * [Build Yourself from Source (for Advanced Users)](#build-yourself-from-source-for-advanced-users) +* [Jib CLI JVM options](#jib-cli-jvm-options) +* [Supported Commands](#supported-commands) +* [Build Command](#build-command) + * [Quickstart](#quickstart) + * [Options](#options) +* [Jar Command](#jar-command) + * [Quickstart](#quickstart-1) + * [Options](#options-1) +* [War Command](#war-command) + * [Quickstart](#quickstart-2) + * [Options](#options-2) +* [Options Shared Between Jar and War Commands](#options-shared-between-jar-and-war-commands) +* [Common Jib CLI Options](#common-jib-cli-options) + * [Auth/Security](#authsecurity) + * [Info Params](#info-params) + * [Debugging Params](#debugging-params) +* [Global Jib Configuration](#global-jib-configuration) +* [References](#references) + * [Fully Annotated Build File (`jib.yaml`)](#fully-annotated-build-file-jibyaml) +* [Privacy](#privacy) + +## Get the Jib CLI + +Most users should download a ZIP archive (Java application). We are working on releasing a native executable binary using GraalVM. (Help wanted!) + +### Download a Java Application + +A JRE is required to run this Jib CLI distribution. + +Find the [latest jib-cli 0.13.0 release](https://github.com/GoogleContainerTools/jib/releases/latest) on the [Releases page](https://github.com/GoogleContainerTools/jib/releases) and download `jib-jre-.zip`. + +Unzip the zip file. The zip file contains the `jib` (`jib.bat` for Windows) script at `jib/bin/`. Optionally, add the binary directory to your `$PATH` so that you can call `jib` from anywhere. + +We generate [SLSA3 signatures](https://slsa.dev/) using the OpenSSF's [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) during the release process. To verify a release binary: +1. Install the verification tool from [slsa-framework/slsa-verifier#installation](https://github.com/slsa-framework/slsa-verifier#installation). +2. Download the signature file `jib-jre-.zip.intoto.jsonl` from the [GitHub releases page](https://github.com/GoogleContainerTools/jib/releases/latest). +3. Run the verifier: +```shell +slsa-verifier -artifact-path jib-jre-.zip -provenance jib-jre-.zip.intoto.jsonl -source github.com/GoogleContainerTools/jib -branch master -workflow-input release_version= +``` + +### Windows: Install with `choco` + +On Windows, you can use the [`choco`](https://community.chocolatey.org/packages/jib) command. To install, upgrade, or uninstall Jib CLI, run the following commands from the command-line or PowerShell: +``` +choco install jib +choco upgrade jib +choco uninstall jib +``` + +### Build Yourself from Source (for Advanced Users) + +Use the `application` plugin's `installDist` task to create a runnable installation in +`build/install/jib`. A zip and tar file are also created in `build/distributions`. +```sh +# build +$ ./gradlew jib-cli:installDist +# run +$ ./jib-cli/build/install/jib/bin/jib +``` + +## Jib CLI JVM options + +Jib CLI is a Java application built with the [Gradle application plugin](https://docs.gradle.org/current/userguide/application_plugin.html). +To configure JVM options for the Jib CLI process itself, you can use the `JIB_OPTS` or `JAVA_OPTS` environment variables. +This is commonly used to configure settings like a proxy by passing standard Java networking properties, as described in the [proxy configuration FAQ](../docs/faq.md#how-do-i-configure-a-proxy). + +## Supported Commands + +The Jib CLI supports two commands: + 1. `build` - containerizes using a [build file](#fully-annotated-build-file-jibyaml). + 2. `jar` - containerizes JAR files. + 3. `war` - containerizes WAR files. + +## Build Command + +This command follows the following pattern: +``` +jib build --target [options] +``` + +### Quickstart + +1. Create a hello world script (`script.sh`) containing: + ```sh + #!/bin/sh + echo "Hello World" + ``` +2. Create a [build file](#fully-annotated-build-file-jibyaml). The default is a file named `jib.yaml` in the project root. + ```yaml + apiVersion: jib/v1alpha1 + kind: BuildFile + + from: + image: ubuntu + + entrypoint: ["/script.sh"] + + layers: + entries: + - name: scripts + files: + - properties: + filePermissions: 755 + src: script.sh + dest: /script.sh + ``` +3. Build to docker daemon + ``` + $ jib build --target=docker://jib-cli-quickstart + ``` +4. Run the container + ``` + $ docker run jib-cli-quickstart + Hello World + ``` + +### Options + +Optional flags for the `build` command: + +Option | Description +--- | --- +`-b, --build-file` | The path to the build file (ex: path/to/other-jib.yaml) +`-c, --context` | The context root directory of the build (ex: path/to/my/build/things) +`-p, --parameter` | Templating parameter to inject into build file, replace ${} with (repeatable) + +## Jar Command + +This command follows the following pattern: +``` +jib jar --target path/to/myapp.jar [options] +``` + +### Quickstart + +1. Have your JAR (thin or fat) ready. We will be using the [Spring Petclinic](https://projects.spring.io/spring-petclinic/) JAR in this Quickstart. + ``` + $ git clone https://github.com/spring-projects/spring-petclinic.git + $ cd spring-petclinic + $ ./mvnw package + ``` +2. Containerize your JAR using the `jar` command. In the default mode (exploded), the entrypoint will be set to `java -cp /app/dependencies/:/app/explodedJar/ HelloWorld` + ``` + $ jib jar --target=docker://cli-jar-quickstart target/spring-petclinic-*.jar + ``` +3. Run the image and open your browser at http://localhost:8080 + ``` + $ docker run -p 8080:8080 cli-jar-quickstart + ``` + +### Options + +Optional flags for the `jar` command: + +Option | Description +--- | --- +`--jvm-flags` | JVM arguments, example: `--jvm-flags=-Dmy.property=value,-Xshare:off` +`--mode` | The jar processing mode, candidates: exploded, packaged, default: exploded + +## War Command + +This command follows the following pattern: +``` + $ jib war --target path/to/myapp.war +``` + +## Quickstart + +1. Have your sample WAR ready and use the `war` command to containerize your WAR. By default, the WAR command uses [`jetty`](https://hub.docker.com/_/jetty) as the base image so the entrypoint is set to `java -jar /usr/local/jetty/start.jar --module=ee10-deploy`: + ``` + $ jib war --target=docker://cli-war-quickstart .war + ``` +2. Run the image and open your browser at http://localhost:8080 + ``` + $ docker run -p 8080:8080 cli-war-quickstart + ``` +## Options + +Flags for the `war` command: + +Option | Description +--- | --- +`--app-root` | The app root on the container. Customizing the app-root is helpful if you are using a different Servlet engine base image (for example, Tomcat) + +## Options Shared Between the Jar and War Commands +Here are a few container configurations that can be customized when using the `jar` and `war` commands. + +Option | Description +--- | --- +`--creation-time` | The creation time of the container in milliseconds since epoch or iso8601 format. Overrides the default (1970-01-01T00:00:00Z) +`--entrypoint` | Entrypoint for container. Overrides the default entrypoint, example: `--entrypoint='custom entrypoint'` +`--environment-variables` | Environment variables to write into container, example: `--environment-variables env1=env_value1, env2=env_value2`. +`--expose` | Ports to expose on container, example: `--expose=5000,7/udp`. +`--from` | The base image to use. +`--image-format` | Format of container, candidates: Docker, OCI, default: Docker. +`--labels` | Labels to write into container metadata, example: `--labels=label1=value1,label2=value2`. +`--program-args` | Program arguments for container entrypoint. +`-u, --user` | The user to run the container as, example: `--user=myuser:mygroup`. +`--volumes` | Directories on container to hold extra volumes, example: `--volumes=/var/log,/var/log2`. + +## Common Jib CLI Options + +The options can either be specified in the command line or defined in a configuration file: +``` +[@...] One or more argument files containing options. +``` + +### Auth/Security + +``` + --allow-insecure-registries Allow jib to send credentials over http (insecure) + --send-credentials-over-http Allow jib to send credentials over http (very insecure) +``` + +### Registry Credentials + +Credentials can be specified using credential helpers or username + password. The following options are available: + +``` + --credential-helper credential helper for communicating with both target and base image registries, either a path to the helper, or a suffix for an executable named `docker-credential-` + --to-credential-helper credential helper for communicating with target registry, either a path to the helper, or a suffix for an executable named `docker-credential- + --from-credential-helper credential helper for communicating with base image registry, either a path to the helper, or a suffix for an executable named `docker-credential-` + + --username username for communicating with both target and base image registries + --password password for communicating with both target and base image registries + --to-username username for communicating with target image registry + --to-password password for communicating with target image registry + --from-username username for communicating with base image registry + --from-password password for communicating with base image registry +``` +*Note* - Combinations of `credential-helper`, `username` and `password` flags come with restrictions and can be use only in the following ways: + +Only Credential Helper +1. `--credential-helper` +2. `--to-credential-helper` +3. `--from-credential-helper` +4. `--to-credential-helper`, `--from-credential-helper` + +Only Username and Password +1. `--username`, `--password` +2. `--to-username`, `--to-password` +3. `--from-username`, `--from-password` +4. `--to-username`, `--to-password`, `--from-username`, `--from-password` + +Mixed Mode +1. `--to-credential-helper`, `--from-username`, `--from-password` +2. `--from-credential-helper`, `--to-username`, `--to-password` + +### Info Params + +``` + --help print usage and exit + --console set console output type, candidates: auto, rich, plain, default: auto + --verbosity set logging verbosity, candidates: quiet, error, warn, lifecycle, info, debug, default: lifecycle +-v, --version Jib CLI version information +``` + +### Debugging Params + +``` + --stacktrace print stacktrace on error (for debugging issues in the jib-cli) + --http-trace enable http tracing at level=config, output=console + --serialize run jib in serialized mode +``` + +## Global Jib Configuration + +Some options can be set in the global Jib configuration file. The file is at the following locations on each platform: + +* *Linux: `[config root]/google-cloud-tools-java/jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/.config/` if not set)* +* *Mac: `[config root]/Google/Jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/Library/Preferences/Config/` if not set)* +* *Windows: `[config root]\Google\Jib\Config\config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`%LOCALAPPDATA%` if not set)* + +### Properties + +* `disableUpdateCheck`: when set to true, disables the periodic up-to-date version check. +* `registryMirrors`: a list of mirror settings for each base image registry. In the following example, if the base image configured in Jib is for a Docker Hub image, then `mirror.gcr.io`, `localhost:5000`, and the Docker Hub (`registry-1.docker.io`) are tried in order until Jib can successfully pull a base image. + +```json +{ + "disableUpdateCheck": false, + "registryMirrors": [ + { + "registry": "registry-1.docker.io", + "mirrors": ["mirror.gcr.io", "localhost:5000"] + }, + { + "registry": "quay.io", + "mirrors": ["private-mirror.test.com"] + } + ] +} +``` +**Note about `mirror.gcr.io`**: it is _not_ a Docker Hub mirror but a cache. It caches [frequently-accessed public Docker Hub images](https://cloud.google.com/container-registry/docs/pulling-cached-images), and it's often possible that your base image does not exist in `mirror.gcr.io`. In that case, Jib will have to fall back to use Docker Hub. + +## References + +### Fully Annotated Build File (`jib.yaml`) + +```yaml +# required apiVersion and kind, for compatibility over versions of the cli +apiVersion: jib/v1alpha1 +kind: BuildFile + +# full base image specification with detail for manifest lists or multiple architectures +from: + image: "ubuntu" + # set platforms for multi architecture builds, defaults to `linux/amd64` + platforms: + - architecture: "arm" + os: "linux" + - architecture: "amd64" + os: "darwin" + +# creation time sets the creation time of the container only +# can be: millis since epoch (ex: 1000) or an ISO 8601 creation time (ex: 2020-06-08T14:54:36+00:00) +creationTime: 2000 + +format: Docker # Docker or OCI + +# container environment variables +environment: + "KEY1": "v1" + "KEY2": "v2" + +# container labels +labels: + "label1": "l1" + "label2": "l2" + +# specify volume mount points +volumes: + - "/volume1" + - "/volume2" + +# specify exposed ports metadata (port-number/protocol) +exposedPorts: + - "123/udp" + - "456" # default protocol is tcp + - "789/tcp" + +# the user to run the container (does not affect file permissions) +user: "customUser" + +workingDirectory: "/home" + +entrypoint: + - "sh" + - "script.sh" +cmd: + - "--param" + - "param" + +# file layers of the container +layers: + properties: # file properties applied to all layers + filePermissions: "123" # octal file permissions, default is 644 + directoryPermissions: "123" # octal directory permissions, default is 755 + user: "2" # default user is 0 + group: "4" # default group is 0 + timestamp: "1232" # timestamp can be millis since epoch or ISO 8601 format, default is "Epoch + 1 second" + entries: + - name: "scripts" # first layer + properties: # file properties applied to only this layer + filePermissions: "123" + # see above for full list of properties... + files: # a list of copy directives constitute a single layer + - src: "project/run.sh" # a simple copy directive (inherits layer level file properties) + dest: "/home/run.sh" # all 'dest' specifications must be absolute paths on the container + - src: "scripts" # a second copy directive in the same layer + dest: "/home/scripts" + excludes: # exclude all files matching these patterns + - "**/exclude.me" + - "**/*.ignore" + includes: # include only files matching these patterns + - "**/include.me" + properties: # file properties applied to only this copy directive + filePermissions: "123" + # see above for full list of properties... + - name: "images" # second layer, inherits file properties from global + files: + - src: "images" + dest: "/images" +``` + +#### Layers Behavior + +- Copy directives are bound by the following rules + `src`: filetype determined by type on local disk + - if `src` is directory, `dest` is always considered a directory, directory and contents will be copied over and renamed to `dest` + - if `src` is file + - if `dest` ends with `/` then it is considered a target directory, file will be copied into directory + - if `dest` doesn't end with `/` then is is the target file location, `src` file will be copied and renamed to `dest` +- Permissions for a file or directory that appear in multiple layers will prioritize the *last* layer and copy directive the file appears in. In the following example, `file.txt` as seen on the running container will have filePermissions `234`. + ``` + - name: layer1 + properties: + filePermissions: "123" + - src: file.txt + dest: /file.txt + - name: layer2 + properties: + filePermissions: "234" + - src: file.txt + dest: /file.txt + ``` +- Parent directories that are not explicitly defined in a layer will the default properties in jib-core (permissions: 755, modification-time: epoch+1). In the following example, `/somewhere` on the container will have the directory permissions `755`, not `777` as some might expect. + ``` + - name: layer + properties: + directoryPermissions: "777" + - src: file.txt + dest: /somewhere/file.txt + ``` +- `excludes` on a directory can lead to unintended inclusion of files in the directory, to exclude a directory *and* all its files + ``` + excludes: + - "**/exclude-dir" + - "**/exclude-dir/** + ``` + +#### Base Image Parameter Inheritance + +Some values defined in the base image may be preserved and propagated into the new container. + +Parameters will append to base image value: +- `volumes` +- `exposedPorts` + +Parameters that will append any new keys, and overwrite existing keys: +- `labels` +- `environment` + +Parameters that will be overwritten: +- `user` +- `workingDirectory` +- `entrypoint` +- `cmd` + +## Privacy + +See the [Privacy page](https://github.com/GoogleContainerTools/jib/blob/master/docs/privacy.md). + +## Disclaimer + +This is not an officially supported Google product. diff --git a/jib-cli/build.gradle b/jib-cli/build.gradle new file mode 100644 index 0000000000..ee5c6ba179 --- /dev/null +++ b/jib-cli/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'application' + id 'net.researchgate.release' + id 'eclipse' +} + +ext { + cliMainClass = 'com.google.cloud.tools.jib.cli.JibCli' +} + +// use `run` to build and run the app +// use `installDist` or `distZip` to create an installable application +application { + applicationName = 'jib' + mainClass = cliMainClass +} + +sourceSets.main.java.srcDirs += ["${buildDir}/generated-src"] + +tasks.register('generateSources', Copy) { + // to re-run the task whenever the file changes + inputs.file 'gradle.properties' + + from fileTree('src/java-templates') + into "${buildDir}/generated-src" + + rename('\\.template$', '') + expand(['version': "${version}"]) +} + +compileJava.source generateSources + +dependencies { + implementation project(':jib-core') + implementation project(':jib-plugins-common') + + implementation dependencyStrings.COMMONS_TEXT + implementation(platform(dependencyStrings.JACKSON_BOM)) + implementation dependencyStrings.JACKSON_DATAFORMAT_YAML + implementation dependencyStrings.JACKSON_DATABIND + implementation dependencyStrings.GUAVA + implementation dependencyStrings.PICOCLI + implementation dependencyStrings.GOOGLE_HTTP_CLIENT + implementation dependencyStrings.GOOGLE_HTTP_CLIENT_APACHE_V2 + + testImplementation dependencyStrings.JUNIT + testImplementation dependencyStrings.JUNIT_PARAMS + testImplementation dependencyStrings.TRUTH + testImplementation dependencyStrings.TRUTH8 + testImplementation dependencyStrings.MOCKITO_CORE + testImplementation dependencyStrings.SLF4J_API + testImplementation dependencyStrings.SYSTEM_RULES + + integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') +} + +release { + tagTemplate = 'v$version-cli' + ignoredSnapshotDependencies = [ + 'com.google.cloud.tools:jib-core', + 'com.google.cloud.tools:jib-plugins-common', + ] + git { + requireBranch = /^cli-release-v\d+.*$/ //regex + } +} + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +eclipse.classpath.file.whenMerged { + entries.each { + if (it.path == 'src/integration-test/resources') { + it.excludes += ['jarTest/', 'warTest/'] + } + } +} +/* ECLIPSE */ diff --git a/jib-cli/gradle.properties b/jib-cli/gradle.properties new file mode 100644 index 0000000000..5b92d1cfd5 --- /dev/null +++ b/jib-cli/gradle.properties @@ -0,0 +1 @@ +version = 0.13.1-SNAPSHOT diff --git a/jib-cli/scripts/update_gcs_latest.sh b/jib-cli/scripts/update_gcs_latest.sh new file mode 100755 index 0000000000..0ee20b9dd9 --- /dev/null +++ b/jib-cli/scripts/update_gcs_latest.sh @@ -0,0 +1,41 @@ +#!/bin/bash - +# Usage: ./jib-cli/scripts/update_gcs_latest.sh + +set -o errexit + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." +} + +[ $# -ne 1 ] && Die "Usage: ./jib-cli/scripts/update_gcs_latest.sh " + +CheckVersion $1 + +versionString="{\"latest\":\"$1\"}" +destination="gs://jib-versions/jib-cli" + +echo $versionString > jib-cli-temp +gcloud storage cp jib-cli-temp $destination +gcloud storage objects update --add-acl-grant=entity=allUsers,role=READER $destination +rm jib-cli-temp + +gcsResult=$(curl https://storage.googleapis.com/jib-versions/jib-cli) +if [ "$gcsResult" == "$versionString" ] +then + EchoGreen "Version updated successfully" +else + Die "Version update failed" +fi \ No newline at end of file diff --git a/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/JarCommandTest.java b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/JarCommandTest.java new file mode 100644 index 0000000000..557b0ba703 --- /dev/null +++ b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/JarCommandTest.java @@ -0,0 +1,368 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.HttpRequestTester; +import com.google.common.base.Preconditions; +import com.google.common.io.Resources; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import javax.annotation.Nullable; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import picocli.CommandLine; + +public class JarCommandTest { + + @ClassRule + public static final TestProject springBootProject = new TestProject("jarTest/spring-boot"); + + @Nullable private String containerName; + + @BeforeClass + public static void createJars() throws IOException, URISyntaxException { + createJarFile( + "jarWithCp.jar", "HelloWorld", "dependency1.jar directory/dependency2.jar", "HelloWorld"); + createJarFile("noDependencyJar.jar", "HelloWorld", null, "HelloWorld"); + createJarFile("dependency1.jar", "dep/A", null, null); + createJarFile("directory/dependency2.jar", "dep2/B", null, null); + } + + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { + new Command("docker", "stop", containerName).run(); + } + } + + @Test + public void testErrorLogging_fileDoesNotExist() { + StringWriter stringWriter = new StringWriter(); + CommandLine jibCli = new CommandLine(new JibCli()).setErr(new PrintWriter(stringWriter)); + + Integer exitCode = jibCli.execute("jar", "--target", "docker://jib-cli-image", "unknown.jar"); + + assertThat(exitCode).isEqualTo(1); + assertThat(stringWriter.toString()) + .isEqualTo("[ERROR] The file path provided does not exist: unknown.jar\n"); + } + + @Test + public void testErrorLogging_directoryGiven() { + StringWriter stringWriter = new StringWriter(); + CommandLine jibCli = new CommandLine(new JibCli()).setErr(new PrintWriter(stringWriter)); + + Path jarFile = Paths.get("/"); + Integer exitCode = + jibCli.execute("jar", "--target", "docker://jib-cli-image", jarFile.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(stringWriter.toString()) + .isEqualTo( + "[ERROR] The file path provided is for a directory. Please provide a path to a JAR: " + + jarFile.toString() + + "\n"); + } + + @Test + public void testStandardJar_explodedMode_toDocker() + throws IOException, InterruptedException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource("jarTest/standard/jarWithCp.jar").toURI()); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://exploded-jar", + jarPath.toString()); + String output = + new Command("docker", "run", "--rm", "exploded-jar", "--privileged", "--network=host") + .run(); + + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String classPath = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + + assertThat(classPath).isEqualTo("dependency1.jar directory/dependency2.jar"); + assertThat(exitCode).isEqualTo(0); + assertThat(output).isEqualTo("Hello World"); + } + } + + @Test + public void testNoDependencyStandardJar_explodedMode_toDocker() + throws IOException, InterruptedException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource("jarTest/standard/noDependencyJar.jar").toURI()); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://exploded-no-dep-jar", + jarPath.toString()); + String output = + new Command( + "docker", "run", "--rm", "exploded-no-dep-jar", "--privileged", "--network=host") + .run(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String classPath = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + + assertThat(classPath).isNull(); + assertThat(exitCode).isEqualTo(0); + assertThat(output).isEqualTo("Hello World"); + } + } + + @Test + public void testStandardJar_packagedMode_toDocker() + throws IOException, InterruptedException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource("jarTest/standard/jarWithCp.jar").toURI()); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://packaged-jar", + jarPath.toString(), + "--mode=packaged"); + String output = + new Command("docker", "run", "--rm", "packaged-jar", "--privileged", "--network=host") + .run(); + + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String classPath = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + + assertThat(classPath).isEqualTo("dependency1.jar directory/dependency2.jar"); + assertThat(exitCode).isEqualTo(0); + assertThat(output).isEqualTo("Hello World"); + } + } + + @Test + public void testNoDependencyStandardJar_packagedMode_toDocker() + throws IOException, InterruptedException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource("jarTest/standard/noDependencyJar.jar").toURI()); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://packaged-no-dep-jar", + jarPath.toString(), + "--mode=packaged"); + String output = + new Command( + "docker", "run", "--rm", "packaged-no-dep-jar", "--privileged", "--network=host") + .run(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String classPath = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + + assertThat(classPath).isNull(); + assertThat(exitCode).isEqualTo(0); + assertThat(output).isEqualTo("Hello World"); + } + } + + @Test + public void testSpringBootLayeredJar_explodedMode() throws IOException, InterruptedException { + springBootProject.build("-c", "settings-layered.gradle", "clean", "bootJar"); + Path jarParentPath = springBootProject.getProjectRoot().resolve("build").resolve("libs"); + Path jarPath = jarParentPath.resolve("spring-boot.jar"); + + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://spring-boot-jar-layered", + jarPath.toString()); + assertThat(exitCode).isEqualTo(0); + + String output = + new Command( + "docker", + "run", + "--rm", + "--detach", + "-p8080:8080", + "spring-boot-jar-layered", + "--privileged", + "--network=host") + .run(); + containerName = output.trim(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + + assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull(); + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080")); + } + } + + @Test + public void testSpringBootNonLayeredJar_explodedMode() throws IOException, InterruptedException { + springBootProject.build("clean", "bootJar"); + Path jarParentPath = springBootProject.getProjectRoot().resolve("build").resolve("libs"); + Path jarPath = jarParentPath.resolve("spring-boot.jar"); + + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://spring-boot-jar", + jarPath.toString()); + assertThat(exitCode).isEqualTo(0); + + String output = + new Command( + "docker", + "run", + "--rm", + "--detach", + "-p8080:8080", + "spring-boot-jar", + "--privileged", + "--network=host") + .run(); + containerName = output.trim(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + + assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNull(); + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080")); + } + } + + @Test + public void testSpringBootJar_packagedMode() throws IOException, InterruptedException { + springBootProject.build("clean", "bootJar"); + Path jarParentPath = springBootProject.getProjectRoot().resolve("build").resolve("libs"); + Path jarPath = jarParentPath.resolve("spring-boot.jar"); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "jar", + "--from", + "eclipse-temurin:8-jdk-focal", + "--target", + "docker://packaged-spring-boot", + jarPath.toString(), + "--mode=packaged"); + assertThat(exitCode).isEqualTo(0); + + String output = + new Command( + "docker", + "run", + "--rm", + "--detach", + "-p8080:8080", + "packaged-spring-boot", + "--privileged", + "--network=host") + .run(); + containerName = output.trim(); + + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080")); + } + + public static void createJarFile( + String name, String className, String classPath, String mainClass) + throws IOException, URISyntaxException { + Path javaFilePath = + Paths.get(Resources.getResource("jarTest/standard/" + className + ".java").toURI()); + Path workingDir = Paths.get(Resources.getResource("jarTest/standard/").toURI()); + + // compile the java file + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + Preconditions.checkNotNull(compiler); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(javaFilePath.toFile())); + Iterable options = Arrays.asList("-source", "1.8", "-target", "1.8"); + JavaCompiler.CompilationTask task = + compiler.getTask(null, fileManager, null, options, null, compilationUnits); + boolean success = task.call(); + assertThat(success).isTrue(); + + // Create a manifest file + Manifest manifest = new Manifest(); + Attributes attributes = new Attributes(); + attributes.putValue("Manifest-Version", "1.0"); + if (classPath != null) { + attributes.putValue("Class-Path", classPath); + } + if (mainClass != null) { + attributes.putValue("Main-Class", mainClass); + } + manifest.getMainAttributes().putAll(attributes); + + // Create JAR + File jarFile = workingDir.resolve(name).toFile(); + jarFile.getParentFile().mkdirs(); + try (FileOutputStream fileOutputStream = new FileOutputStream(jarFile); + JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream, manifest)) { + ZipEntry zipEntry = new ZipEntry(className + ".class"); + jarOutputStream.putNextEntry(zipEntry); + jarOutputStream.write(Files.readAllBytes(workingDir.resolve(className + ".class"))); + jarOutputStream.closeEntry(); + } + } +} diff --git a/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/TestProject.java b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/TestProject.java new file mode 100644 index 0000000000..1b0090cc8b --- /dev/null +++ b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/TestProject.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.io.Resources; +import java.io.Closeable; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.rules.TemporaryFolder; + +public class TestProject extends TemporaryFolder implements Closeable { + + /** Copies test project {@code projectName} to {@code destination} folder. */ + private static void copyProject(String projectName, Path destination) + throws IOException, URISyntaxException { + Path projectPathInResources = Paths.get(Resources.getResource(projectName).toURI()); + new DirectoryWalker(projectPathInResources) + .filterRoot() + .walk( + path -> { + // Creates the same path in the destDir. + Path destPath = destination.resolve(projectPathInResources.relativize(path)); + if (Files.isDirectory(path)) { + Files.createDirectory(destPath); + } else { + Files.copy(path, destPath); + } + }); + } + + private final String testProjectName; + + private Path projectRoot; + + /** Initialize with a specific project directory. */ + public TestProject(String testProjectName) { + this.testProjectName = testProjectName; + } + + @Override + public void close() { + after(); + } + + @Override + protected void before() throws Throwable { + super.before(); + + projectRoot = newFolder().toPath(); + copyProject(testProjectName, projectRoot); + } + + public void build(String... gradleArguments) throws IOException, InterruptedException { + List cmd = new ArrayList<>(); + cmd.add("./gradlew"); + cmd.addAll(Arrays.asList(gradleArguments)); + new Command(cmd).setWorkingDir(projectRoot).run(); + } + + public Path getProjectRoot() { + return projectRoot; + } +} diff --git a/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/WarCommandTest.java b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/WarCommandTest.java new file mode 100644 index 0000000000..f5ab1d1c05 --- /dev/null +++ b/jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/WarCommandTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.HttpRequestTester; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.ClassRule; +import org.junit.Test; +import picocli.CommandLine; + +public class WarCommandTest { + + @ClassRule public static final TestProject servletProject = new TestProject("warTest"); + + @Nullable private String containerName; + + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { + new Command("docker", "stop", containerName).run(); + } + } + + @Test + public void testErrorLogging_fileDoesNotExist() { + StringWriter stringWriter = new StringWriter(); + CommandLine jibCli = new CommandLine(new JibCli()).setErr(new PrintWriter(stringWriter)); + + Integer exitCode = jibCli.execute("war", "--target", "docker://jib-cli-image", "unknown.war"); + + assertThat(exitCode).isEqualTo(1); + assertThat(stringWriter.toString()) + .isEqualTo("[ERROR] The file path provided does not exist: unknown.war\n"); + } + + @Test + public void testErrorLogging_directoryGiven() { + StringWriter stringWriter = new StringWriter(); + CommandLine jibCli = new CommandLine(new JibCli()).setErr(new PrintWriter(stringWriter)); + + Path warFile = Paths.get("/"); + Integer exitCode = + jibCli.execute("war", "--target", "docker://jib-cli-image", warFile.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(stringWriter.toString()) + .isEqualTo( + "[ERROR] The file path provided is for a directory. Please provide a path to a WAR: " + + warFile.toString() + + "\n"); + } + + @Test + public void testWar_jetty() throws IOException, InterruptedException { + servletProject.build("clean", "war"); + Path warParentPath = servletProject.getProjectRoot().resolve("build").resolve("libs"); + Path warPath = warParentPath.resolve("standard-war.war"); + Integer exitCode = + new CommandLine(new JibCli()) + .execute("war", "--target", "docker://exploded-war", warPath.toString()); + assertThat(exitCode).isEqualTo(0); + String output = + new Command( + "docker", + "run", + "--rm", + "--detach", + "-p8080:8080", + "exploded-war", + "--privileged", + "--network=host") + .run(); + containerName = output.trim(); + + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080/hello")); + } + + @Test + public void testWar_customJettySpecified() throws IOException, InterruptedException { + servletProject.build("clean", "war"); + Path warParentPath = servletProject.getProjectRoot().resolve("build").resolve("libs"); + Path warPath = warParentPath.resolve("standard-war.war"); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "war", + "--target", + "docker://exploded-war-custom-jetty", + "--from=jetty:11.0-jre11-slim-openjdk", + warPath.toString()); + assertThat(exitCode).isEqualTo(0); + String output = + new Command("docker", "run", "--rm", "--detach", "-p8080:8080", "exploded-war-custom-jetty") + .run(); + containerName = output.trim(); + + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080/hello")); + } + + @Test + public void testWar_tomcat() throws IOException, InterruptedException { + servletProject.build("clean", "war"); + Path warParentPath = servletProject.getProjectRoot().resolve("build").resolve("libs"); + Path warPath = warParentPath.resolve("standard-war.war"); + Integer exitCode = + new CommandLine(new JibCli()) + .execute( + "war", + "--target", + "docker://exploded-war-tomcat", + "--from=tomcat:10-jre8-openjdk-slim", + "--app-root", + "/usr/local/tomcat/webapps/ROOT", + warPath.toString()); + assertThat(exitCode).isEqualTo(0); + String output = + new Command("docker", "run", "--rm", "--detach", "-p8080:8080", "exploded-war-tomcat") + .run(); + containerName = output.trim(); + + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080/hello")); + } +} diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/build-layered.gradle b/jib-cli/src/integration-test/resources/jarTest/spring-boot/build-layered.gradle new file mode 100644 index 0000000000..14d51f3e92 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/build-layered.gradle @@ -0,0 +1,26 @@ +plugins { + id 'org.springframework.boot' version '2.3.7.RELEASE' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' +} + +sourceCompatibility = '1.8' + +bootJar { + layered { + enabled = true + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +test { + useJUnitPlatform() +} + diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/build.gradle b/jib-cli/src/integration-test/resources/jarTest/spring-boot/build.gradle new file mode 100644 index 0000000000..ac97a750e5 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '2.3.7.RELEASE' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' +} + +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +test { + useJUnitPlatform() +} + diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.jar b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..490fda8577 Binary files /dev/null and b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.properties b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5028f28f8e --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew.bat b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings-layered.gradle b/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings-layered.gradle new file mode 100644 index 0000000000..8d433d17d0 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings-layered.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} +rootProject.name = 'spring-boot' +rootProject.buildFileName = 'build-layered.gradle' diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings.gradle b/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings.gradle new file mode 100644 index 0000000000..17fb2a2bfa --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} +rootProject.name = 'spring-boot' \ No newline at end of file diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/Application.java b/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/Application.java new file mode 100644 index 0000000000..8c26fec153 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/Application.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/HelloController.java b/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..3527662c07 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Hello world"; + } +} diff --git a/jib-cli/src/integration-test/resources/jarTest/standard/HelloWorld.java b/jib-cli/src/integration-test/resources/jarTest/standard/HelloWorld.java new file mode 100644 index 0000000000..558d6f9908 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/standard/HelloWorld.java @@ -0,0 +1,5 @@ +public class HelloWorld { + public static void main(String[] args) { + System.out.print("Hello World"); + } +} diff --git a/jib-cli/src/integration-test/resources/jarTest/standard/dep/A.java b/jib-cli/src/integration-test/resources/jarTest/standard/dep/A.java new file mode 100644 index 0000000000..96d933ef79 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/standard/dep/A.java @@ -0,0 +1,7 @@ +package dep; + +public class A { + public static void getResult() { + System.out.print("Hello "); + } +} diff --git a/jib-cli/src/integration-test/resources/jarTest/standard/dep2/B.java b/jib-cli/src/integration-test/resources/jarTest/standard/dep2/B.java new file mode 100644 index 0000000000..41ff5a9aa4 --- /dev/null +++ b/jib-cli/src/integration-test/resources/jarTest/standard/dep2/B.java @@ -0,0 +1,7 @@ +package dep2; + +public class B { + public static void getResult() { + System.out.print("World"); + } +} diff --git a/jib-cli/src/integration-test/resources/warTest/build.gradle b/jib-cli/src/integration-test/resources/warTest/build.gradle new file mode 100644 index 0000000000..627efcfa29 --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'war' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0' + moreLibs 'jakarta.annotation:jakarta.annotation-api:2.1.0' // random extra JAR +} + +war { + from ('src/extra_static') + from ('src/extra_js', { into 'js' }) + classpath configurations.moreLibs +} diff --git a/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.jar b/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..490fda8577 Binary files /dev/null and b/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.properties b/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5028f28f8e --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jib-cli/src/integration-test/resources/warTest/gradlew b/jib-cli/src/integration-test/resources/warTest/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/jib-cli/src/integration-test/resources/warTest/gradlew.bat b/jib-cli/src/integration-test/resources/warTest/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-cli/src/integration-test/resources/warTest/settings.gradle b/jib-cli/src/integration-test/resources/warTest/settings.gradle new file mode 100644 index 0000000000..0422ce2d8b --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'standard-war' \ No newline at end of file diff --git a/jib-cli/src/integration-test/resources/warTest/src/extra_js/bogus.js b/jib-cli/src/integration-test/resources/warTest/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-cli/src/integration-test/resources/warTest/src/extra_static/bogus.html b/jib-cli/src/integration-test/resources/warTest/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-cli/src/integration-test/resources/warTest/src/main/java/example/HelloWorld.java b/jib-cli/src/integration-test/resources/warTest/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..ae65db32f0 --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/resources/world b/jib-cli/src/integration-test/resources/warTest/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/resources/world rename to jib-cli/src/integration-test/resources/warTest/src/main/resources/world diff --git a/jib-cli/src/integration-test/resources/warTest/src/main/webapp/META-INF/MANIFEST.MF b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e3c07ab38a --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-cli/src/integration-test/resources/warTest/src/main/webapp/WEB-INF/web.xml b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-cli/src/integration-test/resources/warTest/src/main/webapp/index.html b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-cli/src/integration-test/resources/warTest/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

Hello World!

+ + + + + + + + +
Available Servlets:
The HelloWorld servlet
+ + diff --git a/jib-cli/src/java-templates/com/google/cloud/tools/jib/cli/VersionInfo.java.template b/jib-cli/src/java-templates/com/google/cloud/tools/jib/cli/VersionInfo.java.template new file mode 100644 index 0000000000..516ff81b0e --- /dev/null +++ b/jib-cli/src/java-templates/com/google/cloud/tools/jib/cli/VersionInfo.java.template @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import picocli.CommandLine; + +public class VersionInfo implements CommandLine.IVersionProvider { + public static final String TOOL_NAME = "jib-cli"; + + @Override + public String[] getVersion() throws Exception { + return new String[] {getVersionSimple()}; + } + + public static String getVersionSimple() { + return "${version}"; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactLayers.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactLayers.java new file mode 100644 index 0000000000..e5d4536e1f --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactLayers.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import java.io.IOException; +import java.nio.file.Path; +import java.util.function.Predicate; + +public class ArtifactLayers { + + // Used for layer names. + public static final String CLASSES = "classes"; + public static final String RESOURCES = "resources"; + public static final String DEPENDENCIES = "dependencies"; + public static final String SNAPSHOT_DEPENDENCIES = "snapshot dependencies"; + + private ArtifactLayers() {} + + /** + * Creates a layer containing contents of a directory. Only paths that match the given predicate + * will be added. + * + * @param layerName name of the layer + * @param sourceRoot path to source directory + * @param pathFilter predicate to determine whether to add the path or not + * @param basePathInContainer path to destination on container + * @return {@link FileEntriesLayer} representing the layer + * @throws IOException if io exception occurs when reading from the source directory + */ + public static FileEntriesLayer getDirectoryContentsAsLayer( + String layerName, + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer) + throws IOException { + FileEntriesLayer.Builder builder = FileEntriesLayer.builder().setName(layerName); + new DirectoryWalker(sourceRoot) + .filterRoot() + .filter(path -> pathFilter.test(path)) + .walk( + path -> { + AbsoluteUnixPath pathOnContainer = + basePathInContainer.resolve(sourceRoot.relativize(path)); + builder.addEntry(path, pathOnContainer); + }); + return builder.build(); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessor.java new file mode 100644 index 0000000000..646ce01121 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; + +/** Interface to create layers and compute entrypoint from JAR or WAR file contents. */ +public interface ArtifactProcessor { + + /** + * Creates layers on container for a JAR or WAR. + * + * @return list of {@link FileEntriesLayer} + * @throws IOException if I/O error occurs when opening the java artifact or if temporary + * directory provided doesn't exist + */ + List createLayers() throws IOException; + + /** + * Computes the entrypoint for a JAR or WAR. + * + * @param jvmFlags list of jvm flags + * @return list of {@link String} representing entrypoint + * @throws IOException if I/O error occurs when opening the java artifact + */ + ImmutableList computeEntrypoint(List jvmFlags) throws IOException; + + Integer getJavaVersion(); +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessors.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessors.java new file mode 100644 index 0000000000..58f9737b76 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessors.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.cli.jar.ProcessingMode; +import com.google.cloud.tools.jib.cli.jar.SpringBootExplodedProcessor; +import com.google.cloud.tools.jib.cli.jar.SpringBootPackagedProcessor; +import com.google.cloud.tools.jib.cli.jar.StandardExplodedProcessor; +import com.google.cloud.tools.jib.cli.jar.StandardPackagedProcessor; +import com.google.cloud.tools.jib.cli.war.StandardWarExplodedProcessor; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Optional; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Class to create a {@link ArtifactProcessor} instance depending on jar or war type and processing + * mode. + */ +public class ArtifactProcessors { + private static String SPRING_BOOT = "spring-boot"; + private static String STANDARD = "standard"; + private static Integer VERSION_NOT_FOUND = 0; + private static final String DEFAULT_JETTY_APP_ROOT = "/var/lib/jetty/webapps/ROOT"; + + private ArtifactProcessors() {} + + /** + * Creates a {@link ArtifactProcessor} instance based on jar type and processing mode. + * + * @param jarPath path to the jar + * @param cacheDirectories the location of the relevant caches + * @param jarOptions jar cli options + * @param commonContainerConfigCliOptions common cli options shared between jar and war command + * @return ArtifactProcessor + * @throws IOException if I/O error occurs when opening the jar file + */ + public static ArtifactProcessor fromJar( + Path jarPath, + CacheDirectories cacheDirectories, + Jar jarOptions, + CommonContainerConfigCliOptions commonContainerConfigCliOptions) + throws IOException { + Integer jarJavaVersion = determineJavaMajorVersion(jarPath); + if (jarJavaVersion > 17 && !commonContainerConfigCliOptions.getFrom().isPresent()) { + throw new IllegalStateException( + String.format( + "The input JAR (%s) is compiled with Java %d, but the default base image only " + + "supports versions up to Java 17. Specify a custom base image with --from.", + jarPath, jarJavaVersion)); + } + String jarType = determineJarType(jarPath); + ProcessingMode mode = jarOptions.getMode(); + if (jarType.equals(SPRING_BOOT) && mode.equals(ProcessingMode.packaged)) { + return new SpringBootPackagedProcessor(jarPath, jarJavaVersion); + } else if (jarType.equals(SPRING_BOOT) && mode.equals(ProcessingMode.exploded)) { + return new SpringBootExplodedProcessor( + jarPath, cacheDirectories.getExplodedArtifactDirectory(), jarJavaVersion); + } else if (jarType.equals(STANDARD) && mode.equals(ProcessingMode.packaged)) { + return new StandardPackagedProcessor(jarPath, jarJavaVersion); + } else { + return new StandardExplodedProcessor( + jarPath, cacheDirectories.getExplodedArtifactDirectory(), jarJavaVersion); + } + } + + /** + * Creates a {@link ArtifactProcessor} instance. + * + * @param warPath path to the war + * @param cacheDirectories the location of the relevant caches + * @param warOptions war cli options + * @param commonContainerConfigCliOptions common cli options shared between jar and war command + * @return ArtifactProcessor + * @throws InvalidImageReferenceException if base image reference is invalid + */ + public static ArtifactProcessor fromWar( + Path warPath, + CacheDirectories cacheDirectories, + War warOptions, + CommonContainerConfigCliOptions commonContainerConfigCliOptions) + throws InvalidImageReferenceException { + Optional appRoot = warOptions.getAppRoot(); + if (!commonContainerConfigCliOptions.isJettyBaseimage() && !appRoot.isPresent()) { + throw new IllegalArgumentException( + "Please set the app root of the container with `--app-root` when specifying a base image that is not jetty."); + } + AbsoluteUnixPath chosenAppRoot = appRoot.orElse(AbsoluteUnixPath.get(DEFAULT_JETTY_APP_ROOT)); + return new StandardWarExplodedProcessor( + warPath, cacheDirectories.getExplodedArtifactDirectory(), chosenAppRoot); + } + + /** + * Determines whether the jar is a spring boot or standard jar. + * + * @param jarPath path to the jar + * @return the jar type + * @throws IOException if I/O error occurs when opening the file + */ + private static String determineJarType(Path jarPath) throws IOException { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + if (jarFile.getEntry("BOOT-INF") != null) { + return SPRING_BOOT; + } + return STANDARD; + } + } + + /** + * Determines the java version of JAR. Derives the version from the first .class file it finds in + * the JAR. + * + * @param jarPath path to the jar + * @return java version + * @throws IOException if I/O exception thrown when opening the jar file + */ + public static Integer determineJavaMajorVersion(Path jarPath) throws IOException { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + Enumeration jarEntries = jarFile.entries(); + while (jarEntries.hasMoreElements()) { + String jarEntry = jarEntries.nextElement().toString(); + if (jarEntry.endsWith(".class") && !jarEntry.endsWith("module-info.class")) { + try (URLClassLoader loader = new URLClassLoader(new URL[] {jarPath.toUri().toURL()}); + DataInputStream classFile = + new DataInputStream(loader.getResourceAsStream(jarEntry))) { + + // Check magic number + if (classFile.readInt() != 0xCAFEBABE) { + throw new IllegalArgumentException( + "The class file (" + jarEntry + ") is of an invalid format."); + } + + // Skip over minor version + classFile.skipBytes(2); + + int majorVersion = classFile.readUnsignedShort(); + int javaVersion = (majorVersion - 45) + 1; + return javaVersion; + } catch (EOFException ex) { + throw new IllegalArgumentException( + "Reached end of class file (" + + jarEntry + + ") before being able to read the java major version. Make sure that the file is of the correct format."); + } + } + } + return VERSION_NOT_FOUND; + } + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java new file mode 100644 index 0000000000..1ffa9302cd --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.cli.buildfile.BuildFiles; +import com.google.cloud.tools.jib.cli.logging.CliLogger; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Multimaps; +import com.google.common.util.concurrent.Futures; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +@CommandLine.Command( + name = "build", + mixinStandardHelpOptions = true, + showAtFileInUsageHelp = true, + description = "Build a container") +public class Build implements Callable { + + @CommandLine.Spec + @SuppressWarnings("NullAway.Init") // initialized by picocli + private CommandSpec spec; + + @CommandLine.Mixin + @SuppressWarnings("NullAway.Init") // initialized by picocli + @VisibleForTesting + CommonCliOptions commonCliOptions; + + @VisibleForTesting + @CommandLine.Option( + names = {"-c", "--context"}, + defaultValue = ".", + paramLabel = "", + description = "The context root directory of the build (ex: path/to/my/build/things)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + Path contextRoot; + + @VisibleForTesting + @CommandLine.Option( + names = {"-b", "--build-file"}, + paramLabel = "", + description = "The path to the build file (ex: path/to/other-jib.yaml)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + Path buildFileUnprocessed; + + @CommandLine.Option( + names = {"-p", "--parameter"}, + paramLabel = "=", + description = + "templating parameter to inject into build file, replace $${} with (repeatable)") + private Map templateParameters = Collections.emptyMap(); + + /** + * Returns a user configured Path to a buildfile and if none is configured returns jib.yaml in + * {@link #contextRoot}. + * + * @return a path to a buildfile + */ + @VisibleForTesting + Path getBuildFile() { + if (buildFileUnprocessed == null) { + return contextRoot.resolve("jib.yaml"); + } + return buildFileUnprocessed; + } + + public Map getTemplateParameters() { + return templateParameters; + } + + @Override + public Integer call() { + commonCliOptions.validate(); + Path buildFile = getBuildFile(); + SingleThreadedExecutor executor = new SingleThreadedExecutor(); + ConsoleLogger logger = + CliLogger.newLogger( + commonCliOptions.getVerbosity(), + commonCliOptions.getHttpTrace(), + commonCliOptions.getConsoleOutput(), + spec.commandLine().getOut(), + spec.commandLine().getErr(), + executor); + Future> updateCheckFuture = Futures.immediateFuture(Optional.empty()); + try { + JibCli.configureHttpLogging(commonCliOptions.getHttpTrace().toJulLevel()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + updateCheckFuture = + JibCli.newUpdateChecker( + globalConfig, + commonCliOptions.getVerbosity(), + logEvent -> logger.log(logEvent.getLevel(), logEvent.getMessage())); + if (!Files.isReadable(buildFile)) { + logger.log( + LogEvent.Level.ERROR, + "The Build File YAML either does not exist or cannot be opened for reading: " + + buildFile); + return 1; + } + if (!Files.isRegularFile(buildFile)) { + logger.log(LogEvent.Level.ERROR, "Build File YAML path is a not a file: " + buildFile); + return 1; + } + + CacheDirectories cacheDirectories = CacheDirectories.from(commonCliOptions, contextRoot); + Containerizer containerizer = Containerizers.from(commonCliOptions, logger, cacheDirectories); + + JibContainerBuilder containerBuilder = + BuildFiles.toJibContainerBuilder(contextRoot, buildFile, this, commonCliOptions, logger); + + // Enable registry mirrors + Multimaps.asMap(globalConfig.getRegistryMirrors()).forEach(containerizer::addRegistryMirrors); + + JibContainer jibContainer = containerBuilder.containerize(containerizer); + JibCli.writeImageJson(commonCliOptions.getImageJsonPath(), jibContainer); + } catch (InterruptedException ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + Thread.currentThread().interrupt(); + return 1; + } catch (Exception ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + return 1; + } finally { + JibCli.finishUpdateChecker(logger, updateCheckFuture); + executor.shutDownAndAwaitTermination(Duration.ofSeconds(3)); + } + return 0; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CacheDirectories.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CacheDirectories.java new file mode 100644 index 0000000000..b8a4f0827b --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CacheDirectories.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import javax.annotation.Nullable; + +/** A class to determine cache locations for any cli commands. */ +public class CacheDirectories { + + private static final String APPLICATION_LAYER_CACHE_DIR = "application-layers"; + private static final String EXPLODED_ARTIFACT_DIR = "exploded-artifact"; + + @Nullable private final Path baseImageCache; + private final Path projectCache; + + /** + * Create a caches helper for cli cache locations. + * + * @param commonCliOptions cli options for user configured cache directories + * @param contextRoot the context root, use the parent directory of single files, this context + * root must exist + * @return an instance of CacheDirectories with cli specific cache locations + */ + public static CacheDirectories from(CommonCliOptions commonCliOptions, Path contextRoot) { + Preconditions.checkArgument( + Files.isDirectory(contextRoot), + "contextRoot must be a directory, but " + contextRoot.toString() + " is not."); + return new CacheDirectories( + commonCliOptions.getBaseImageCache().orElse(null), + commonCliOptions + .getProjectCache() + .orElse( + Paths.get(System.getProperty("java.io.tmpdir")) + .resolve("jib-cli-cache") + .resolve("projects") + .resolve(getProjectCacheDirectoryFromProject(contextRoot)))); + } + + @VisibleForTesting + static String getProjectCacheDirectoryFromProject(Path path) { + try { + byte[] hashedBytes = + MessageDigest.getInstance("SHA-256") + .digest(path.toFile().getCanonicalPath().getBytes(Charsets.UTF_8)); + StringBuilder stringBuilder = new StringBuilder(2 * hashedBytes.length); + for (byte b : hashedBytes) { + stringBuilder.append(String.format("%02x", b)); + } + return stringBuilder.toString(); + } catch (IOException | SecurityException ex) { + throw new RuntimeException( + "Unable to create cache directory for project path: " + + path + + " - you can try to configure --project-cache manually", + ex); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException( + "SHA-256 algorithm implementation not found - might be a broken JVM"); + } + } + + public CacheDirectories(@Nullable Path baseImageCache, Path projectCache) { + this.baseImageCache = baseImageCache; + this.projectCache = projectCache; + } + + public Optional getBaseImageCache() { + return Optional.ofNullable(baseImageCache); + } + + public Path getProjectCache() { + return projectCache; + } + + public Path getApplicationLayersCache() { + return projectCache.resolve(APPLICATION_LAYER_CACHE_DIR); + } + + public Path getExplodedArtifactDirectory() { + return projectCache.resolve(EXPLODED_ARTIFACT_DIR); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java new file mode 100644 index 0000000000..afd0113905 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java @@ -0,0 +1,429 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.cloud.tools.jib.api.Jib.TAR_IMAGE_PREFIX; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.cli.logging.ConsoleOutput; +import com.google.cloud.tools.jib.cli.logging.HttpTraceLevel; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.common.base.Verify; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +public class CommonCliOptions { + + @CommandLine.Spec + @SuppressWarnings("NullAway.Init") // initialized by picocli + private CommandSpec spec; + + @CommandLine.Option( + names = {"-t", "--target"}, + required = true, + paramLabel = "", + description = + "The destination image reference or jib style url,%nexamples:%n gcr.io/project/image,%n registry://image-ref,%n docker://image,%n tar://path") + @SuppressWarnings("NullAway.Init") // initialized by picocli + static String targetImage; + + // unfortunately we cannot verify for --target=tar://... this is required, we must do this after + // pico cli is done parsing + @CommandLine.Option( + names = "--name", + paramLabel = "", + description = + "The image reference to inject into the tar configuration (required when using --target tar://...)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String name; + + @CommandLine.Option( + names = "--additional-tags", + paramLabel = "", + split = ",", + description = "Additional tags for target image") + private List additionalTags = Collections.emptyList(); + + @CommandLine.Option( + names = "--base-image-cache", + paramLabel = "", + description = "A path to a base image cache") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path baseImageCache; + + @CommandLine.Option( + names = "--project-cache", + paramLabel = "", + description = "A path to the project cache") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path projectCache; + + // Auth/Security + @CommandLine.Option( + names = "--allow-insecure-registries", + description = "Allow jib to communicate with registries over http (insecure)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private boolean allowInsecureRegistries; + + @CommandLine.Option( + names = "--send-credentials-over-http", + description = "Allow jib to send credentials over http (very insecure)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private boolean sendCredentialsOverHttp; + + @CommandLine.ArgGroup(exclusive = true) + @SuppressWarnings("NullAway.Init") + private Credentials credentials; + + private static class Credentials { + @CommandLine.Option( + names = {"--credential-helper"}, + paramLabel = "", + description = + "credential helper for communicating with both target and base image registries, either a path to the helper, or a suffix for an executable named `docker-credential-`") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String credentialHelper; + + @CommandLine.ArgGroup(exclusive = false) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private SingleUsernamePassword usernamePassword; + + @CommandLine.ArgGroup(exclusive = false) + @SuppressWarnings("NullAway.Init") + private SeparateCredentials separate; + } + + private static class SingleUsernamePassword { + @CommandLine.Option( + names = "--username", + required = true, + description = "username for communicating with both target and base image registries") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String username; + + @CommandLine.Option( + names = "--password", + arity = "0..1", + required = true, + interactive = true, + description = "password for communicating with both target and base image registries") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String password; + } + + private static class SeparateCredentials { + @CommandLine.ArgGroup + @SuppressWarnings("NullAway.Init") // initialized by picocli + private ToCredentials to; + + @CommandLine.ArgGroup + @SuppressWarnings("NullAway.Init") // initialized by picocli + private FromCredentials from; + } + + private static class ToCredentials { + @CommandLine.Option( + names = {"--to-credential-helper"}, + paramLabel = "", + description = + "credential helper for communicating with target registry, either a path to the helper, or a suffix for an executable named `docker-credential-`") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String credentialHelper; + + @CommandLine.ArgGroup(exclusive = false) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private ToUsernamePassword usernamePassword; + } + + private static class FromCredentials { + @CommandLine.Option( + names = {"--from-credential-helper"}, + paramLabel = "", + description = + "credential helper for communicating with base image registry, either a path to the helper, or a suffix for an executable named `docker-credential-`") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String credentialHelper; + + @CommandLine.ArgGroup(exclusive = false) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private FromUsernamePassword usernamePassword; + } + + private static class ToUsernamePassword { + @CommandLine.Option( + names = "--to-username", + required = true, + description = "username for communicating with target image registry") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String username; + + @CommandLine.Option( + names = "--to-password", + arity = "0..1", + interactive = true, + required = true, + description = "password for communicating with target image registry") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String password; + } + + private static class FromUsernamePassword { + @CommandLine.Option( + names = "--from-username", + required = true, + description = "username for communicating with base image registry") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String username; + + @CommandLine.Option( + names = "--from-password", + arity = "0..1", + required = true, + interactive = true, + description = "password for communicating with base image registry") + @SuppressWarnings("NullAway.Init") // initialized by picocli + String password; + } + + @CommandLine.Option( + names = "--verbosity", + paramLabel = "", + defaultValue = "lifecycle", + description = + "set logging verbosity, candidates: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}", + scope = CommandLine.ScopeType.INHERIT) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Verbosity verbosity; + + @CommandLine.Option( + names = "--console", + paramLabel = "", + defaultValue = "auto", + description = + "set console output type, candidates: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}", + scope = CommandLine.ScopeType.INHERIT) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private ConsoleOutput consoleOutput; + + // Hidden debug parameters + @CommandLine.Option(names = "--stacktrace", hidden = true, scope = CommandLine.ScopeType.INHERIT) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private boolean stacktrace; + + @CommandLine.Option( + names = "--http-trace", + hidden = true, + arity = "0..1", + defaultValue = "off", + fallbackValue = "config", + description = + "set http logging level, candidates: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}", + scope = CommandLine.ScopeType.INHERIT) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private HttpTraceLevel httpTrace; + + @CommandLine.Option(names = "--serialize", hidden = true, scope = CommandLine.ScopeType.INHERIT) + @SuppressWarnings("NullAway.Init") // initialized by picocli + private boolean serialize; + + @CommandLine.Option( + names = "--image-metadata-out", + paramLabel = "", + description = + "path to the json file that should contain image metadata (for example, digest, id and tags) after build is" + + "complete") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path imageJsonPath; + + public Verbosity getVerbosity() { + return Verify.verifyNotNull(verbosity); + } + + public ConsoleOutput getConsoleOutput() { + return Verify.verifyNotNull(consoleOutput); + } + + public boolean isStacktrace() { + return stacktrace; + } + + public HttpTraceLevel getHttpTrace() { + return httpTrace; + } + + public boolean isSerialize() { + return serialize; + } + + public String getTargetImage() { + return targetImage; + } + + public String getName() { + return name; + } + + /** + * Returns the user configured credential helper for any registry during the build, this can be + * interpreted as a path or a string. + * + * @return a Optional string reference to the credential helper + */ + public Optional getCredentialHelper() { + if (credentials != null && credentials.credentialHelper != null) { + return Optional.of(credentials.credentialHelper); + } + return Optional.empty(); + } + + /** + * Returns the user configured credential helper for the target image registry, this can be + * interpreted as a path or a string. + * + * @return a Optional string reference to the credential helper + */ + public Optional getToCredentialHelper() { + if (credentials != null + && credentials.separate != null + && credentials.separate.to != null + && credentials.separate.to.credentialHelper != null) { + return Optional.of(credentials.separate.to.credentialHelper); + } + return Optional.empty(); + } + + /** + * Returns the user configured credential helper for the base image registry, this can be + * interpreted as a path or a string. + * + * @return a Optional string reference to the credential helper + */ + public Optional getFromCredentialHelper() { + if (credentials != null + && credentials.separate != null + && credentials.separate.from != null + && credentials.separate.from.credentialHelper != null) { + return Optional.of(credentials.separate.from.credentialHelper); + } + return Optional.empty(); + } + + /** + * If configured, returns a {@link Credential} created from user configured username/password. + * + * @return an optional Credential + */ + public Optional getUsernamePassword() { + if (credentials != null && credentials.usernamePassword != null) { + return Optional.of( + Credential.from( + Verify.verifyNotNull(credentials.usernamePassword.username), + Verify.verifyNotNull(credentials.usernamePassword.password))); + } + return Optional.empty(); + } + + /** + * If configured, returns a {@link Credential} created from user configured "to" + * username/password. + * + * @return a optional Credential + */ + public Optional getToUsernamePassword() { + if (credentials != null + && credentials.separate != null + && credentials.separate.to != null + && credentials.separate.to.usernamePassword != null) { + return Optional.of( + Credential.from( + Verify.verifyNotNull(credentials.separate.to.usernamePassword.username), + Verify.verifyNotNull(credentials.separate.to.usernamePassword.password))); + } + return Optional.empty(); + } + + /** + * If configured, returns a {@link Credential} created from user configured "from" + * username/password. + * + * @return a optional Credential + */ + public Optional getFromUsernamePassword() { + if (credentials != null + && credentials.separate != null + && credentials.separate.from != null + && credentials.separate.from.usernamePassword != null) { + return Optional.of( + Credential.from( + Verify.verifyNotNull(credentials.separate.from.usernamePassword.username), + Verify.verifyNotNull(credentials.separate.from.usernamePassword.password))); + } + return Optional.empty(); + } + + public boolean isAllowInsecureRegistries() { + return allowInsecureRegistries; + } + + public boolean isSendCredentialsOverHttp() { + return sendCredentialsOverHttp; + } + + /** + * Do not use directly, use {@link CacheDirectories} instead. + * + * @return a user configured base image cache + */ + public Optional getBaseImageCache() { + return Optional.ofNullable(baseImageCache); + } + + /** + * Do not use directly, use {@link CacheDirectories} instead. + * + * @return a user configured project cache + */ + public Optional getProjectCache() { + return Optional.ofNullable(projectCache); + } + + public List getAdditionalTags() { + return additionalTags; + } + + /** + * Returns the full path to the image metadata json file, if provided. + * + * @return optional value of path to image json file + */ + public Optional getImageJsonPath() { + return Optional.ofNullable(imageJsonPath); + } + + /** Validates parameters defined in this class that could not be done declaratively. */ + public void validate() { + if (targetImage.startsWith(TAR_IMAGE_PREFIX) && name == null) { + throw new CommandLine.ParameterException( + spec.commandLine(), + "Missing option: --name must be specified when using --target=tar://...."); + } + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonContainerConfigCliOptions.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonContainerConfigCliOptions.java new file mode 100644 index 0000000000..261499828b --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonContainerConfigCliOptions.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Ports; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.common.collect.ImmutableSet; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import picocli.CommandLine; + +/** Common command line options shared between jar and war command. */ +public class CommonContainerConfigCliOptions { + + @CommandLine.Option( + names = "--from", + paramLabel = "", + description = "The base image to use.") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String from; + + @CommandLine.Option( + names = "--expose", + paramLabel = "", + split = ",", + description = "Ports to expose on container, example: --expose=5000,7/udp.") + private List exposedPorts = Collections.emptyList(); + + @CommandLine.Option( + names = "--volumes", + paramLabel = "", + split = ",", + description = + "Directories on container to hold extra volumes, example: --volumes=/var/log,/var/log2.") + private List volumes = Collections.emptyList(); + + @CommandLine.Option( + names = "--environment-variables", + paramLabel = "=", + split = ",", + description = + "Environment variables to write into container, example: --environment-variables env1=env_value1,env2=env_value2.") + private Map environment = Collections.emptyMap(); + + @CommandLine.Option( + names = "--labels", + paramLabel = "=", + split = ",", + description = + "Labels to write into container metadata, example: --labels=label1=value1,label2=value2.") + private Map labels = Collections.emptyMap(); + + @CommandLine.Option( + names = {"-u", "--user"}, + paramLabel = "", + description = "The user to run the container as, example: --user=myuser:mygroup.") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String user; + + @CommandLine.Option( + names = {"--image-format"}, + defaultValue = "Docker", + paramLabel = "", + description = + "Format of container, candidates: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}.") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private ImageFormat format; + + @CommandLine.Option( + names = "--program-args", + paramLabel = "", + split = ",", + description = "Program arguments for container entrypoint.") + private List programArguments = Collections.emptyList(); + + @CommandLine.Option( + names = "--entrypoint", + paramLabel = "", + split = "\\s+", + description = + "Entrypoint for container. Overrides the default entrypoint, example: --entrypoint='custom entrypoint'") + private List entrypoint = Collections.emptyList(); + + @CommandLine.Option( + names = "--creation-time", + paramLabel = "", + description = + "The creation time of the container in milliseconds since epoch or iso8601 format. Overrides the default (1970-01-01T00:00:00Z)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private String creationTime; + + /** + * Returns the user-specified base image. + * + * @return an optional base image + */ + public Optional getFrom() { + return Optional.ofNullable(from); + } + + /** + * Returns set of {@link Port} representing ports to be exposed on container (if specified). + * + * @return set of exposed ports + */ + public Set getExposedPorts() { + return (exposedPorts == null) ? ImmutableSet.of() : Ports.parse(exposedPorts); + } + + /** + * Returns a set of {@link AbsoluteUnixPath} representing directories on container to hold volumes + * (if specified). + * + * @return set of volumes + */ + public Set getVolumes() { + if (volumes == null) { + return ImmutableSet.of(); + } + return volumes.stream().map(AbsoluteUnixPath::get).collect(Collectors.toSet()); + } + + public Map getEnvironment() { + return environment; + } + + public Map getLabels() { + return labels; + } + + public Optional getUser() { + return Optional.ofNullable(user); + } + + public Optional getFormat() { + return Optional.ofNullable(format); + } + + public List getProgramArguments() { + return programArguments; + } + + public List getEntrypoint() { + return entrypoint; + } + + /** + * Returns {@link Instant} representing creation time of container. + * + * @return an optional creation time + */ + public Optional getCreationTime() { + if (creationTime != null) { + return Optional.of(Instants.fromMillisOrIso8601(creationTime, "creationTime")); + } + return Optional.empty(); + } + + /** + * Returns {@code true} if the user-specified base image is jetty or if a custom base image is not + * specified. + * + * @return a boolean + * @throws InvalidImageReferenceException if image reference is invalid + */ + public Boolean isJettyBaseimage() throws InvalidImageReferenceException { + if (from != null) { + ImageReference baseImageReference = ImageReference.parse(from); + return baseImageReference.getRegistry().equals("registry-1.docker.io") + && baseImageReference.getRepository().equals("library/jetty"); + } + return true; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ContainerBuilders.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ContainerBuilders.java new file mode 100644 index 0000000000..1b0bc8f352 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ContainerBuilders.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.cloud.tools.jib.api.Jib.DOCKER_DAEMON_IMAGE_PREFIX; +import static com.google.cloud.tools.jib.api.Jib.REGISTRY_IMAGE_PREFIX; +import static com.google.cloud.tools.jib.api.Jib.TAR_IMAGE_PREFIX; + +import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import java.io.FileNotFoundException; +import java.nio.file.Paths; +import java.util.Set; + +/** Helper class for creating JibContainerBuilders from JibCli specifications. */ +public class ContainerBuilders { + + private ContainerBuilders() {} + + /** + * Creates a {@link JibContainerBuilder} depending on the base image specified. + * + * @param baseImageReference base image reference + * @param platforms platforms for multi-platform support in build command + * @param commonCliOptions common cli options + * @param logger console logger + * @return a {@link JibContainerBuilder} + * @throws InvalidImageReferenceException if the baseImage reference cannot be parsed + * @throws FileNotFoundException if credential helper file cannot be found + */ + public static JibContainerBuilder create( + String baseImageReference, + Set platforms, + CommonCliOptions commonCliOptions, + ConsoleLogger logger) + throws InvalidImageReferenceException, FileNotFoundException { + if (baseImageReference.startsWith(DOCKER_DAEMON_IMAGE_PREFIX)) { + return Jib.from( + DockerDaemonImage.named(baseImageReference.replaceFirst(DOCKER_DAEMON_IMAGE_PREFIX, ""))); + } + if (baseImageReference.startsWith(TAR_IMAGE_PREFIX)) { + return Jib.from( + TarImage.at(Paths.get(baseImageReference.replaceFirst(TAR_IMAGE_PREFIX, "")))); + } + ImageReference imageReference = + ImageReference.parse(baseImageReference.replaceFirst(REGISTRY_IMAGE_PREFIX, "")); + RegistryImage registryImage = RegistryImage.named(imageReference); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage( + imageReference, + logEvent -> logger.log(logEvent.getLevel(), logEvent.getMessage()))); + Credentials.getFromCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers) + .forEach(registryImage::addCredentialRetriever); + JibContainerBuilder containerBuilder = Jib.from(registryImage); + if (!platforms.isEmpty()) { + containerBuilder.setPlatforms(platforms); + } + return containerBuilder; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Containerizers.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Containerizers.java new file mode 100644 index 0000000000..a880b37861 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Containerizers.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.cloud.tools.jib.api.Jib.DOCKER_DAEMON_IMAGE_PREFIX; +import static com.google.cloud.tools.jib.api.Jib.REGISTRY_IMAGE_PREFIX; +import static com.google.cloud.tools.jib.api.Jib.TAR_IMAGE_PREFIX; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; +import java.io.FileNotFoundException; +import java.nio.file.Paths; +import java.util.List; + +/** Helper class for creating Containerizers from JibCli specifications. */ +public class Containerizers { + + private Containerizers() {} + + /** + * Create a Containerizer from a command line specification. + * + * @param commonCliOptions common cli options + * @param logger a logger to inject into the build + * @param cacheDirectories the location of the relevant caches for this build + * @return a populated Containerizer + * @throws InvalidImageReferenceException if the image reference could not be parsed + * @throws FileNotFoundException if a credential helper file is not found + */ + public static Containerizer from( + CommonCliOptions commonCliOptions, ConsoleLogger logger, CacheDirectories cacheDirectories) + throws InvalidImageReferenceException, FileNotFoundException { + Containerizer containerizer = create(commonCliOptions, logger); + + applyHandlers(containerizer, logger); + applyConfiguration(containerizer, commonCliOptions, cacheDirectories); + + return containerizer; + } + + private static Containerizer create(CommonCliOptions commonCliOptions, ConsoleLogger logger) + throws InvalidImageReferenceException, FileNotFoundException { + String imageSpec = commonCliOptions.getTargetImage(); + if (imageSpec.startsWith(DOCKER_DAEMON_IMAGE_PREFIX)) { + // TODO: allow setting docker env and docker executable (along with path/env) + return Containerizer.to( + DockerDaemonImage.named(imageSpec.replaceFirst(DOCKER_DAEMON_IMAGE_PREFIX, ""))); + } + if (imageSpec.startsWith(TAR_IMAGE_PREFIX)) { + return Containerizer.to( + TarImage.at(Paths.get(imageSpec.replaceFirst(TAR_IMAGE_PREFIX, ""))) + .named(commonCliOptions.getName())); + } + ImageReference imageReference = + ImageReference.parse(imageSpec.replaceFirst(REGISTRY_IMAGE_PREFIX, "")); + RegistryImage registryImage = RegistryImage.named(imageReference); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage( + imageReference, + logEvent -> logger.log(logEvent.getLevel(), logEvent.getMessage()))); + Credentials.getToCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers) + .forEach(registryImage::addCredentialRetriever); + return Containerizer.to(registryImage); + } + + private static void applyConfiguration( + Containerizer containerizer, + CommonCliOptions commonCliOptions, + CacheDirectories cacheDirectories) { + containerizer.setToolName(VersionInfo.TOOL_NAME); + containerizer.setToolVersion(VersionInfo.getVersionSimple()); + + // TODO: it's strange that we use system properties to set these + // TODO: perhaps we should expose these as configuration options on the containerizer + if (commonCliOptions.isSendCredentialsOverHttp()) { + System.setProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP, Boolean.TRUE.toString()); + } + if (commonCliOptions.isSerialize()) { + System.setProperty(JibSystemProperties.SERIALIZE, Boolean.TRUE.toString()); + } + + containerizer.setAllowInsecureRegistries(commonCliOptions.isAllowInsecureRegistries()); + cacheDirectories.getBaseImageCache().ifPresent(containerizer::setBaseImageLayersCache); + containerizer.setApplicationLayersCache(cacheDirectories.getApplicationLayersCache()); + + commonCliOptions.getAdditionalTags().forEach(containerizer::withAdditionalTag); + } + + private static void applyHandlers(Containerizer containerizer, ConsoleLogger consoleLogger) { + containerizer + .addEventHandler( + LogEvent.class, + logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) + .addEventHandler( + ProgressEvent.class, + new ProgressEventHandler( + update -> { + List footer = + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedLeafTasks()); + footer.add(""); + consoleLogger.setFooter(footer); + })); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Credentials.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Credentials.java new file mode 100644 index 0000000000..ed70d3d74d --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Credentials.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import java.io.FileNotFoundException; +import java.util.List; + +/** + * A helper class to process command line args and generate a list of {@link CredentialRetriever}s. + */ +public class Credentials { + + private Credentials() {} + + /** + * Gets credentials for a target image registry. + * + * @param commonCliOptions common cli options + * @param defaultCredentialRetrievers An initialized {@link DefaultCredentialRetrievers} to use + * @return a list of credentials for a target image registry + * @throws FileNotFoundException when a credential helper file cannot be found + */ + public static List getToCredentialRetrievers( + CommonCliOptions commonCliOptions, DefaultCredentialRetrievers defaultCredentialRetrievers) + throws FileNotFoundException { + // these are all mutually exclusive as enforced by the CLI + commonCliOptions + .getUsernamePassword() + .ifPresent( + credential -> + defaultCredentialRetrievers.setKnownCredential( + credential, "--username/--password")); + commonCliOptions + .getToUsernamePassword() + .ifPresent( + credential -> + defaultCredentialRetrievers.setKnownCredential( + credential, "--to-username/--to-password")); + commonCliOptions + .getCredentialHelper() + .ifPresent(defaultCredentialRetrievers::setCredentialHelper); + commonCliOptions + .getToCredentialHelper() + .ifPresent(defaultCredentialRetrievers::setCredentialHelper); + + return defaultCredentialRetrievers.asList(); + } + + /** + * Gets credentials for a base image registry. + * + * @param commonCliOptions common cli options + * @param defaultCredentialRetrievers An initialized {@link DefaultCredentialRetrievers} to use + * @return a list of credentials for a base image registry + * @throws FileNotFoundException when a credential helper file cannot be found + */ + public static List getFromCredentialRetrievers( + CommonCliOptions commonCliOptions, DefaultCredentialRetrievers defaultCredentialRetrievers) + throws FileNotFoundException { + // these are all mutually exclusive as enforced by the CLI + + commonCliOptions + .getUsernamePassword() + .ifPresent( + credential -> + defaultCredentialRetrievers.setKnownCredential( + credential, "--username/--password")); + commonCliOptions + .getFromUsernamePassword() + .ifPresent( + credential -> + defaultCredentialRetrievers.setKnownCredential( + credential, "--from-username/--from-password")); + commonCliOptions + .getCredentialHelper() + .ifPresent(defaultCredentialRetrievers::setCredentialHelper); + commonCliOptions + .getFromCredentialHelper() + .ifPresent(defaultCredentialRetrievers::setCredentialHelper); + + return defaultCredentialRetrievers.asList(); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Instants.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Instants.java new file mode 100644 index 0000000000..933d5ae7a1 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Instants.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; + +/** Helper class to convert various strings in a buildfile to Instants. */ +public class Instants { + + private Instants() {} + + /** + * Parses a time string into Instant. The string must be time in milliseconds since unix epoch or + * an iso8601 datetime. + * + * @param time in milliseconds since epoch or iso8601 format + * @param fieldName name of field being parsed (for error messaging) + * @return Instant value of parsed time + */ + public static Instant fromMillisOrIso8601(String time, String fieldName) { + try { + return Instant.ofEpochMilli(Long.parseLong(time)); + } catch (NumberFormatException nfe) { + // TODO: copied from PluginConfigurationProcessor, find a way to share better + try { + DateTimeFormatter formatter = + new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_DATE_TIME) + .optionalStart() + .appendOffset("+HHmm", "+0000") + .optionalEnd() + .toFormatter(); + return formatter.parse(time, Instant::from); + } catch (DateTimeParseException dtpe) { + throw new IllegalArgumentException( + fieldName + + " must be a number of milliseconds since epoch or an ISO 8601 formatted date"); + } + } + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java new file mode 100644 index 0000000000..7198223556 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.cli.jar.JarFiles; +import com.google.cloud.tools.jib.cli.jar.ProcessingMode; +import com.google.cloud.tools.jib.cli.logging.CliLogger; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import com.google.common.collect.Multimaps; +import com.google.common.util.concurrent.Futures; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +@CommandLine.Command( + name = "jar", + mixinStandardHelpOptions = true, + showAtFileInUsageHelp = true, + description = "Containerize a jar") +public class Jar implements Callable { + + @CommandLine.Spec + @SuppressWarnings("NullAway.Init") // initialized by picocli + private CommandSpec spec; + + @CommandLine.Mixin + @VisibleForTesting + @SuppressWarnings("NullAway.Init") // initialized by picocli + CommonCliOptions commonCliOptions; + + @CommandLine.Mixin + @VisibleForTesting + @SuppressWarnings("NullAway.Init") // initialized by picocli + CommonContainerConfigCliOptions commonContainerConfigCliOptions; + + @CommandLine.Parameters(description = "The path to the jar file (ex: path/to/my-jar.jar)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path jarFile; + + @CommandLine.Option( + names = "--mode", + defaultValue = "exploded", + paramLabel = "", + description = + "The jar processing mode, candidates: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private ProcessingMode mode; + + @CommandLine.Option( + names = "--jvm-flags", + paramLabel = "", + split = ",", + description = "JVM arguments, example: --jvm-flags=-Dmy.property=value,-Xshare:off") + private List jvmFlags = Collections.emptyList(); + + @Override + public Integer call() { + commonCliOptions.validate(); + SingleThreadedExecutor executor = new SingleThreadedExecutor(); + ConsoleLogger logger = + CliLogger.newLogger( + commonCliOptions.getVerbosity(), + commonCliOptions.getHttpTrace(), + commonCliOptions.getConsoleOutput(), + spec.commandLine().getOut(), + spec.commandLine().getErr(), + executor); + Future> updateCheckFuture = Futures.immediateFuture(Optional.empty()); + try { + JibCli.configureHttpLogging(commonCliOptions.getHttpTrace().toJulLevel()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + updateCheckFuture = + JibCli.newUpdateChecker( + globalConfig, + commonCliOptions.getVerbosity(), + logEvent -> logger.log(logEvent.getLevel(), logEvent.getMessage())); + if (!Files.exists(jarFile)) { + logger.log(LogEvent.Level.ERROR, "The file path provided does not exist: " + jarFile); + return 1; + } + if (Files.isDirectory(jarFile)) { + logger.log( + LogEvent.Level.ERROR, + "The file path provided is for a directory. Please provide a path to a JAR: " + + jarFile); + return 1; + } + if (!commonContainerConfigCliOptions.getEntrypoint().isEmpty() && !jvmFlags.isEmpty()) { + logger.log(LogEvent.Level.WARN, "--jvm-flags is ignored when --entrypoint is specified"); + } + + Path jarFileParentDir = Verify.verifyNotNull(jarFile.toAbsolutePath().getParent()); + CacheDirectories cacheDirectories = CacheDirectories.from(commonCliOptions, jarFileParentDir); + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarFile, cacheDirectories, this, commonContainerConfigCliOptions); + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + processor, this, commonCliOptions, commonContainerConfigCliOptions, logger); + Containerizer containerizer = Containerizers.from(commonCliOptions, logger, cacheDirectories); + + // Enable registry mirrors + Multimaps.asMap(globalConfig.getRegistryMirrors()).forEach(containerizer::addRegistryMirrors); + + JibContainer jibContainer = containerBuilder.containerize(containerizer); + JibCli.writeImageJson(commonCliOptions.getImageJsonPath(), jibContainer); + } catch (InterruptedException ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + Thread.currentThread().interrupt(); + return 1; + } catch (Exception ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + return 1; + } finally { + JibCli.finishUpdateChecker(logger, updateCheckFuture); + executor.shutDownAndAwaitTermination(Duration.ofSeconds(3)); + } + return 0; + } + + public List getJvmFlags() { + return jvmFlags; + } + + public ProcessingMode getMode() { + return mode; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java new file mode 100644 index 0000000000..436599b34c --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.cloud.tools.jib.plugins.common.ImageMetadataOutput; +import com.google.cloud.tools.jib.plugins.common.UpdateChecker; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.util.concurrent.Futures; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import picocli.CommandLine; + +@CommandLine.Command( + name = "jib", + versionProvider = VersionInfo.class, + mixinStandardHelpOptions = true, + showAtFileInUsageHelp = true, + synopsisSubcommandLabel = "COMMAND", + description = "A tool for creating container images", + subcommands = {Build.class, Jar.class, War.class}) +public class JibCli { + + private static final String VERSION_URL = "https://storage.googleapis.com/jib-versions/jib-cli"; + + static Logger configureHttpLogging(Level level) { + // To instantiate the static HttpTransport logger field. + // Fixes https://github.com/GoogleContainerTools/jib/issues/3156. + new ApacheHttpTransport(); + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(level); + + Logger logger = Logger.getLogger(HttpTransport.class.getName()); + logger.setLevel(level); + logger.addHandler(consoleHandler); + return logger; + } + + static void logTerminatingException( + ConsoleLogger consoleLogger, Exception exception, boolean logStackTrace) { + if (logStackTrace) { + StringWriter writer = new StringWriter(); + exception.printStackTrace(new PrintWriter(writer)); + consoleLogger.log(LogEvent.Level.ERROR, writer.toString()); + } + + consoleLogger.log( + LogEvent.Level.ERROR, + "\u001B[31;1m" + + exception.getClass().getName() + + ": " + + exception.getMessage() + + "\u001B[0m"); + } + + static Future> newUpdateChecker( + GlobalConfig globalConfig, Verbosity verbosity, Consumer log) { + if (!verbosity.atLeast(Verbosity.info) || globalConfig.isDisableUpdateCheck()) { + return Futures.immediateFuture(Optional.empty()); + } + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + return UpdateChecker.checkForUpdate( + executorService, VERSION_URL, VersionInfo.TOOL_NAME, VersionInfo.getVersionSimple(), log); + } finally { + executorService.shutdown(); + } + } + + static void finishUpdateChecker( + ConsoleLogger logger, Future> updateCheckFuture) { + UpdateChecker.finishUpdateCheck(updateCheckFuture) + .ifPresent( + latestVersion -> { + String cliReleaseUrl = + ProjectInfo.GITHUB_URL + "/releases/tag/v" + latestVersion + "-cli"; + String changelogUrl = ProjectInfo.GITHUB_URL + "/blob/master/jib-cli/CHANGELOG.md"; + String privacyUrl = ProjectInfo.GITHUB_URL + "/blob/master/docs/privacy.md"; + String message = + String.format( + "\n\u001B[33mA new version of Jib CLI (%s) is available (currently using %s). Download the latest" + + " Jib CLI version from %s\n%s\u001B[0m\n\nPlease see %s for info on disabling this update check.\n", + latestVersion, + VersionInfo.getVersionSimple(), + cliReleaseUrl, + changelogUrl, + privacyUrl); + logger.log(LogEvent.Level.LIFECYCLE, message); + }); + } + + /** + * Writes image details (imageId, digest, tags, etc.) to a json file, if the path to the json is + * provided. + * + * @param imageJsonOutputPath optional path to json file (for example, + * path/to/json/jib-image.json) + * @param jibContainer the {@link JibContainer} to derive image details from + * @throws IOException if error occurs when writing to the json file. + */ + static void writeImageJson(Optional imageJsonOutputPath, JibContainer jibContainer) + throws IOException { + if (imageJsonOutputPath.isPresent()) { + ImageMetadataOutput metadataOutput = ImageMetadataOutput.fromJibContainer(jibContainer); + Files.write( + imageJsonOutputPath.get(), metadataOutput.toJson().getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * The magic starts here. + * + * @param args the command-line arguments + */ + public static void main(String[] args) { + int exitCode = + new CommandLine(new JibCli()) + .setParameterExceptionHandler(new ShortErrorMessageHandler()) + .execute(args); + System.exit(exitCode); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ShortErrorMessageHandler.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ShortErrorMessageHandler.java new file mode 100644 index 0000000000..bf1ce2b860 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ShortErrorMessageHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import java.io.PrintWriter; +import picocli.CommandLine; +import picocli.CommandLine.IParameterExceptionHandler; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; + +/** Class to print a short error message when an invalid input is passed in. */ +public class ShortErrorMessageHandler implements IParameterExceptionHandler { + + @Override + public int handleParseException(ParameterException exception, String[] args) { + CommandLine command = exception.getCommandLine(); + PrintWriter writer = command.getErr(); + + // Print error message + writer.println(exception.getMessage()); + CommandLine.UnmatchedArgumentException.printSuggestions(exception, writer); + writer.print(command.getHelp().fullSynopsis()); + + CommandSpec commandSpec = command.getCommandSpec(); + writer.printf("Run '%s --help' for more information on usage.%n", commandSpec.qualifiedName()); + return command.getExitCodeExceptionMapper() != null + ? command.getExitCodeExceptionMapper().getExitCode(exception) + : commandSpec.exitCodeOnInvalidInput(); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/War.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/War.java new file mode 100644 index 0000000000..7959c189e3 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/War.java @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.cli.logging.CliLogger; +import com.google.cloud.tools.jib.cli.war.WarFiles; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import com.google.common.collect.Multimaps; +import com.google.common.util.concurrent.Futures; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import picocli.CommandLine; + +@CommandLine.Command( + name = "war", + mixinStandardHelpOptions = true, + showAtFileInUsageHelp = true, + description = "Containerize a war") +public class War implements Callable { + @CommandLine.Spec + @SuppressWarnings("NullAway.Init") // initialized by picocli + private CommandLine.Model.CommandSpec spec; + + @CommandLine.Mixin + @VisibleForTesting + @SuppressWarnings("NullAway.Init") // initialized by picocli + CommonCliOptions commonCliOptions; + + @CommandLine.Mixin + @VisibleForTesting + @SuppressWarnings("NullAway.Init") // initialized by picocli + CommonContainerConfigCliOptions commonContainerConfigCliOptions; + + @CommandLine.Parameters(description = "The path to the war file (ex: path/to/my-war.war)") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path warFile; + + @CommandLine.Option( + names = "--app-root", + paramLabel = "", + description = "The app root on the container") + @SuppressWarnings("NullAway.Init") // initialized by picocli + private Path appRoot; + + @Override + public Integer call() { + commonCliOptions.validate(); + SingleThreadedExecutor executor = new SingleThreadedExecutor(); + ConsoleLogger logger = + CliLogger.newLogger( + commonCliOptions.getVerbosity(), + commonCliOptions.getHttpTrace(), + commonCliOptions.getConsoleOutput(), + spec.commandLine().getOut(), + spec.commandLine().getErr(), + executor); + Future> updateCheckFuture = Futures.immediateFuture(Optional.empty()); + try { + JibCli.configureHttpLogging(commonCliOptions.getHttpTrace().toJulLevel()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + updateCheckFuture = + JibCli.newUpdateChecker( + globalConfig, + commonCliOptions.getVerbosity(), + logEvent -> logger.log(logEvent.getLevel(), logEvent.getMessage())); + if (!Files.exists(warFile)) { + logger.log(LogEvent.Level.ERROR, "The file path provided does not exist: " + warFile); + return 1; + } + if (Files.isDirectory(warFile)) { + logger.log( + LogEvent.Level.ERROR, + "The file path provided is for a directory. Please provide a path to a WAR: " + + warFile); + return 1; + } + + Path warFileParentDir = Verify.verifyNotNull(warFile.toAbsolutePath().getParent()); + CacheDirectories cacheDirectories = CacheDirectories.from(commonCliOptions, warFileParentDir); + ArtifactProcessor processor = + ArtifactProcessors.fromWar( + warFile, cacheDirectories, this, commonContainerConfigCliOptions); + JibContainerBuilder containerBuilder = + WarFiles.toJibContainerBuilder( + processor, commonCliOptions, commonContainerConfigCliOptions, logger); + Containerizer containerizer = Containerizers.from(commonCliOptions, logger, cacheDirectories); + + // Enable registry mirrors + Multimaps.asMap(globalConfig.getRegistryMirrors()).forEach(containerizer::addRegistryMirrors); + + JibContainer jibContainer = containerBuilder.containerize(containerizer); + JibCli.writeImageJson(commonCliOptions.getImageJsonPath(), jibContainer); + } catch (InterruptedException ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + Thread.currentThread().interrupt(); + return 1; + } catch (Exception ex) { + JibCli.logTerminatingException(logger, ex, commonCliOptions.isStacktrace()); + return 1; + } finally { + JibCli.finishUpdateChecker(logger, updateCheckFuture); + executor.shutDownAndAwaitTermination(Duration.ofSeconds(3)); + } + return 0; + } + + /** + * Returns the user-specified app root in the container. + * + * @return a user configured app root + */ + public Optional getAppRoot() { + if (appRoot == null) { + return Optional.empty(); + } + return Optional.of(AbsoluteUnixPath.fromPath(appRoot)); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpec.java new file mode 100644 index 0000000000..1da87b205b --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpec.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying archive layers. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * name: "my tar layer"
+ * archive: output/mytar.tgz
+ * // optional mediatype
+ * mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
+ * }
+ */ +@JsonDeserialize(using = JsonDeserializer.None.class) // required since LayerSpec overrides this +public class ArchiveLayerSpec implements LayerSpec { + + private final String name; + // TODO: arhive should maybe be a uri to support file paths or urls + private final Path archive; + @Nullable private final String mediaType; + + /** + * Constructor for use by jackson to populate this object. + * + * @param name a unique name for this layer + * @param archive a path to an archive file + * @param mediaType the media type of the file + */ + @JsonCreator + public ArchiveLayerSpec( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "archive", required = true) String archive, + @JsonProperty("mediaType") String mediaType) { + Validator.checkNotNullAndNotEmpty(name, "name"); + Validator.checkNotNullAndNotEmpty(archive, "archive"); + Validator.checkNullOrNotEmpty(mediaType, "mediaType"); + this.name = name; + this.archive = Paths.get(archive); + this.mediaType = mediaType; + } + + public String getName() { + return name; + } + + public Path getArchive() { + return archive; + } + + public Optional getMediaType() { + return Optional.ofNullable(mediaType); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpec.java new file mode 100644 index 0000000000..cd1aa4927a --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpec.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** + * A yaml block for specifying a base image with support for multi platform selections. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * image: gcr.io/example/baseimage
+ * platforms:
+ *   - see }{@link PlatformSpec}{@code
+ *   - see }{@link PlatformSpec}{@code
+ * }
+ */ +public class BaseImageSpec { + private final String image; + private final List platforms; + + /** + * Constructor for use by jackson to populate this object. + * + * @param image an image reference for a base image + * @param platforms a list of platforms when using a manifest list or image index + */ + @JsonCreator + public BaseImageSpec( + @JsonProperty(value = "image", required = true) String image, + @JsonProperty("platforms") List platforms) { + Validator.checkNotNullAndNotEmpty(image, "image"); + Validator.checkNullOrNonNullEntries(platforms, "platforms"); + this.image = image; + this.platforms = platforms == null ? ImmutableList.of() : platforms; + } + + public String getImage() { + return image; + } + + public List getPlatforms() { + return platforms; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpec.java new file mode 100644 index 0000000000..c7c444e111 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpec.java @@ -0,0 +1,218 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.api.Ports; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.cli.Instants; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying a jib cli buildfile. + * + *

Example use of this yaml. + * + *

{@code
+ * apiVersion: v1alpha1
+ * kind: BuildFile
+ * from: see }{@link BaseImageSpec}{@code
+ * creationTime: 100
+ * format: docker
+ * environment:
+ *   env_key: env_value
+ * labels:
+ *   label_key: label_value
+ * volumes:
+ *   - /my/volume
+ * exposedPorts:
+ *   - 8080
+ * user: username
+ * workingDirectory: /workspace
+ * entrypoint:
+ *   - java
+ *   - -jar
+ * cmd:
+ *   - myjar.jar
+ * layers: see }{@link LayersSpec}{@code
+ * }
+ */ +public class BuildFileSpec { + private final String apiVersion; + private final String kind; + @Nullable private final BaseImageSpec from; + @Nullable private final Instant creationTime; + @Nullable private final ImageFormat format; + private final Map environment; + private final Map labels; + private final Set volumes; + private final Set exposedPorts; + @Nullable private final String user; + @Nullable private final AbsoluteUnixPath workingDirectory; + /** + * Entrypoint has special behavior as a nullable list. When null, it delegates to the existing + * base image entrypoint. If non null (including empty) it overwrites the base image entrypoint. + */ + @Nullable private final List entrypoint; + /** + * Cmd has special behavior as a nullable list. When null, it delegates to the existing base image + * cmd. If non null (including empty) it overwrites the base image cmd. + */ + @Nullable private final List cmd; + + @Nullable private final LayersSpec layers; + + /** + * Constructor for use by jackson to populate this object. + * + * @param apiVersion the api version of this buildfile + * @param kind the type of configuration file (always BuildFile) + * @param from a {@link BaseImageSpec} for specifying the base image + * @param creationTime in milliseconds since epoch or ISO 8601 datetime + * @param format of the container, valid values in {@link ImageFormat} + * @param environment to write into container + * @param labels to write into container metadata + * @param volumes directories on container that may hold external volumes + * @param exposedPorts a set of ports to expose on the container + * @param user the username or id to run the container + * @param workingDirectory an absolute path to the default working directory + * @param entrypoint the container entry point + * @param cmd the container entrypoint command arguments + * @param layers a list of {@link LayerSpec} that define the container filesystem + */ + @JsonCreator + public BuildFileSpec( + @JsonProperty(value = "apiVersion", required = true) String apiVersion, + @JsonProperty(value = "kind", required = true) String kind, + @JsonProperty("from") BaseImageSpec from, + @JsonProperty("creationTime") String creationTime, + @JsonProperty("format") String format, + @JsonProperty("environment") Map environment, + @JsonProperty("labels") Map labels, + @JsonProperty("volumes") Set volumes, + @JsonProperty("exposedPorts") List exposedPorts, + @JsonProperty("user") String user, + @JsonProperty("workingDirectory") String workingDirectory, + @JsonProperty("entrypoint") List entrypoint, + @JsonProperty("cmd") List cmd, + @JsonProperty("layers") LayersSpec layers) { + + Validator.checkNotNullAndNotEmpty(apiVersion, "apiVersion"); + Validator.checkEquals(kind, "kind", "BuildFile"); + + Validator.checkNullOrNotEmpty(creationTime, "creationTime"); + Validator.checkNullOrNotEmpty(format, "format"); + Validator.checkNullOrNonNullNonEmptyEntries(environment, "environment"); + Validator.checkNullOrNonNullNonEmptyEntries(labels, "labels"); + Validator.checkNullOrNonNullNonEmptyEntries(volumes, "volumes"); + Validator.checkNullOrNonNullNonEmptyEntries(exposedPorts, "exposedPorts"); + Validator.checkNullOrNotEmpty(user, "user"); + Validator.checkNullOrNotEmpty(workingDirectory, "workingDirectory"); + Validator.checkNullOrNonNullNonEmptyEntries(entrypoint, "entrypoint"); + Validator.checkNullOrNonNullNonEmptyEntries(cmd, "cmd"); + + this.apiVersion = apiVersion; + Preconditions.checkArgument( + "BuildFile".equals(kind), "Field 'kind' must be BuildFile but is " + kind); + this.kind = kind; + this.from = from; + this.creationTime = + (creationTime == null) ? null : Instants.fromMillisOrIso8601(creationTime, "creationTime"); + this.format = (format == null) ? null : ImageFormat.valueOf(format); + this.environment = (environment == null) ? ImmutableMap.of() : environment; + this.labels = (labels == null) ? ImmutableMap.of() : labels; + this.volumes = + (volumes == null) + ? ImmutableSet.of() + : volumes.stream().map(AbsoluteUnixPath::get).collect(Collectors.toSet()); + this.exposedPorts = (exposedPorts == null) ? ImmutableSet.of() : Ports.parse(exposedPorts); + this.user = user; + this.workingDirectory = + (workingDirectory == null) ? null : AbsoluteUnixPath.get(workingDirectory); + this.entrypoint = entrypoint; + this.cmd = cmd; + this.layers = layers; + } + + public String getApiVersion() { + return apiVersion; + } + + public String getKind() { + return kind; + } + + public Optional getFrom() { + return Optional.ofNullable(from); + } + + public Optional getCreationTime() { + return Optional.ofNullable(creationTime); + } + + public Optional getFormat() { + return Optional.ofNullable(format); + } + + public Map getEnvironment() { + return environment; + } + + public Map getLabels() { + return labels; + } + + public Set getVolumes() { + return volumes; + } + + public Set getExposedPorts() { + return exposedPorts; + } + + public Optional getUser() { + return Optional.ofNullable(user); + } + + public Optional getWorkingDirectory() { + return Optional.ofNullable(workingDirectory); + } + + public Optional> getEntrypoint() { + return Optional.ofNullable(entrypoint); + } + + public Optional> getCmd() { + return Optional.ofNullable(cmd); + } + + public Optional getLayers() { + return Optional.ofNullable(layers); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFiles.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFiles.java new file mode 100644 index 0000000000..d67de21515 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFiles.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.cli.Build; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.cli.ContainerBuilders; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.base.Charsets; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.text.StringSubstitutor; +import org.apache.commons.text.io.StringSubstitutorReader; + +/** Class to convert BuildFiles to build container representations. */ +public class BuildFiles { + + private BuildFiles() {} + + /** Read a build file from disk and apply templating parameters. */ + private static BuildFileSpec toBuildFileSpec( + Path buildFilePath, Map templateParameters) throws IOException { + ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + StringSubstitutor templater = + new StringSubstitutor(templateParameters).setEnableUndefinedVariableException(true); + try (StringSubstitutorReader reader = + new StringSubstitutorReader( + Files.newBufferedReader(buildFilePath, Charsets.UTF_8), templater)) { + return yamlObjectMapper.readValue(reader, BuildFileSpec.class); + } + } + + /** + * Read a buildfile from disk and generate a JibContainerBuilder instance. All parsing of files + * considers the directory the buildfile is located in as the working directory. + * + * @param projectRoot the root context directory of this build + * @param buildFilePath a file containing the build definition + * @param buildCommandOptions the build configuration from the command line + * @param commonCliOptions common cli options + * @param logger a logger to inject into various objects that do logging + * @return a {@link JibContainerBuilder} generated from the contents of {@code buildFilePath} + * @throws IOException if an I/O error occurs opening the file, or an error occurs while + * traversing files on the filesystem + * @throws InvalidImageReferenceException if the baseImage reference can not be parsed + */ + public static JibContainerBuilder toJibContainerBuilder( + Path projectRoot, + Path buildFilePath, + Build buildCommandOptions, + CommonCliOptions commonCliOptions, + ConsoleLogger logger) + throws InvalidImageReferenceException, IOException { + BuildFileSpec buildFile = + toBuildFileSpec(buildFilePath, buildCommandOptions.getTemplateParameters()); + Optional baseImageSpec = buildFile.getFrom(); + JibContainerBuilder containerBuilder = + baseImageSpec.isPresent() + ? createJibContainerBuilder(baseImageSpec.get(), commonCliOptions, logger) + : Jib.fromScratch(); + + buildFile.getCreationTime().ifPresent(containerBuilder::setCreationTime); + buildFile.getFormat().ifPresent(containerBuilder::setFormat); + containerBuilder.setEnvironment(buildFile.getEnvironment()); + containerBuilder.setLabels(buildFile.getLabels()); + containerBuilder.setVolumes(buildFile.getVolumes()); + containerBuilder.setExposedPorts(buildFile.getExposedPorts()); + buildFile.getUser().ifPresent(containerBuilder::setUser); + buildFile.getWorkingDirectory().ifPresent(containerBuilder::setWorkingDirectory); + buildFile.getEntrypoint().ifPresent(containerBuilder::setEntrypoint); + buildFile.getCmd().ifPresent(containerBuilder::setProgramArguments); + + Optional layersSpec = buildFile.getLayers(); + if (layersSpec.isPresent()) { + containerBuilder.setFileEntriesLayers(Layers.toLayers(projectRoot, layersSpec.get())); + } + return containerBuilder; + } + + private static JibContainerBuilder createJibContainerBuilder( + BaseImageSpec baseImageSpec, CommonCliOptions commonCliOptions, ConsoleLogger logger) + throws InvalidImageReferenceException, FileNotFoundException { + LinkedHashSet platforms = + baseImageSpec.getPlatforms().stream() + .map(platformSpec -> new Platform(platformSpec.getArchitecture(), platformSpec.getOs())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return ContainerBuilders.create(baseImageSpec.getImage(), platforms, commonCliOptions, logger); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/CopySpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/CopySpec.java new file mode 100644 index 0000000000..506a9de175 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/CopySpec.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying a copy directive. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * src: path/to/somewhere
+ * dest: /absolute/path/on/container
+ * includes:
+ *   - **\/*.txt
+ * excludes:
+ *   - **\/goose.txt
+ *   - **\/moose.txt
+ * properties: see }{@link FilePropertiesSpec}{@code
+ * }
+ */ +public class CopySpec { + private final Path src; + private final AbsoluteUnixPath dest; + // extra metadata to determine if the target can be considered a file + private final boolean destEndsWithSlash; + + @Nullable private final FilePropertiesSpec properties; + private final List excludes; + private final List includes; + + /** + * Constructor for use by jackson to populate this object. + * + * @param src a file/directory on the local filesystem to copy *from* + * @param dest an absolute unix style path to copy *to* on the container + * @param includes glob to filter files to include from "src" + * @param excludes glob to filter out files included by "src" and "includes" (applied last) + * @param properties a {@link FilePropertiesSpec} that applies to all files in this copy + */ + @JsonCreator + public CopySpec( + @JsonProperty(value = "src", required = true) String src, + @JsonProperty(value = "dest", required = true) String dest, + @JsonProperty("includes") List includes, + @JsonProperty("excludes") List excludes, + @JsonProperty("properties") FilePropertiesSpec properties) { + Validator.checkNotNullAndNotEmpty(src, "src"); + Validator.checkNotNullAndNotEmpty(dest, "dest"); + Validator.checkNullOrNonNullNonEmptyEntries(includes, "includes"); + Validator.checkNullOrNonNullNonEmptyEntries(excludes, "excludes"); + this.src = Paths.get(src); + this.dest = AbsoluteUnixPath.get(dest); + this.destEndsWithSlash = dest.endsWith("/"); + this.excludes = (excludes == null) ? ImmutableList.of() : excludes; + this.includes = (includes == null) ? ImmutableList.of() : includes; + this.properties = properties; + } + + public Path getSrc() { + return src; + } + + public AbsoluteUnixPath getDest() { + return dest; + } + + public boolean isDestEndsWithSlash() { + return destEndsWithSlash; + } + + public List getExcludes() { + return excludes; + } + + public List getIncludes() { + return includes; + } + + public Optional getProperties() { + return Optional.ofNullable(properties); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpec.java new file mode 100644 index 0000000000..b20ccb8dd9 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpec.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying files layers. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * name: "my classes layer"
+ * files:
+ *   - }{@link CopySpec}{@code
+ *   - }{@link CopySpec}{@code
+ * // optional properties
+ * properties: see }{@link FilePropertiesSpec}{@code
+ * }
+ */ +@JsonDeserialize(using = JsonDeserializer.None.class) // required since LayerSpec overrides this +public class FileLayerSpec implements LayerSpec { + private final String name; + private final List files; + @Nullable private final FilePropertiesSpec properties; + + /** + * Constructor for use by jackson to populate this object. + * + * @param name a unique name for this layer + * @param files a list of {@link CopySpec} describing files to add to the layer + * @param properties a {@link FilePropertiesSpec} that applies to all files in this layer + */ + @JsonCreator + public FileLayerSpec( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "files", required = true) List files, + @JsonProperty("properties") FilePropertiesSpec properties) { + Validator.checkNotNullAndNotEmpty(name, "name"); + Validator.checkNotNullAndNotEmpty(files, "files"); + this.name = name; + this.properties = properties; + this.files = files; + } + + public String getName() { + return name; + } + + public List getFiles() { + return files; + } + + public Optional getProperties() { + return Optional.ofNullable(properties); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpec.java new file mode 100644 index 0000000000..18e589cc77 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpec.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.cli.Instants; +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying file properties (used in conjunction with layers). + * + *

Example use of this yaml snippet. + * + *

{@code
+ * filePermissions: 644
+ * directoryPermissions: 755
+ * user: foo
+ * group: bar
+ * timestamp: 100
+ * }
+ */ +public class FilePropertiesSpec { + @Nullable private final FilePermissions filePermissions; + @Nullable private final FilePermissions directoryPermissions; + @Nullable private final String user; + @Nullable private final String group; + @Nullable private final Instant timestamp; + + /** + * Constructor for use by jackson to populate this object. + * + * @param filePermissions octal string for file permissions + * @param directoryPermissions octal string for directory permissions + * @param user name or number for ownership user + * @param group name or number for ownership group + * @param timestamp in milliseconds since epoch or ISO 8601 datetime + */ + @JsonCreator + public FilePropertiesSpec( + @JsonProperty("filePermissions") String filePermissions, + @JsonProperty("directoryPermissions") String directoryPermissions, + @JsonProperty("user") String user, + @JsonProperty("group") String group, + @JsonProperty("timestamp") String timestamp) { + Validator.checkNullOrNotEmpty(filePermissions, "filePermissions"); + Validator.checkNullOrNotEmpty(directoryPermissions, "directoryPermissions"); + Validator.checkNullOrNotEmpty(user, "user"); + Validator.checkNullOrNotEmpty(group, "group"); + Validator.checkNullOrNotEmpty(timestamp, "timestamp"); + this.filePermissions = + filePermissions == null ? null : FilePermissions.fromOctalString(filePermissions); + this.directoryPermissions = + directoryPermissions == null ? null : FilePermissions.fromOctalString(directoryPermissions); + this.user = user; + this.group = group; + this.timestamp = + (timestamp == null) ? null : Instants.fromMillisOrIso8601(timestamp, "timestamp"); + } + + public Optional getFilePermissions() { + return Optional.ofNullable(filePermissions); + } + + public Optional getDirectoryPermissions() { + return Optional.ofNullable(directoryPermissions); + } + + public Optional getUser() { + return Optional.ofNullable(user); + } + + public Optional getGroup() { + return Optional.ofNullable(group); + } + + public Optional getTimestamp() { + return Optional.ofNullable(timestamp); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStack.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStack.java new file mode 100644 index 0000000000..191855e4d6 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStack.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.common.base.Preconditions; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * A class that keeps track of permissions for various stacking file permissions settings in {@link + * LayerSpec}. + */ +class FilePropertiesStack { + + // TODO perhaps use a fixed size list here + private final List stack = new ArrayList<>(3); + + private FilePermissions filePermissions; + private FilePermissions directoryPermissions; + private Instant modificationTime; + private String ownership; + + /** Create new FilePropertiesStack with defaults. */ + public FilePropertiesStack() { + setDefaults(); + } + + private void setDefaults() { + filePermissions = FilePermissions.DEFAULT_FILE_PERMISSIONS; + directoryPermissions = FilePermissions.DEFAULT_FOLDER_PERMISSIONS; + modificationTime = FileEntriesLayer.DEFAULT_MODIFICATION_TIME; + // TODO: get default from FileEntriesLayer (requires buildplan release) + ownership = ""; + } + + /** + * Add a new layer to the file properties stack. When adding a new layer, it is given highest + * priority when resolving properties. All values are recalculated. + */ + public void push(FilePropertiesSpec filePropertiesSpec) { + Preconditions.checkState( + stack.size() < 3, "Error in file properties stack push, stacking over 3"); + stack.add(filePropertiesSpec); + updateProperties(); + } + + /** Remove the last layer from the stack. All values are recalculated. */ + public void pop() { + Preconditions.checkState(!stack.isEmpty(), "Error in file properties stack pop, popping at 0"); + stack.remove(stack.size() - 1); + updateProperties(); + } + + private void updateProperties() { + // clear existing permissions before recalculating + setDefaults(); + + String user = null; + String group = null; + + // the item with the lowest index has the lowest priority + for (FilePropertiesSpec properties : stack) { + filePermissions = properties.getFilePermissions().orElse(filePermissions); + directoryPermissions = properties.getDirectoryPermissions().orElse(directoryPermissions); + modificationTime = properties.getTimestamp().orElse(modificationTime); + user = properties.getUser().orElse(user); + group = properties.getGroup().orElse(group); + } + // ownership calculations + ownership = (user != null ? user : "") + (group != null ? ":" + group : ""); + } + + public FilePermissions getFilePermissions() { + return filePermissions; + } + + public FilePermissions getDirectoryPermissions() { + return directoryPermissions; + } + + public Instant getModificationTime() { + return modificationTime; + } + + public String getOwnership() { + return ownership; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpec.java new file mode 100644 index 0000000000..2ba5319db2 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpec.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; + +/** + * Polymorphic yaml LayerSpec interface with custom deserializer, can parse both {@link + * ArchiveLayerSpec} and {@link FileLayerSpec}. + */ +@JsonDeserialize(using = LayerSpec.Deserializer.class) +public interface LayerSpec { + class Deserializer extends StdDeserializer { + + public Deserializer() { + super(LayerSpec.class); + } + + /** + * Deserializes to {@link ArchiveLayerSpec} if yaml contains "archive" field, to {@link + * FileLayerSpec} if yaml contains "files" field or throws {@link IOException} if neither is + * found or no "name" was specified for the layer entry. + */ + @Override + public LayerSpec deserialize(JsonParser jsonParser, DeserializationContext context) + throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + if (!node.has("name")) { + throw new IOException("Could not parse layer entry, missing required property 'name'"); + } + if (node.has("archive")) { + return jsonParser.getCodec().treeToValue(node, ArchiveLayerSpec.class); + } + if (node.has("files")) { + return jsonParser.getCodec().treeToValue(node, FileLayerSpec.class); + } + throw new IOException("Could not parse entry into ArchiveLayer or FileLayer"); + } + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Layers.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Layers.java new file mode 100644 index 0000000000..b8175582ae --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Layers.java @@ -0,0 +1,192 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Class to convert between different layer representations. */ +class Layers { + + private Layers() {} + + /** + * Convert a layer spec to a list of layer objects. + * + *

Does not handle missing directories for files added via this method. We can either prefill + * directories here, or allow passing of the file entry information directly to the reproducible + * layer builder + * + * @param buildRoot the directory to resolve relative paths, usually the directory where the build + * config file is located + * @param layersSpec a layersSpec containing configuration for all layers + * @return a {@link List} of {@link FileEntriesLayer} to use as part of a jib container build + * @throws IOException if traversing a directory fails + */ + static List toLayers(Path buildRoot, LayersSpec layersSpec) throws IOException { + List layers = new ArrayList<>(); + + FilePropertiesStack filePropertiesStack = new FilePropertiesStack(); + // base properties + layersSpec.getProperties().ifPresent(filePropertiesStack::push); + + for (LayerSpec entry : layersSpec.getEntries()) { + // each loop is a new layer + if (entry instanceof FileLayerSpec) { + FileEntriesLayer.Builder layerBuiler = FileEntriesLayer.builder(); + + FileLayerSpec fileLayer = (FileLayerSpec) entry; + layerBuiler.setName(fileLayer.getName()); + // layer properties + fileLayer.getProperties().ifPresent(filePropertiesStack::push); + for (CopySpec copySpec : ((FileLayerSpec) entry).getFiles()) { + // copy spec properties + copySpec.getProperties().ifPresent(filePropertiesStack::push); + + // relativize all paths to the buildRoot location + Path rawSrc = copySpec.getSrc(); + Path src = rawSrc.isAbsolute() ? rawSrc : buildRoot.resolve(rawSrc); + AbsoluteUnixPath dest = copySpec.getDest(); + + if (!Files.isDirectory(src) && !Files.isRegularFile(src)) { + throw new UnsupportedOperationException( + "Cannot create FileLayers from non-file, non-directory: " + src.toString()); + } + if (Files.isRegularFile(src)) { // regular file + if (!copySpec.getExcludes().isEmpty() || !copySpec.getIncludes().isEmpty()) { + throw new UnsupportedOperationException( + "Cannot apply includes/excludes on single file copy directives."); + } + layerBuiler.addEntry( + src, + copySpec.isDestEndsWithSlash() ? dest.resolve(src.getFileName()) : dest, + filePropertiesStack.getFilePermissions(), + filePropertiesStack.getModificationTime(), + filePropertiesStack.getOwnership()); + } else if (Files.isDirectory(src)) { // directory + List excludes = + copySpec.getExcludes().stream() + .map(Layers::toPathMatcher) + .collect(Collectors.toList()); + List includes = + copySpec.getIncludes().stream() + .map(Layers::toPathMatcher) + .collect(Collectors.toList()); + try (Stream dirWalk = Files.walk(src)) { + List filtered = + dirWalk + // filter out against excludes + .filter(path -> excludes.stream().noneMatch(exclude -> exclude.matches(path))) + .filter( + path -> { + // if there are no includes directives, include everything + if (includes.isEmpty()) { + return true; + } + // if there are includes directives, only include those specified + for (PathMatcher matcher : includes) { + if (matcher.matches(path)) { + return true; + } + } + return false; + }) + .collect(Collectors.toList()); + + BiFunction newEntry = + (file, permission) -> + new FileEntry( + file, + dest.resolve(src.relativize(file)), + permission, + filePropertiesStack.getModificationTime(), + filePropertiesStack.getOwnership()); + + Set addedDirectories = new HashSet<>(); + for (Path path : filtered) { + if (!Files.isDirectory(path) && !Files.isRegularFile(path)) { + throw new UnsupportedOperationException( + "Cannot create FileLayers from non-file, non-directory: " + src.toString()); + } + + if (Files.isDirectory(path)) { + addedDirectories.add(path); + layerBuiler.addEntry( + newEntry.apply(path, filePropertiesStack.getDirectoryPermissions())); + } else if (Files.isRegularFile(path)) { + if (!path.startsWith(src)) { + // if we end up in a situation where the file added is somehow outside of the + // tree then we do not know how to properly handle it at the moment. It could + // be from a link scenario that we do not understand. + throw new IllegalStateException( + src.toString() + " is not a parent of " + path.toString()); + } + Path parent = Verify.verifyNotNull(path.getParent()); + while (true) { + if (addedDirectories.contains(parent)) { + break; + } + layerBuiler.addEntry( + newEntry.apply(parent, filePropertiesStack.getDirectoryPermissions())); + addedDirectories.add(parent); + if (parent.equals(src)) { + break; + } + parent = Verify.verifyNotNull(parent.getParent()); + } + layerBuiler.addEntry( + newEntry.apply(path, filePropertiesStack.getFilePermissions())); + } + } + } + } + copySpec.getProperties().ifPresent(ignored -> filePropertiesStack.pop()); + } + fileLayer.getProperties().ifPresent(ignored -> filePropertiesStack.pop()); + // TODO: add logging/handling for empty layers + layers.add(layerBuiler.build()); + } else { + throw new UnsupportedOperationException("Only FileLayers are supported at this time."); + } + } + layersSpec.getProperties().ifPresent(ignored -> filePropertiesStack.pop()); + return layers; + } + + @VisibleForTesting + static PathMatcher toPathMatcher(String glob) { + return FileSystems.getDefault() + .getPathMatcher( + "glob:" + ((glob.endsWith("/") || glob.endsWith("\\")) ? glob + "**" : glob)); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpec.java new file mode 100644 index 0000000000..e022102541 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpec.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A yaml block for specifying layers. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * properties: see }{@link FilePropertiesSpec}{@code
+ * entries:
+ *   - see }{@link LayerSpec}{@code
+ *   - see }{@link LayerSpec}{@code
+ * }
+ */ +public class LayersSpec { + private final List entries; + @Nullable private final FilePropertiesSpec properties; + + /** + * Constructor for use by jackson to populate this object. + * + * @param entries a list of {@link LayerSpec} defining the layers in this container + * @param properties a {@link FilePropertiesSpec} that applies to all layers in this buildfile + */ + @JsonCreator + public LayersSpec( + @JsonProperty(value = "entries", required = true) List entries, + @JsonProperty("properties") FilePropertiesSpec properties) { + Validator.checkNotNullAndNotEmpty(entries, "entries"); + this.entries = entries; + this.properties = properties; + } + + public Optional getProperties() { + return Optional.ofNullable(properties); + } + + public List getEntries() { + return entries; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpec.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpec.java new file mode 100644 index 0000000000..0ce973a246 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpec.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A yaml block for specifying platforms. + * + *

Example use of this yaml snippet. + * + *

{@code
+ * architecture: amd64
+ * os: linux
+ * os.version: 1.0.0
+ * os.features:
+ *   - headless
+ * variant: amd64v10
+ * features:
+ *   - sse4
+ *   - aes
+ * }
+ */ +// TODO: reintroduce platform details when ready +// TODO: revert https://github.com/GoogleContainerTools/jib/pull/2763 +public class PlatformSpec { + private final String architecture; + private final String os; + + /** + * Constructor for use by jackson to populate this object. + * + * @param architecture the target cpu architecture + * @param os the target operating system + */ + @JsonCreator + public PlatformSpec( + @JsonProperty(value = "architecture", required = true) String architecture, + @JsonProperty(value = "os", required = true) String os) { + Validator.checkNotNullAndNotEmpty(architecture, "architecture"); + Validator.checkNotNullAndNotEmpty(os, "os"); + this.architecture = architecture; + this.os = os; + } + + public String getArchitecture() { + return architecture; + } + + public String getOs() { + return os; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Validator.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Validator.java new file mode 100644 index 0000000000..f86c2b14a1 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Validator.java @@ -0,0 +1,161 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.common.base.Preconditions; +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Utility helper class to detect errors in parsed yaml values. This class is mostly concerned with + * error message formatting, checking is delegated to guava. + */ +public class Validator { + + private Validator() {} + + /** + * Checks if string is non null and non empty. + * + * @param value the string in question + * @param propertyName the equivalent 'yaml' property name + * @throws NullPointerException if {@code value} is null + * @throws IllegalArgumentException if {@code value} is empty or only whitespace + */ + public static void checkNotNullAndNotEmpty(@Nullable String value, String propertyName) { + Preconditions.checkNotNull(value, "Property '" + propertyName + "' cannot be null"); + Preconditions.checkArgument( + !value.trim().isEmpty(), "Property '" + propertyName + "' cannot be an empty string"); + } + + /** + * Checks if string is null or non empty. + * + * @param value the string in question + * @param propertyName the equivalent 'yaml' property name + * @throws IllegalArgumentException if {@code value} is empty or only whitespace + */ + public static void checkNullOrNotEmpty(@Nullable String value, String propertyName) { + if (value == null) { + // pass + return; + } + Preconditions.checkArgument( + !value.trim().isEmpty(), "Property '" + propertyName + "' cannot be an empty string"); + } + + /** + * Checks if a collection is not null and not empty. + * + * @param value the string in question + * @param propertyName the equivalent 'yaml' property name + * @throws NullPointerException if {@code value} is null + * @throws IllegalArgumentException if {@code value} is empty + */ + public static void checkNotNullAndNotEmpty(@Nullable Collection value, String propertyName) { + Preconditions.checkNotNull(value, "Property '" + propertyName + "' cannot be null"); + Preconditions.checkArgument( + !value.isEmpty(), "Property '" + propertyName + "' cannot be an empty collection"); + } + + /** + * Check if a collection is either null, empty or contains only non-null, non-empty values. + * + * @param values the collection in question + * @param propertyName the equivalent 'yaml' property name + * @throws IllegalArgumentException if {@code values} contains empty entries + * @throws NullPointerException if {@code values} contains null entries + */ + public static void checkNullOrNonNullNonEmptyEntries( + @Nullable Collection values, String propertyName) { + if (values == null) { + // pass + return; + } + for (String value : values) { + Preconditions.checkNotNull( + value, "Property '" + propertyName + "' cannot contain null entries"); + Preconditions.checkArgument( + !value.trim().isEmpty(), "Property '" + propertyName + "' cannot contain empty strings"); + } + } + + /** + * Check if a map is either null, empty or contains only non-null, non-empty keys and values. + * + * @param values the collection in question + * @param propertyName the equivalent 'yaml' property name + * @throws IllegalArgumentException if {@code values} contains empty keys or values + * @throws NullPointerException if {@code values} contains null keys or values + */ + public static void checkNullOrNonNullNonEmptyEntries( + @Nullable Map values, String propertyName) { + if (values == null) { + // pass + return; + } + for (Map.Entry entry : values.entrySet()) { + Preconditions.checkNotNull( + entry.getKey(), "Property '" + propertyName + "' cannot contain null keys"); + Preconditions.checkArgument( + !entry.getKey().trim().isEmpty(), + "Property '" + propertyName + "' cannot contain empty string keys"); + Preconditions.checkNotNull( + entry.getValue(), "Property '" + propertyName + "' cannot contain null values"); + Preconditions.checkArgument( + !entry.getValue().trim().isEmpty(), + "Property '" + propertyName + "' cannot contain empty string values"); + } + } + + /** + * Check if a collection is either null, empty or contains only non-null values. + * + * @param values the collection in question + * @param propertyName the equivalent 'yaml' property name + * @throws NullPointerException if {@code values} contains null entries + */ + public static void checkNullOrNonNullEntries( + @Nullable Collection values, String propertyName) { + if (values == null) { + // pass + return; + } + for (Object value : values) { + Preconditions.checkNotNull( + value, "Property '" + propertyName + "' cannot contain null entries"); + } + } + + /** + * Checks if string is equal to the expected string. + * + * @param value the string in question + * @param propertyName the equivalent 'yaml' property name + * @param expectedValue the value we expect {@code value} to be + * @throws NullPointerException if {@code value} is null + * @throws IllegalArgumentException if {@code value} is not equal to {@code expectedValue} + */ + public static void checkEquals( + @Nullable String value, String propertyName, String expectedValue) { + Preconditions.checkNotNull(value, "Property '" + propertyName + "' cannot be null"); + Preconditions.checkArgument( + value.equals(expectedValue), + "Property '" + propertyName + "' must be '" + expectedValue + "' but is '" + value + "'"); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarFiles.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarFiles.java new file mode 100644 index 0000000000..090dad8a20 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarFiles.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.cli.CommonContainerConfigCliOptions; +import com.google.cloud.tools.jib.cli.ContainerBuilders; +import com.google.cloud.tools.jib.cli.Jar; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** Class to build a container representation from the contents of a jar file. */ +public class JarFiles { + + private JarFiles() {} + + /** + * Generates a {@link JibContainerBuilder} from contents of a jar file. + * + * @param processor jar processor + * @param jarOptions jar cli options + * @param commonCliOptions common cli options + * @param commonContainerConfigCliOptions common command line options shared between jar and war + * command + * @param logger console logger + * @return JibContainerBuilder + * @throws IOException if I/O error occurs when opening the jar file or if temporary directory + * provided doesn't exist + * @throws InvalidImageReferenceException if the base image reference is invalid + */ + public static JibContainerBuilder toJibContainerBuilder( + ArtifactProcessor processor, + Jar jarOptions, + CommonCliOptions commonCliOptions, + CommonContainerConfigCliOptions commonContainerConfigCliOptions, + ConsoleLogger logger) + throws IOException, InvalidImageReferenceException { + String imageReference = + commonContainerConfigCliOptions.getFrom().orElseGet(() -> getDefaultBaseImage(processor)); + JibContainerBuilder containerBuilder = + ContainerBuilders.create(imageReference, Collections.emptySet(), commonCliOptions, logger); + + List layers = processor.createLayers(); + List customEntrypoint = commonContainerConfigCliOptions.getEntrypoint(); + List entrypoint = + customEntrypoint.isEmpty() + ? processor.computeEntrypoint(jarOptions.getJvmFlags()) + : customEntrypoint; + + containerBuilder + .setEntrypoint(entrypoint) + .setFileEntriesLayers(layers) + .setExposedPorts(commonContainerConfigCliOptions.getExposedPorts()) + .setVolumes(commonContainerConfigCliOptions.getVolumes()) + .setEnvironment(commonContainerConfigCliOptions.getEnvironment()) + .setLabels(commonContainerConfigCliOptions.getLabels()) + .setProgramArguments(commonContainerConfigCliOptions.getProgramArguments()); + commonContainerConfigCliOptions.getUser().ifPresent(containerBuilder::setUser); + commonContainerConfigCliOptions.getFormat().ifPresent(containerBuilder::setFormat); + commonContainerConfigCliOptions.getCreationTime().ifPresent(containerBuilder::setCreationTime); + + return containerBuilder; + } + + private static String getDefaultBaseImage(ArtifactProcessor processor) { + if (processor.getJavaVersion() <= 8) { + return "eclipse-temurin:8-jre"; + } + if (processor.getJavaVersion() <= 11) { + return "eclipse-temurin:11-jre"; + } + if (processor.getJavaVersion() <= 17) { + return "eclipse-temurin:17-jre"; + } + if (processor.getJavaVersion() <= 21) { + return "eclipse-temurin:21-jre"; + } + return "eclipse-temurin:25-jre"; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarLayers.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarLayers.java new file mode 100644 index 0000000000..5932328165 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarLayers.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactLayers; +import com.google.common.base.Splitter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +public class JarLayers { + + static final AbsoluteUnixPath APP_ROOT = AbsoluteUnixPath.get("/app"); + static final String JAR = "jar"; + + private JarLayers() {} + + static List getDependenciesLayers(Path jarPath, ProcessingMode mode) + throws IOException { + // Get dependencies from Class-Path in the jar's manifest and add a layer each for non-snapshot + // and snapshot dependencies. If Class-Path is not present in the JAR's manifest then skip + // adding the dependencies layers. + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String classPath = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + if (classPath == null) { + return new ArrayList<>(); + } + List layers = new ArrayList<>(); + Path jarParent = jarPath.getParent() == null ? Paths.get("") : jarPath.getParent(); + Predicate isSnapshot = name -> name.contains("SNAPSHOT"); + List allDependencies = Splitter.onPattern("\\s+").splitToList(classPath.trim()); + List nonSnapshots = + allDependencies.stream() + .filter(isSnapshot.negate()) + .map(Paths::get) + .collect(Collectors.toList()); + List snapshots = + allDependencies.stream().filter(isSnapshot).map(Paths::get).collect(Collectors.toList()); + if (!nonSnapshots.isEmpty()) { + FileEntriesLayer.Builder nonSnapshotLayer = + FileEntriesLayer.builder().setName(ArtifactLayers.DEPENDENCIES); + nonSnapshots.forEach( + path -> + addDependency( + nonSnapshotLayer, + jarParent.resolve(path), + mode.equals(ProcessingMode.packaged) + ? APP_ROOT.resolve(path) + : APP_ROOT + .resolve(ArtifactLayers.DEPENDENCIES) + .resolve(path.getFileName()))); + layers.add(nonSnapshotLayer.build()); + } + if (!snapshots.isEmpty()) { + FileEntriesLayer.Builder snapshotLayer = + FileEntriesLayer.builder().setName(ArtifactLayers.SNAPSHOT_DEPENDENCIES); + snapshots.forEach( + path -> + addDependency( + snapshotLayer, + jarParent.resolve(path), + mode.equals(ProcessingMode.packaged) + ? APP_ROOT.resolve(path) + : APP_ROOT + .resolve(ArtifactLayers.DEPENDENCIES) + .resolve(path.getFileName()))); + layers.add(snapshotLayer.build()); + } + return layers; + } + } + + private static void addDependency( + FileEntriesLayer.Builder layerbuilder, Path fullDepPath, AbsoluteUnixPath pathOnContainer) { + if (!Files.exists(fullDepPath)) { + throw new IllegalArgumentException( + String.format( + "Dependency required by the JAR (as specified in `Class-Path` in the JAR manifest) doesn't exist: %s", + fullDepPath)); + } + layerbuilder.addEntry(fullDepPath, pathOnContainer); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/ProcessingMode.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/ProcessingMode.java new file mode 100644 index 0000000000..6209c224aa --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/ProcessingMode.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +public enum ProcessingMode { + exploded, + packaged +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessor.java new file mode 100644 index 0000000000..75c212be8d --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessor.java @@ -0,0 +1,219 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactLayers; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; +import com.google.common.base.Predicates; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; + +public class SpringBootExplodedProcessor implements ArtifactProcessor { + + private static final String BOOT_INF = "BOOT-INF"; + + private final Path jarPath; + private final Path targetExplodedJarRoot; + private final Integer jarJavaVersion; + + /** + * Constructor for {@link SpringBootExplodedProcessor}. + * + * @param jarPath path to jar file + * @param targetExplodedJarRoot path to exploded-jar root + * @param jarJavaVersion jar java version + */ + public SpringBootExplodedProcessor( + Path jarPath, Path targetExplodedJarRoot, Integer jarJavaVersion) { + this.jarPath = jarPath; + this.targetExplodedJarRoot = targetExplodedJarRoot; + this.jarJavaVersion = jarJavaVersion; + } + + @Override + public List createLayers() throws IOException { + // Clear the exploded-artifact root first + if (Files.exists(targetExplodedJarRoot)) { + MoreFiles.deleteRecursively(targetExplodedJarRoot, RecursiveDeleteOption.ALLOW_INSECURE); + } + + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + ZipUtil.unzip(jarPath, targetExplodedJarRoot, true); + ZipEntry layerIndex = jarFile.getEntry(BOOT_INF + "/layers.idx"); + if (layerIndex != null) { + return createLayersForLayeredSpringBootJar(targetExplodedJarRoot); + } + + Predicate isFile = Files::isRegularFile; + + // Non-snapshot layer + Predicate isInBootInfLib = + path -> path.startsWith(targetExplodedJarRoot.resolve(BOOT_INF).resolve("lib")); + Predicate isSnapshot = path -> path.getFileName().toString().contains("SNAPSHOT"); + Predicate isInBootInfLibAndIsNotSnapshot = isInBootInfLib.and(isSnapshot.negate()); + Predicate nonSnapshotPredicate = isFile.and(isInBootInfLibAndIsNotSnapshot); + FileEntriesLayer nonSnapshotLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.DEPENDENCIES, + targetExplodedJarRoot, + nonSnapshotPredicate, + JarLayers.APP_ROOT); + + // Snapshot layer + Predicate isInBootInfLibAndIsSnapshot = isInBootInfLib.and(isSnapshot); + Predicate snapshotPredicate = isFile.and(isInBootInfLibAndIsSnapshot); + FileEntriesLayer snapshotLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.SNAPSHOT_DEPENDENCIES, + targetExplodedJarRoot, + snapshotPredicate, + JarLayers.APP_ROOT); + + // Spring-boot-loader layer. + Predicate isLoader = path -> path.startsWith(targetExplodedJarRoot.resolve("org")); + Predicate loaderPredicate = isFile.and(isLoader); + FileEntriesLayer loaderLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + "spring-boot-loader", targetExplodedJarRoot, loaderPredicate, JarLayers.APP_ROOT); + + // Classes layer. + Predicate isClass = path -> path.getFileName().toString().endsWith(".class"); + Predicate isInBootInfClasses = + path -> path.startsWith(targetExplodedJarRoot.resolve(BOOT_INF).resolve("classes")); + Predicate classesPredicate = isInBootInfClasses.and(isClass); + FileEntriesLayer classesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.CLASSES, targetExplodedJarRoot, classesPredicate, JarLayers.APP_ROOT); + + // Resources layer. + Predicate isInMetaInf = + path -> path.startsWith(targetExplodedJarRoot.resolve("META-INF")); + Predicate isResource = isInMetaInf.or(isInBootInfClasses.and(isClass.negate())); + Predicate resourcesPredicate = isFile.and(isResource); + FileEntriesLayer resourcesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.RESOURCES, + targetExplodedJarRoot, + resourcesPredicate, + JarLayers.APP_ROOT); + + return Arrays.asList( + nonSnapshotLayer, loaderLayer, snapshotLayer, resourcesLayer, classesLayer); + } + } + + @Override + public ImmutableList computeEntrypoint(List jvmFlags) { + ImmutableList.Builder entrypoint = ImmutableList.builder(); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(JarLayers.APP_ROOT.toString()); + entrypoint.add("org.springframework.boot.loader.JarLauncher"); + return entrypoint.build(); + } + + @Override + public Integer getJavaVersion() { + return jarJavaVersion; + } + + /** + * Creates layers as specified by the layers.idx file (located in the BOOT-INF/ directory of the + * JAR). + * + * @param localExplodedJarRoot Path to exploded JAR content root + * @return list of {@link FileEntriesLayer} + * @throws IOException when an IO error occurs + */ + private static List createLayersForLayeredSpringBootJar( + Path localExplodedJarRoot) throws IOException { + Path layerIndexPath = localExplodedJarRoot.resolve(BOOT_INF).resolve("layers.idx"); + Pattern layerNamePattern = Pattern.compile("- \"(.*)\":"); + Pattern layerEntryPattern = Pattern.compile(" - \"(.*)\""); + Map> layersMap = new LinkedHashMap<>(); + List layerEntries = null; + for (String line : Files.readAllLines(layerIndexPath, StandardCharsets.UTF_8)) { + Matcher layerMatcher = layerNamePattern.matcher(line); + Matcher entryMatcher = layerEntryPattern.matcher(line); + if (layerMatcher.matches()) { + layerEntries = new ArrayList<>(); + String layerName = layerMatcher.group(1); + layersMap.put(layerName, layerEntries); + } else if (entryMatcher.matches()) { + Verify.verifyNotNull(layerEntries).add(entryMatcher.group(1)); + } else { + throw new IllegalStateException( + "Unable to parse BOOT-INF/layers.idx file in the JAR. Please check the format of layers.idx."); + } + } + + // If the layers.idx file looks like this, for example: + // - "dependencies": + // - "BOOT-INF/lib/dependency1.jar" + // - "application": + // - "BOOT-INF/classes/" + // - "META-INF/" + // The predicate for the "dependencies" layer will be true if `path` is equal to + // `BOOT-INF/lib/dependency1.jar` and the predicate for the "spring-boot-loader" layer will be + // true if `path` is in either 'BOOT-INF/classes/` or `META-INF/`. + List layers = new ArrayList<>(); + for (Map.Entry> entry : layersMap.entrySet()) { + String layerName = entry.getKey(); + List contents = entry.getValue(); + if (!contents.isEmpty()) { + Predicate belongsToThisLayer = + isInListedDirectoryOrIsSameFile(contents, localExplodedJarRoot); + layers.add( + ArtifactLayers.getDirectoryContentsAsLayer( + layerName, localExplodedJarRoot, belongsToThisLayer, JarLayers.APP_ROOT)); + } + } + return layers; + } + + private static Predicate isInListedDirectoryOrIsSameFile( + List layerContents, Path localExplodedJarRoot) { + Predicate predicate = Predicates.alwaysFalse(); + for (String pathName : layerContents) { + if (pathName.endsWith("/")) { + predicate = predicate.or(path -> path.startsWith(localExplodedJarRoot.resolve(pathName))); + } else { + predicate = predicate.or(path -> path.equals(localExplodedJarRoot.resolve(pathName))); + } + } + return predicate.and(Files::isRegularFile); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessor.java new file mode 100644 index 0000000000..f529f09ba4 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +public class SpringBootPackagedProcessor implements ArtifactProcessor { + + private final Path jarPath; + private final Integer jarJavaVersion; + + /** + * Constructor for {@link SpringBootPackagedProcessor}. + * + * @param jarPath path to jar file + * @param jarJavaVersion jar java version + */ + public SpringBootPackagedProcessor(Path jarPath, Integer jarJavaVersion) { + this.jarPath = jarPath; + this.jarJavaVersion = jarJavaVersion; + } + + @Override + public List createLayers() { + FileEntriesLayer jarLayer = + FileEntriesLayer.builder() + .setName(JarLayers.JAR) + .addEntry(jarPath, JarLayers.APP_ROOT.resolve(jarPath.getFileName())) + .build(); + return Collections.singletonList(jarLayer); + } + + @Override + public ImmutableList computeEntrypoint(List jvmFlags) { + ImmutableList.Builder entrypoint = ImmutableList.builder(); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-jar"); + entrypoint.add(JarLayers.APP_ROOT + "/" + jarPath.getFileName().toString()); + return entrypoint.build(); + } + + @Override + public Integer getJavaVersion() { + return jarJavaVersion; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessor.java new file mode 100644 index 0000000000..92e4cae471 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactLayers; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; +import com.google.common.collect.ImmutableList; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +public class StandardExplodedProcessor implements ArtifactProcessor { + + private final Path jarPath; + private final Path targetExplodedJarRoot; + private final Integer jarJavaVersion; + + /** + * Constructor for {@link StandardExplodedProcessor}. + * + * @param jarPath path to jar file + * @param targetExplodedJarRoot path to exploded-jar root + * @param jarJavaVersion jar java version + */ + public StandardExplodedProcessor( + Path jarPath, Path targetExplodedJarRoot, Integer jarJavaVersion) { + this.jarPath = jarPath; + this.targetExplodedJarRoot = targetExplodedJarRoot; + this.jarJavaVersion = jarJavaVersion; + } + + @Override + public List createLayers() throws IOException { + // Clear the exploded-artifact root first + if (Files.exists(targetExplodedJarRoot)) { + MoreFiles.deleteRecursively(targetExplodedJarRoot, RecursiveDeleteOption.ALLOW_INSECURE); + } + + // Add dependencies layers. + List layers = + JarLayers.getDependenciesLayers(jarPath, ProcessingMode.exploded); + + // Determine class and resource files in the directory containing jar contents and create + // FileEntriesLayer for each type of layer (classes or resources). + ZipUtil.unzip(jarPath, targetExplodedJarRoot, true); + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + Predicate isResourceFile = isClassFile.negate().and(Files::isRegularFile); + FileEntriesLayer classesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.CLASSES, + targetExplodedJarRoot, + isClassFile, + JarLayers.APP_ROOT.resolve("explodedJar")); + FileEntriesLayer resourcesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.RESOURCES, + targetExplodedJarRoot, + isResourceFile, + JarLayers.APP_ROOT.resolve("explodedJar")); + + if (!resourcesLayer.getEntries().isEmpty()) { + layers.add(resourcesLayer); + } + if (!classesLayer.getEntries().isEmpty()) { + layers.add(classesLayer); + } + return layers; + } + + @Override + public ImmutableList computeEntrypoint(List jvmFlags) throws IOException { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String mainClass = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + if (mainClass == null) { + throw new IllegalArgumentException( + "`Main-Class:` attribute for an application main class not defined in the input JAR's " + + "manifest (`META-INF/MANIFEST.MF` in the JAR)."); + } + String classpath = + JarLayers.APP_ROOT + "/explodedJar:" + JarLayers.APP_ROOT + "/dependencies/*"; + ImmutableList.Builder entrypoint = ImmutableList.builder(); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(classpath); + entrypoint.add(mainClass); + return entrypoint.build(); + } + } + + @Override + public Integer getJavaVersion() { + return jarJavaVersion; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessor.java new file mode 100644 index 0000000000..11854325a7 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +public class StandardPackagedProcessor implements ArtifactProcessor { + + private final Path jarPath; + private final Integer jarJavaVersion; + + /** + * Constructor for {@link StandardPackagedProcessor}. + * + * @param jarPath path to jar file + * @param jarJavaVersion jar java version + */ + public StandardPackagedProcessor(Path jarPath, Integer jarJavaVersion) { + this.jarPath = jarPath; + this.jarJavaVersion = jarJavaVersion; + } + + @Override + public List createLayers() throws IOException { + // Add dependencies layers. + List layers = + JarLayers.getDependenciesLayers(jarPath, ProcessingMode.packaged); + + // Add layer for jar. + FileEntriesLayer jarLayer = + FileEntriesLayer.builder() + .setName(JarLayers.JAR) + .addEntry(jarPath, JarLayers.APP_ROOT.resolve(jarPath.getFileName())) + .build(); + layers.add(jarLayer); + + return layers; + } + + @Override + public ImmutableList computeEntrypoint(List jvmFlags) throws IOException { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String mainClass = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + if (mainClass == null) { + throw new IllegalArgumentException( + "`Main-Class:` attribute for an application main class not defined in the input JAR's " + + "manifest (`META-INF/MANIFEST.MF` in the JAR)."); + } + ImmutableList.Builder entrypoint = ImmutableList.builder(); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-jar"); + entrypoint.add(JarLayers.APP_ROOT + "/" + jarPath.getFileName().toString()); + return entrypoint.build(); + } + } + + @Override + public Integer getJavaVersion() { + return jarJavaVersion; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/CliLogger.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/CliLogger.java new file mode 100644 index 0000000000..ea4f013467 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/CliLogger.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.logging; + +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import com.google.common.annotations.VisibleForTesting; +import java.io.PrintWriter; + +/** A simple CLI logger that logs to the command line based on the configured log level. */ +public class CliLogger { + + private CliLogger() {} + + /** + * Create a new logger for the CLI. + * + * @param verbosity the configure verbosity + * @param httpTraceLevel the log level for http trace + * @param consoleOutput the configured consoleOutput format + * @param stdout the writer to store stdout + * @param stderr the writer to store stderr + * @param executor a {@link SingleThreadedExecutor} to ensure that all messages are logged in a + * sequential, deterministic order + * @return a new ConsoleLogger instance + */ + public static ConsoleLogger newLogger( + Verbosity verbosity, + HttpTraceLevel httpTraceLevel, + ConsoleOutput consoleOutput, + PrintWriter stdout, + PrintWriter stderr, + SingleThreadedExecutor executor) { + boolean enableRichProgress = + isRichConsole(consoleOutput, httpTraceLevel) && verbosity.atLeast(Verbosity.lifecycle); + ConsoleLoggerBuilder builder = + enableRichProgress + ? ConsoleLoggerBuilder.rich(executor, false) + : ConsoleLoggerBuilder.plain(executor); + if (verbosity.atLeast(Verbosity.error)) { + builder.error(message -> stderr.println("[ERROR] " + message)); + } + if (verbosity.atLeast(Verbosity.warn)) { + builder.warn(message -> stdout.println("[WARN] " + message)); + } + if (verbosity.atLeast(Verbosity.lifecycle)) { + builder.lifecycle(stdout::println); + // Rich progress reporting will be through ProgressEvent (note this is not LogEvent of + // Level.PROGRESS), so we ignore PROGRESS LogEvent. + if (!enableRichProgress) { + builder.progress(stdout::println); + } + } + if (verbosity.atLeast(Verbosity.info)) { + builder.info(stdout::println); + } + if (verbosity.atLeast(Verbosity.debug)) { + builder.debug(stdout::println); + } + + return builder.build(); + } + + @VisibleForTesting + static boolean isRichConsole(ConsoleOutput consoleOutput, HttpTraceLevel httpTraceLevel) { + if (httpTraceLevel != HttpTraceLevel.off) { + return false; + } + + switch (consoleOutput) { + case plain: + return false; + case auto: + // Enables progress footer when ANSI is supported (Windows or TERM not 'dumb'). + return System.getProperty("os.name").startsWith("windows") + || (System.console() != null && !"dumb".equals(System.getenv("TERM"))); + case rich: + default: + return true; + } + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/ConsoleOutput.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/ConsoleOutput.java new file mode 100644 index 0000000000..e9e9e45001 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/ConsoleOutput.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.logging; + +public enum ConsoleOutput { + auto, + rich, + plain +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/HttpTraceLevel.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/HttpTraceLevel.java new file mode 100644 index 0000000000..9f3c0c0a1a --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/HttpTraceLevel.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.logging; + +import java.util.logging.Level; + +public enum HttpTraceLevel { + off("OFF"), + config("CONFIG"), + all("ALL"); + + private final String value; + + private HttpTraceLevel(String value) { + this.value = value; + } + + public Level toJulLevel() { + return Level.parse(value); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/Verbosity.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/Verbosity.java new file mode 100644 index 0000000000..d8b2d79e98 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/Verbosity.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.logging; + +public enum Verbosity { + quiet(0), + error(1), + warn(2), + lifecycle(3), + info(4), + debug(5); + + private final int value; + + private Verbosity(int value) { + this.value = value; + } + + public int value() { + return value; + } + + public boolean atLeast(Verbosity target) { + return value >= target.value; + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessor.java new file mode 100644 index 0000000000..9256357174 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessor.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.war; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.cli.ArtifactLayers; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; +import com.google.common.collect.ImmutableList; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public class StandardWarExplodedProcessor implements ArtifactProcessor { + + private final Path warPath; + private final Path targetExplodedWarRoot; + private final AbsoluteUnixPath appRoot; + + /** + * Constructor for {@link StandardWarExplodedProcessor}. + * + * @param warPath path to WAR file + * @param targetExplodedWarRoot path to exploded-war root + * @param appRoot the absolute path of the app on the container + */ + public StandardWarExplodedProcessor( + Path warPath, Path targetExplodedWarRoot, AbsoluteUnixPath appRoot) { + this.warPath = warPath; + this.targetExplodedWarRoot = targetExplodedWarRoot; + this.appRoot = appRoot; + } + + @Override + public List createLayers() throws IOException { + // Clear the exploded-artifact root first + if (Files.exists(targetExplodedWarRoot)) { + MoreFiles.deleteRecursively(targetExplodedWarRoot, RecursiveDeleteOption.ALLOW_INSECURE); + } + + ZipUtil.unzip(warPath, targetExplodedWarRoot, true); + Predicate isFile = Files::isRegularFile; + Predicate isInWebInfLib = + path -> path.startsWith(targetExplodedWarRoot.resolve("WEB-INF").resolve("lib")); + Predicate isSnapshot = path -> path.getFileName().toString().contains("SNAPSHOT"); + + // Non-snapshot layer + Predicate isInWebInfLibAndIsNotSnapshot = isInWebInfLib.and(isSnapshot.negate()); + FileEntriesLayer nonSnapshotLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.DEPENDENCIES, + targetExplodedWarRoot, + isFile.and(isInWebInfLibAndIsNotSnapshot), + appRoot); + + // Snapshot layer + Predicate isInWebInfLibAndIsSnapshot = isInWebInfLib.and(isSnapshot); + FileEntriesLayer snapshotLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.SNAPSHOT_DEPENDENCIES, + targetExplodedWarRoot, + isFile.and(isInWebInfLibAndIsSnapshot), + appRoot); + + // Classes layer. + Predicate isClass = path -> path.getFileName().toString().endsWith(".class"); + Predicate isInWebInfClasses = + path -> path.startsWith(targetExplodedWarRoot.resolve("WEB-INF").resolve("classes")); + Predicate classesPredicate = isInWebInfClasses.and(isClass); + FileEntriesLayer classesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.CLASSES, targetExplodedWarRoot, classesPredicate, appRoot); + + // Resources layer. + Predicate resourcesPredicate = isInWebInfLib.or(isClass).negate(); + FileEntriesLayer resourcesLayer = + ArtifactLayers.getDirectoryContentsAsLayer( + ArtifactLayers.RESOURCES, + targetExplodedWarRoot, + isFile.and(resourcesPredicate), + appRoot); + + ArrayList layers = new ArrayList<>(); + if (!nonSnapshotLayer.getEntries().isEmpty()) { + layers.add(nonSnapshotLayer); + } + if (!snapshotLayer.getEntries().isEmpty()) { + layers.add(snapshotLayer); + } + if (!resourcesLayer.getEntries().isEmpty()) { + layers.add(resourcesLayer); + } + if (!classesLayer.getEntries().isEmpty()) { + layers.add(classesLayer); + } + + return layers; + } + + @Override + public ImmutableList computeEntrypoint(List jvmFlags) { + throw new UnsupportedOperationException("Computing the entrypoint is currently not supported."); + } + + @Override + public Integer getJavaVersion() { + throw new UnsupportedOperationException( + "Getting the java version from a WAR file is currently not supported."); + } +} diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/WarFiles.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/WarFiles.java new file mode 100644 index 0000000000..ee7830dfd2 --- /dev/null +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/WarFiles.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.war; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.cli.CommonContainerConfigCliOptions; +import com.google.cloud.tools.jib.cli.ContainerBuilders; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +public class WarFiles { + + private WarFiles() {} + + /** + * Generates a {@link JibContainerBuilder} from contents of a WAR file. + * + * @param processor artifact processor + * @param commonCliOptions common cli options + * @param commonContainerConfigCliOptions common cli options shared between jar and war command + * @param logger console logger + * @return JibContainerBuilder + * @throws IOException if I/O error occurs when opening the war file or if temporary directory + * provided doesn't exist + * @throws InvalidImageReferenceException if the base image reference is invalid + */ + public static JibContainerBuilder toJibContainerBuilder( + ArtifactProcessor processor, + CommonCliOptions commonCliOptions, + CommonContainerConfigCliOptions commonContainerConfigCliOptions, + ConsoleLogger logger) + throws IOException, InvalidImageReferenceException { + String baseImage = commonContainerConfigCliOptions.getFrom().orElse("jetty"); + JibContainerBuilder containerBuilder = + ContainerBuilders.create(baseImage, Collections.emptySet(), commonCliOptions, logger); + List programArguments = commonContainerConfigCliOptions.getProgramArguments(); + if (!commonContainerConfigCliOptions.getProgramArguments().isEmpty()) { + containerBuilder.setProgramArguments(programArguments); + } + containerBuilder + .setEntrypoint(computeEntrypoint(commonContainerConfigCliOptions)) + .setFileEntriesLayers(processor.createLayers()) + .setExposedPorts(commonContainerConfigCliOptions.getExposedPorts()) + .setVolumes(commonContainerConfigCliOptions.getVolumes()) + .setEnvironment(commonContainerConfigCliOptions.getEnvironment()) + .setLabels(commonContainerConfigCliOptions.getLabels()); + commonContainerConfigCliOptions.getUser().ifPresent(containerBuilder::setUser); + commonContainerConfigCliOptions.getFormat().ifPresent(containerBuilder::setFormat); + commonContainerConfigCliOptions.getCreationTime().ifPresent(containerBuilder::setCreationTime); + + return containerBuilder; + } + + @Nullable + private static List computeEntrypoint( + CommonContainerConfigCliOptions commonContainerConfigCliOptions) + throws InvalidImageReferenceException { + List entrypoint = commonContainerConfigCliOptions.getEntrypoint(); + if (!entrypoint.isEmpty()) { + return entrypoint; + } + if (commonContainerConfigCliOptions.isJettyBaseimage()) { + // Since we are using Jetty 12 or later as the default, the deploy module needs to be + // specified. See + // https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html + return ImmutableList.of("java", "-jar", "/usr/local/jetty/start.jar", "--module=ee10-deploy"); + } + return null; + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/ArtifactProcessorsTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/ArtifactProcessorsTest.java new file mode 100644 index 0000000000..edf8293f7c --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/ArtifactProcessorsTest.java @@ -0,0 +1,235 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.cli.ArtifactProcessor; +import com.google.cloud.tools.jib.cli.ArtifactProcessors; +import com.google.cloud.tools.jib.cli.CacheDirectories; +import com.google.cloud.tools.jib.cli.CommonContainerConfigCliOptions; +import com.google.cloud.tools.jib.cli.Jar; +import com.google.cloud.tools.jib.cli.War; +import com.google.cloud.tools.jib.cli.jar.ProcessingMode; +import com.google.cloud.tools.jib.cli.jar.SpringBootExplodedProcessor; +import com.google.cloud.tools.jib.cli.jar.SpringBootPackagedProcessor; +import com.google.cloud.tools.jib.cli.jar.StandardExplodedProcessor; +import com.google.cloud.tools.jib.cli.jar.StandardPackagedProcessor; +import com.google.cloud.tools.jib.cli.war.StandardWarExplodedProcessor; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ArtifactProcessors}. */ +@RunWith(MockitoJUnitRunner.class) +public class ArtifactProcessorsTest { + + private static final String SPRING_BOOT = "jar/spring-boot/springboot_sample.jar"; + private static final String STANDARD = "jar/standard/emptyStandardJar.jar"; + private static final String STANDARD_WITH_INVALID_CLASS = "jar/standard/jarWithInvalidClass.jar"; + private static final String STANDARD_WITH_EMPTY_CLASS_FILE = + "jar/standard/standardJarWithOnlyClasses.jar"; + private static final String JAVA_18_JAR = "jar/java18.jar"; + + @Mock private CacheDirectories mockCacheDirectories; + @Mock private Jar mockJarCommand; + @Mock private War mockWarCommand; + @Mock private CommonContainerConfigCliOptions mockCommonContainerConfigCliOptions; + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testFromJar_standardExploded() throws IOException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(STANDARD).toURI()); + Path explodedJarRoot = temporaryFolder.getRoot().toPath(); + when(mockCacheDirectories.getExplodedArtifactDirectory()).thenReturn(explodedJarRoot); + when(mockJarCommand.getMode()).thenReturn(ProcessingMode.exploded); + + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarPath, mockCacheDirectories, mockJarCommand, mockCommonContainerConfigCliOptions); + + verify(mockCacheDirectories).getExplodedArtifactDirectory(); + assertThat(processor).isInstanceOf(StandardExplodedProcessor.class); + } + + @Test + public void testFromJar_standardPackaged() throws IOException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(STANDARD).toURI()); + when(mockJarCommand.getMode()).thenReturn(ProcessingMode.packaged); + + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarPath, mockCacheDirectories, mockJarCommand, mockCommonContainerConfigCliOptions); + + verifyNoInteractions(mockCacheDirectories); + assertThat(processor).isInstanceOf(StandardPackagedProcessor.class); + } + + @Test + public void testFromJar_springBootPackaged() throws IOException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(SPRING_BOOT).toURI()); + when(mockJarCommand.getMode()).thenReturn(ProcessingMode.packaged); + + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarPath, mockCacheDirectories, mockJarCommand, mockCommonContainerConfigCliOptions); + + verifyNoInteractions(mockCacheDirectories); + assertThat(processor).isInstanceOf(SpringBootPackagedProcessor.class); + } + + @Test + public void testFromJar_springBootExploded() throws IOException, URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(SPRING_BOOT).toURI()); + Path explodedJarRoot = temporaryFolder.getRoot().toPath(); + when(mockCacheDirectories.getExplodedArtifactDirectory()).thenReturn(explodedJarRoot); + when(mockJarCommand.getMode()).thenReturn(ProcessingMode.exploded); + + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarPath, mockCacheDirectories, mockJarCommand, mockCommonContainerConfigCliOptions); + + verify(mockCacheDirectories).getExplodedArtifactDirectory(); + assertThat(processor).isInstanceOf(SpringBootExplodedProcessor.class); + } + + @Test + public void testFromJar_incompatibleDefaultBaseImage() throws URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(JAVA_18_JAR).toURI()); + + IllegalStateException exception = + assertThrows( + IllegalStateException.class, + () -> + ArtifactProcessors.fromJar( + jarPath, + mockCacheDirectories, + mockJarCommand, + mockCommonContainerConfigCliOptions)); + + assertThat(exception) + .hasMessageThat() + .startsWith("The input JAR (" + jarPath + ") is compiled with Java 18"); + } + + @Test + public void testFromJar_incompatibleDefaultBaseImage_baseImageSpecified() + throws URISyntaxException, IOException { + Path jarPath = Paths.get(Resources.getResource(JAVA_18_JAR).toURI()); + when(mockJarCommand.getMode()).thenReturn(ProcessingMode.exploded); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.of("base-image")); + + ArtifactProcessor processor = + ArtifactProcessors.fromJar( + jarPath, mockCacheDirectories, mockJarCommand, mockCommonContainerConfigCliOptions); + + verify(mockCacheDirectories).getExplodedArtifactDirectory(); + assertThat(processor).isInstanceOf(StandardExplodedProcessor.class); + } + + @Test + public void testDetermineJavaMajorVersion_versionNotFound() + throws URISyntaxException, IOException { + Path jarPath = Paths.get(Resources.getResource(STANDARD).toURI()); + Integer version = ArtifactProcessors.determineJavaMajorVersion(jarPath); + assertThat(version).isEqualTo(0); + } + + @Test + public void testDetermineJavaMajorVersion_invalidClassFile() throws URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(STANDARD_WITH_INVALID_CLASS).toURI()); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> ArtifactProcessors.determineJavaMajorVersion(jarPath)); + assertThat(exception) + .hasMessageThat() + .isEqualTo("The class file (class1.class) is of an invalid format."); + } + + @Test + public void testDetermineJavaMajorVersion_emptyClassFile() throws URISyntaxException { + Path jarPath = Paths.get(Resources.getResource(STANDARD_WITH_EMPTY_CLASS_FILE).toURI()); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> ArtifactProcessors.determineJavaMajorVersion(jarPath)); + assertThat(exception).hasMessageThat().startsWith("Reached end of class file (class1.class)"); + } + + @Test + public void testFromWar_noJettyBaseImageAndNoAppRoot() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ArtifactProcessors.fromWar( + Paths.get("my-app.war"), + mockCacheDirectories, + mockWarCommand, + mockCommonContainerConfigCliOptions)); + + assertThat(exception) + .hasMessageThat() + .startsWith("Please set the app root of the container with `--app-root`"); + } + + @Test + public void testFromWar_noJettyBaseImageAndAppRootPresent_success() + throws InvalidImageReferenceException { + when(mockWarCommand.getAppRoot()).thenReturn(Optional.of(AbsoluteUnixPath.get("/app-root"))); + when(mockCacheDirectories.getExplodedArtifactDirectory()) + .thenReturn(Paths.get("exploded-artifact")); + ArtifactProcessor processor = + ArtifactProcessors.fromWar( + Paths.get("my-app.war"), + mockCacheDirectories, + mockWarCommand, + mockCommonContainerConfigCliOptions); + + assertThat(processor).isInstanceOf(StandardWarExplodedProcessor.class); + } + + @Test + public void testFromWar_jettyBaseImageSpecified_success() throws InvalidImageReferenceException { + when(mockCommonContainerConfigCliOptions.isJettyBaseimage()).thenReturn(true); + + ArtifactProcessor processor = + ArtifactProcessors.fromWar( + Paths.get("my-app.war"), + mockCacheDirectories, + mockWarCommand, + mockCommonContainerConfigCliOptions); + assertThat(processor).isInstanceOf(StandardWarExplodedProcessor.class); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTestProxy.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTestProxy.java new file mode 100644 index 0000000000..abb0e23187 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTestProxy.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import java.nio.file.Path; +import java.util.Set; + +/** + * A simple test proxy to access the innards of Containerizer which are otherwise package private. + */ +public class ContainerizerTestProxy { + + private final Containerizer containerizer; + + public ContainerizerTestProxy(Containerizer containerizer) { + this.containerizer = containerizer; + } + + public boolean getAllowInsecureRegistries() { + return containerizer.getAllowInsecureRegistries(); + } + + public boolean isOfflineMode() { + return containerizer.isOfflineMode(); + } + + public String getToolName() { + return containerizer.getToolName(); + } + + public String getToolVersion() { + return containerizer.getToolVersion(); + } + + public boolean getAlwaysCacheBaseImage() { + return containerizer.getAlwaysCacheBaseImage(); + } + + public String getDescription() { + return containerizer.getDescription(); + } + + public ImageConfiguration getImageConfiguration() { + return containerizer.getImageConfiguration(); + } + + public Path getBaseImageLayersCacheDirectory() { + return containerizer.getBaseImageLayersCacheDirectory(); + } + + public Path getApplicationsLayersCacheDirectory() throws CacheDirectoryCreationException { + return containerizer.getApplicationLayersCacheDirectory(); + } + + public Set getAdditionalTags() { + return containerizer.getAdditionalTags(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/api/HttpRequestTester.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/HttpRequestTester.java new file mode 100644 index 0000000000..8a8856a21a --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/HttpRequestTester.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.blob.Blobs; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import javax.annotation.Nullable; +import org.junit.Assert; + +/** Test helpers for making HTTP requests. */ +public class HttpRequestTester { + + /** + * Verifies the response body. Repeatedly tries {@code url} at the interval of .5 seconds for up + * to 20 seconds until getting OK HTTP response code. + */ + public static void verifyBody(String expectedBody, URL url) throws InterruptedException { + Assert.assertEquals(expectedBody, getContent(url)); + } + + /** Fetches the host to use for the http request. */ + public static String fetchDockerHostForHttpRequest() { + if (System.getenv("KOKORO_JOB_CLUSTER") != null + && System.getenv("KOKORO_JOB_CLUSTER").equals("MACOS_EXTERNAL")) { + return System.getenv("DOCKER_IP"); + } else if (System.getenv("KOKORO_JOB_CLUSTER") != null + && System.getenv("KOKORO_JOB_CLUSTER").equals("GCP_UBUNTU_DOCKER")) { + return System.getenv("DOCKER_IP_UBUNTU"); + } else { + return "localhost"; + } + } + + @Nullable + private static String getContent(URL url) throws InterruptedException { + for (int i = 0; i < 40; i++) { + Thread.sleep(500); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream in = connection.getInputStream()) { + return Blobs.writeToString(Blobs.from(in)); + } + } + } catch (IOException ignored) { + // ignored + } + } + return null; + } + + private HttpRequestTester() {} +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java new file mode 100644 index 0000000000..6534a32a88 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.BuildContext; + +/** Test helper to expose package-private members of {@link JibContainerBuilder}. */ +public class JibContainerBuilderTestHelper { + + public static BuildContext toBuildContext( + JibContainerBuilder jibContainerBuilder, Containerizer containerizer) + throws CacheDirectoryCreationException { + return jibContainerBuilder.toBuildContext(containerizer); + } + + private JibContainerBuilderTestHelper() {} +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java new file mode 100644 index 0000000000..5eba093f58 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java @@ -0,0 +1,429 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.cli.logging.HttpTraceLevel; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.nio.file.Paths; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import picocli.CommandLine; +import picocli.CommandLine.MissingParameterException; + +@RunWith(JUnitParamsRunner.class) +public class BuildTest { + @Test + public void testParse_missingRequiredParams() { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, () -> CommandLine.populateCommand(new Build(), "")); + assertThat(mpe.getMessage()).isEqualTo("Missing required option: '--target='"); + } + + @Test + public void testParse_defaults() { + Build buildCommand = CommandLine.populateCommand(new Build(), "-t", "test-image-ref"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(buildCommand.buildFileUnprocessed).isNull(); + assertThat(buildCommand.getBuildFile()).isEqualTo(Paths.get("./jib.yaml")); + assertThat(buildCommand.contextRoot).isEqualTo(Paths.get(".")); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(buildCommand.getTemplateParameters()).isEmpty(); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + } + + @Test + public void testParse_shortFormParams() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "-t=test-image-ref", + "-c=test-context", + "-b=test-build-file", + "-p=param1=value1", + "-p=param2=value2"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(buildCommand.buildFileUnprocessed).isEqualTo(Paths.get("test-build-file")); + assertThat(buildCommand.getBuildFile()).isEqualTo(Paths.get("test-build-file")); + assertThat(buildCommand.contextRoot).isEqualTo(Paths.get("test-context")); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(buildCommand.getTemplateParameters()) + .isEqualTo(ImmutableMap.of("param1", "value1", "param2", "value2")); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + } + + @Test + public void testParse_longFormParams() { + // this test does not check credential helpers, scroll down for specialized credential helper + // tests + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--context=test-context", + "--build-file=test-build-file", + "--parameter=param1=value1", + "--parameter=param2=value2", + "--additional-tags=tag1,tag2,tag3", + "--allow-insecure-registries", + "--send-credentials-over-http", + "--project-cache=test-project-cache", + "--base-image-cache=test-base-image-cache", + "--verbosity=info", + "--stacktrace", + "--http-trace", + "--serialize", + "--image-metadata-out=path/to/json/jib-image.json"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(buildCommand.buildFileUnprocessed).isEqualTo(Paths.get("test-build-file")); + assertThat(buildCommand.getBuildFile()).isEqualTo(Paths.get("test-build-file")); + assertThat(buildCommand.contextRoot).isEqualTo(Paths.get("test-context")); + assertThat(commonCliOptions.getAdditionalTags()) + .isEqualTo(ImmutableList.of("tag1", "tag2", "tag3")); + assertThat(buildCommand.getTemplateParameters()) + .isEqualTo(ImmutableMap.of("param1", "value1", "param2", "value2")); + assertThat(commonCliOptions.getProjectCache()).hasValue(Paths.get("test-project-cache")); + assertThat(commonCliOptions.getBaseImageCache()).hasValue(Paths.get("test-base-image-cache")); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isTrue(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isTrue(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.info); + assertThat(commonCliOptions.isStacktrace()).isTrue(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.config); + assertThat(commonCliOptions.isSerialize()).isTrue(); + assertThat(commonCliOptions.getImageJsonPath()) + .hasValue(Paths.get("path/to/json/jib-image.json")); + } + + @Test + public void testParse_buildFileDefaultForContext() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), "--target", "test-image-ref", "--context", "test-context"); + assertThat(buildCommand.buildFileUnprocessed).isNull(); + assertThat(buildCommand.getBuildFile()).isEqualTo(Paths.get("test-context/jib.yaml")); + assertThat(buildCommand.contextRoot).isEqualTo(Paths.get("test-context")); + } + + @Test + public void testParse_credentialHelper() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), "--target=test-image-ref", "--credential-helper=test-cred-helper"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelper() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), "--target=test-image-ref", "--to-credential-helper=test-cred-helper"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromCredentialHelper() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), "--target=test-image-ref", "--from-credential-helper=test-cred-helper"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_usernamePassword() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--username=test-username", + "--password=test-password"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePassword() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromUsernamePassword() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--from-username=test-username", + "--from-password=test-password"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + @Test + public void testParse_toAndFromUsernamePassword() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--to-username=test-username-1", + "--to-password=test-password-1", + "--from-username=test-username-2", + "--from-password=test-password-2"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username-1", "test-password-1")); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username-2", "test-password-2")); + } + + @Test + public void testParse_toAndFromCredentialHelper() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--to-credential-helper=to-test-helper", + "--from-credential-helper=from-test-helper"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("to-test-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("from-test-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePasswordAndFromCredentialHelper() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password", + "--from-credential-helper=test-cred-helper"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelperAndFromUsernamePassword() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), + "--target=test-image-ref", + "--to-credential-helper=test-cred-helper", + "--from-username=test-username", + "--from-password=test-password"); + CommonCliOptions commonCliOptions = buildCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + private Object usernamePasswordPairs() { + return new Object[][] { + {"--username", "--password"}, + {"--to-username", "--to-password"}, + {"--from-username", "--from-password"} + }; + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_usernameWithoutPassword(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new Build(), "--target", "test-image-ref", usernameField, "test-username")); + assertThat(mpe.getMessage()).isEqualTo("Error: Missing required argument(s): " + passwordField); + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_passwordWithoutUsername(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new Build(), "--target", "test-image-ref", passwordField, "test-password")); + assertThat(mpe.getMessage()) + .isEqualTo("Error: Missing required argument(s): " + usernameField + "="); + } + + public String[][] incompatibleCredentialOptions() { + return new String[][] { + {"--credential-helper=x", "--to-credential-helper=x"}, + {"--credential-helper=x", "--from-credential-helper=x"}, + {"--credential-helper=x", "--username=x", "--password=x"}, + {"--credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--credential-helper=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--from-username=x", "--from-password=x"}, + {"--username=x", "--password=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--to-credential-helper=x"}, + {"--username=x", "--password=x", "--from-credential-helper=x"}, + {"--from-credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--to-credential-helper=x", "--to-password=x", "--to-username=x"}, + }; + } + + @Test + @Parameters(method = "incompatibleCredentialOptions") + public void testParse_incompatibleCredentialOptions(String[] authArgs) { + CommandLine.MutuallyExclusiveArgsException meae = + assertThrows( + CommandLine.MutuallyExclusiveArgsException.class, + () -> + CommandLine.populateCommand( + new Build(), ArrayUtils.add(authArgs, "--target=ignored"))); + assertThat(meae) + .hasMessageThat() + .containsMatch("^Error: (\\[)*(--(from-|to-)?credential-helper|\\[--(username|password))"); + } + + @Test + public void testValidate_nameMissingFail() { + Build buildCommand = CommandLine.populateCommand(new Build(), "--target=tar://sometar.tar"); + CommandLine.ParameterException pex = + assertThrows(CommandLine.ParameterException.class, buildCommand.commonCliOptions::validate); + assertThat(pex.getMessage()) + .isEqualTo("Missing option: --name must be specified when using --target=tar://...."); + } + + @Test + public void testValidate_pass() { + Build buildCommand = + CommandLine.populateCommand( + new Build(), "--target=tar://sometar.tar", "--name=test.io/test/test"); + buildCommand.commonCliOptions.validate(); + // pass + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CacheDirectoriesTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CacheDirectoriesTest.java new file mode 100644 index 0000000000..4a158da989 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CacheDirectoriesTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import picocli.CommandLine; + +public class CacheDirectoriesTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testCacheDirectories_defaults() throws IOException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), "-t", "ignored"); + Path buildContext = temporaryFolder.newFolder("some-context").toPath(); + CacheDirectories cacheDirectories = CacheDirectories.from(commonCliOptions, buildContext); + + Path expectedProjectCache = + Paths.get(System.getProperty("java.io.tmpdir")) + .resolve("jib-cli-cache") + .resolve("projects") + .resolve(CacheDirectories.getProjectCacheDirectoryFromProject(buildContext)); + assertThat(cacheDirectories.getBaseImageCache()).isEmpty(); + assertThat(cacheDirectories.getProjectCache()).isEqualTo(expectedProjectCache); + assertThat(cacheDirectories.getApplicationLayersCache()) + .isEqualTo(expectedProjectCache.resolve("application-layers")); + assertThat(cacheDirectories.getExplodedArtifactDirectory()) + .isEqualTo(expectedProjectCache.resolve("exploded-artifact")); + } + + @Test + public void testCacheDirectories_configuredValuesIgnoresBuildContext() throws IOException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand( + new CommonCliOptions(), + "-t=ignored", + "--base-image-cache=test-base-image-cache", + "--project-cache=test-project-cache"); + Path ignoredContext = temporaryFolder.newFolder("ignored").toPath(); + CacheDirectories cacheDirectories = CacheDirectories.from(commonCliOptions, ignoredContext); + + assertThat(cacheDirectories.getBaseImageCache()).hasValue(Paths.get("test-base-image-cache")); + assertThat(cacheDirectories.getProjectCache()).isEqualTo(Paths.get("test-project-cache")); + assertThat(cacheDirectories.getApplicationLayersCache()) + .isEqualTo(Paths.get("test-project-cache").resolve("application-layers")); + assertThat(cacheDirectories.getExplodedArtifactDirectory()) + .isEqualTo(Paths.get("test-project-cache").resolve("exploded-artifact")); + } + + @Test + public void testCacheDirectories_failIfContextIsNotDirectory() throws IOException { + Path badContext = temporaryFolder.newFile().toPath(); + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), "-t", "ignored"); + + IllegalArgumentException exception = + Assert.assertThrows( + IllegalArgumentException.class, + () -> CacheDirectories.from(commonCliOptions, badContext)); + assertThat(exception) + .hasMessageThat() + .isEqualTo("contextRoot must be a directory, but " + badContext.toString() + " is not."); + } + + @Test + public void testGetProjectCacheDirectoryFromProject_sameFileDifferentPaths() throws IOException { + temporaryFolder.newFolder("ignored"); + Path path = temporaryFolder.getRoot().toPath(); + Path indirectPath = temporaryFolder.getRoot().toPath().resolve("ignored").resolve(".."); + + assertThat(path).isNotEqualTo(indirectPath); // the general equality should not hold true + assertThat(Files.isSameFile(path, indirectPath)).isTrue(); // path equality holds + assertThat(CacheDirectories.getProjectCacheDirectoryFromProject(path)) + .isEqualTo( + CacheDirectories.getProjectCacheDirectoryFromProject( + indirectPath)); // our hash should hold + } + + @Test + public void testGetProjectCacheDirectoryFromProject_different() { + assertThat(CacheDirectories.getProjectCacheDirectoryFromProject(Paths.get("1"))) + .isNotEqualTo(CacheDirectories.getProjectCacheDirectoryFromProject(Paths.get("2"))); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerBuildersTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerBuildersTest.java new file mode 100644 index 0000000000..98f9358eea --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerBuildersTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ContainerBuilders}. */ +@RunWith(MockitoJUnitRunner.class) +public class ContainerBuildersTest { + + @Mock private CommonCliOptions mockCommonCliOptions; + + @Mock private ConsoleLogger mockLogger; + + @Test + public void testCreate_dockerBaseImage() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + JibContainerBuilder containerBuilder = + ContainerBuilders.create( + "docker://docker-image-ref", Collections.emptySet(), mockCommonCliOptions, mockLogger); + BuildContext buildContext = + JibContainerBuilderTestHelper.toBuildContext( + containerBuilder, Containerizer.to(RegistryImage.named("ignored"))); + ImageConfiguration imageConfiguration = buildContext.getBaseImageConfiguration(); + + assertThat(imageConfiguration.getImage().toString()).isEqualTo("docker-image-ref"); + assertThat(imageConfiguration.getDockerClient().isPresent()).isTrue(); + assertThat(imageConfiguration.getTarPath().isPresent()).isFalse(); + } + + @Test + public void testCreate_registry() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + JibContainerBuilder containerBuilder = + ContainerBuilders.create( + "registry://registry-image-ref", + Collections.emptySet(), + mockCommonCliOptions, + mockLogger); + BuildContext buildContext = + JibContainerBuilderTestHelper.toBuildContext( + containerBuilder, Containerizer.to(RegistryImage.named("ignored"))); + ImageConfiguration imageConfiguration = buildContext.getBaseImageConfiguration(); + + assertThat(imageConfiguration.getImage().toString()).isEqualTo("registry-image-ref"); + assertThat(imageConfiguration.getDockerClient().isPresent()).isFalse(); + assertThat(imageConfiguration.getTarPath().isPresent()).isFalse(); + } + + @Test + public void testCreate_tarBase() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + JibContainerBuilder containerBuilder = + ContainerBuilders.create( + "tar:///path/to.tar", Collections.emptySet(), mockCommonCliOptions, mockLogger); + BuildContext buildContext = + JibContainerBuilderTestHelper.toBuildContext( + containerBuilder, Containerizer.to(RegistryImage.named("ignored"))); + ImageConfiguration imageConfiguration = buildContext.getBaseImageConfiguration(); + + assertThat(imageConfiguration.getTarPath()).isEqualTo(Optional.of(Paths.get("/path/to.tar"))); + assertThat(imageConfiguration.getDockerClient().isPresent()).isFalse(); + } + + @Test + public void testCreate_platforms() throws IOException, InvalidImageReferenceException { + JibContainerBuilder containerBuilder = + ContainerBuilders.create( + "registry://registry-image-ref", + ImmutableSet.of(new Platform("arch1", "os1"), new Platform("arch2", "os2")), + mockCommonCliOptions, + mockLogger); + + assertThat(containerBuilder.toContainerBuildPlan().getPlatforms()) + .isEqualTo(ImmutableSet.of(new Platform("arch1", "os1"), new Platform("arch2", "os2"))); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerizersTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerizersTest.java new file mode 100644 index 0000000000..250aff2ece --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerizersTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ContainerizerTestProxy; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableSet; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import picocli.CommandLine; + +public class ContainerizersTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + // Containerizers will add system properties based on cli properties + @Rule + public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties(); + + @Mock private ConsoleLogger consoleLogger; + @Mock private CacheDirectories cacheDirectories; + + private static final Path baseImageCache = Paths.get("base-image-cache-for-test"); + private static final Path applicationCache = Paths.get("application-cache-for-test"); + + @Before + public void initCaches() { + Mockito.when(cacheDirectories.getBaseImageCache()).thenReturn(Optional.of(baseImageCache)); + Mockito.when(cacheDirectories.getApplicationLayersCache()).thenReturn(applicationCache); + } + + @Test + public void testApplyConfiguration_defaults() + throws InvalidImageReferenceException, FileNotFoundException, + CacheDirectoryCreationException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), "-t", "test-image-ref"); + ContainerizerTestProxy containerizer = + new ContainerizerTestProxy( + Containerizers.from(commonCliOptions, consoleLogger, cacheDirectories)); + + assertThat(Boolean.getBoolean(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP)).isFalse(); + assertThat(Boolean.getBoolean(JibSystemProperties.SERIALIZE)).isFalse(); + assertThat(containerizer.getToolName()).isEqualTo(VersionInfo.TOOL_NAME); + assertThat(containerizer.getToolVersion()).isEqualTo(VersionInfo.getVersionSimple()); + assertThat(Boolean.getBoolean("sendCredentialsOverHttp")).isFalse(); + assertThat(containerizer.getAllowInsecureRegistries()).isFalse(); + assertThat(containerizer.getBaseImageLayersCacheDirectory()).isEqualTo(baseImageCache); + assertThat(containerizer.getApplicationsLayersCacheDirectory()).isEqualTo(applicationCache); + assertThat(containerizer.getAdditionalTags()).isEqualTo(ImmutableSet.of()); + } + + @Test + public void testApplyConfiguration_withValues() + throws InvalidImageReferenceException, CacheDirectoryCreationException, + FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand( + new CommonCliOptions(), + "-t=test-image-ref", + "--send-credentials-over-http", + "--allow-insecure-registries", + "--additional-tags=tag1,tag2", + "--serialize"); + ContainerizerTestProxy containerizer = + new ContainerizerTestProxy( + Containerizers.from(commonCliOptions, consoleLogger, cacheDirectories)); + + assertThat(Boolean.getBoolean(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP)).isTrue(); + assertThat(Boolean.getBoolean(JibSystemProperties.SERIALIZE)).isTrue(); + assertThat(containerizer.getAllowInsecureRegistries()).isTrue(); + assertThat(containerizer.getBaseImageLayersCacheDirectory()).isEqualTo(baseImageCache); + assertThat(containerizer.getApplicationsLayersCacheDirectory()).isEqualTo(applicationCache); + assertThat(containerizer.getAdditionalTags()).isEqualTo(ImmutableSet.of("tag1", "tag2")); + } + + @Test + public void testFrom_dockerDaemonImage() + throws InvalidImageReferenceException, FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand( + new CommonCliOptions(), "-t", "docker://gcr.io/test/test-image-ref"); + ContainerizerTestProxy containerizer = + new ContainerizerTestProxy( + Containerizers.from(commonCliOptions, consoleLogger, cacheDirectories)); + + assertThat(containerizer.getDescription()).isEqualTo("Building image to Docker daemon"); + ImageConfiguration config = containerizer.getImageConfiguration(); + + assertThat(config.getCredentialRetrievers()).isEmpty(); + assertThat(config.getDockerClient()).isEmpty(); + assertThat(config.getImage().toString()).isEqualTo("gcr.io/test/test-image-ref"); + assertThat(config.getTarPath()).isEmpty(); + } + + @Test + public void testFrom_tarImage() throws InvalidImageReferenceException, IOException { + Path tarPath = temporaryFolder.getRoot().toPath().resolve("test-tar.tar"); + CommonCliOptions commonCliOptions = + CommandLine.populateCommand( + new CommonCliOptions(), + "-t=tar://" + tarPath.toAbsolutePath(), + "--name=gcr.io/test/test-image-ref"); + ContainerizerTestProxy containerizer = + new ContainerizerTestProxy( + Containerizers.from(commonCliOptions, consoleLogger, cacheDirectories)); + + assertThat(containerizer.getDescription()).isEqualTo("Building image tarball"); + ImageConfiguration config = containerizer.getImageConfiguration(); + + assertThat(config.getCredentialRetrievers()).isEmpty(); + assertThat(config.getDockerClient()).isEmpty(); + assertThat(config.getImage().toString()).isEqualTo("gcr.io/test/test-image-ref"); + assertThat(config.getTarPath()).isEmpty(); // weird, but the way jib currently works + } + + @Test + public void testFrom_registryImage() throws InvalidImageReferenceException, IOException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand( + new CommonCliOptions(), "-t", "registry://gcr.io/test/test-image-ref"); + ContainerizerTestProxy containerizer = + new ContainerizerTestProxy( + Containerizers.from(commonCliOptions, consoleLogger, cacheDirectories)); + + // description from Containerizer.java + assertThat(containerizer.getDescription()).isEqualTo("Building and pushing image"); + ImageConfiguration config = containerizer.getImageConfiguration(); + + assertThat(config.getCredentialRetrievers()).isNotEmpty(); + assertThat(config.getDockerClient()).isEmpty(); + assertThat(config.getImage().toString()).isEqualTo("gcr.io/test/test-image-ref"); + assertThat(config.getTarPath()).isEmpty(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CredentialsTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CredentialsTest.java new file mode 100644 index 0000000000..7ba6130aee --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CredentialsTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import java.io.FileNotFoundException; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import picocli.CommandLine; + +@RunWith(JUnitParamsRunner.class) +public class CredentialsTest { + + private static final String[] DEFAULT_ARGS = {"--target=ignored"}; + @Rule public final MockitoRule mockitoJUnit = MockitoJUnit.rule(); + @Mock private DefaultCredentialRetrievers defaultCredentialRetrievers; + + private String[][] paramsToNone() { + return new String[][] { + {"--from-credential-helper=ignored"}, {"--from-username=ignored", "--from-password=ignored"}, + }; + } + + @Test + @Parameters(method = "paramsToNone") + public void testGetToCredentialRetriever_none(String[] args) throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getToCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } + + private String[][] paramsFromNone() { + return new String[][] { + {"--to-credential-helper=ignored"}, {"--to-username=ignored", "--to-password=ignored"}, + }; + } + + @Test + @Parameters(method = "paramsFromNone") + public void testGetFromCredentialRetriever_none(String[] args) throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getFromCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } + + private String[][] paramsToCredHelper() { + return new String[][] { + {"--credential-helper=abc"}, + {"--to-credential-helper=abc"}, + {"--to-credential-helper=abc", "--from-credential-helper=ignored"}, + {"--to-credential-helper=abc", "--from-username=ignored", "--from-password=ignored"}, + }; + } + + @Test + @Parameters(method = "paramsToCredHelper") + public void testGetToCredentialRetriever_credHelper(String[] args) throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getToCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + verify(defaultCredentialRetrievers).setCredentialHelper("abc"); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } + + private String[][] paramsFromCredHelper() { + return new String[][] { + {"--credential-helper=abc"}, + {"--from-credential-helper=abc"}, + {"--from-credential-helper=abc", "--to-credential-helper=ignored"}, + {"--from-credential-helper=abc", "--to-username=ignored", "--to-password=ignored"}, + }; + } + + @Test + @Parameters(method = "paramsFromCredHelper") + public void testGetFromCredentialHelper(String[] args) throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getFromCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + verify(defaultCredentialRetrievers).setCredentialHelper("abc"); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } + + public Object paramsToUsernamePassword() { + return new Object[][] { + {"--username/--password", new String[] {"--username=abc", "--password=xyz"}}, + {"--to-username/--to-password", new String[] {"--to-username=abc", "--to-password=xyz"}}, + { + "--to-username/--to-password", + new String[] { + "--to-username=abc", + "--to-password=xyz", + "--from-username=ignored", + "--from-password=ignored" + } + }, + { + "--to-username/--to-password", + new String[] {"--to-username=abc", "--to-password=xyz", "--from-credential-helper=ignored"} + } + }; + } + + @Test + @Parameters(method = "paramsToUsernamePassword") + public void testGetToUsernamePassword(String expectedSource, String[] args) + throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getToCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + ArgumentCaptor captor = ArgumentCaptor.forClass(Credential.class); + verify(defaultCredentialRetrievers) + .setKnownCredential(captor.capture(), ArgumentMatchers.eq(expectedSource)); + assertThat(captor.getValue()).isEqualTo(Credential.from("abc", "xyz")); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } + + public Object paramsFromUsernamePassword() { + return new Object[][] { + {"--username/--password", new String[] {"--username=abc", "--password=xyz"}}, + { + "--from-username/--from-password", + new String[] {"--from-username=abc", "--from-password=xyz"} + }, + { + "--from-username/--from-password", + new String[] { + "--from-username=abc", + "--from-password=xyz", + "--to-username=ignored", + "--to-password=ignored" + } + }, + { + "--from-username/--from-password", + new String[] { + "--from-username=abc", "--from-password=xyz", "--to-credential-helper=ignored" + } + }, + }; + } + + @Test + @Parameters(method = "paramsFromUsernamePassword") + public void testGetFromUsernamePassword(String expectedSource, String[] args) + throws FileNotFoundException { + CommonCliOptions commonCliOptions = + CommandLine.populateCommand(new CommonCliOptions(), ArrayUtils.addAll(DEFAULT_ARGS, args)); + Credentials.getFromCredentialRetrievers(commonCliOptions, defaultCredentialRetrievers); + ArgumentCaptor captor = ArgumentCaptor.forClass(Credential.class); + verify(defaultCredentialRetrievers) + .setKnownCredential(captor.capture(), ArgumentMatchers.eq(expectedSource)); + assertThat(captor.getValue()).isEqualTo(Credential.from("abc", "xyz")); + verify(defaultCredentialRetrievers).asList(); + verifyNoMoreInteractions(defaultCredentialRetrievers); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/InstantsTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/InstantsTest.java new file mode 100644 index 0000000000..ff822be7a7 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/InstantsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Instants}. */ +public class InstantsTest { + + @Test + public void testFromMillisOrIso8601_millis() { + Instant parsed = Instants.fromMillisOrIso8601("100", "ignored"); + Assert.assertEquals(Instant.ofEpochMilli(100), parsed); + } + + @Test + public void testFromMillisOrIso8601_iso8601() { + Instant parsed = Instants.fromMillisOrIso8601("2020-06-08T14:54:36+00:00", "ignored"); + Assert.assertEquals(Instant.parse("2020-06-08T14:54:36Z"), parsed); + } + + @Test + public void testFromMillisOrIso8601_failed() { + try { + Instants.fromMillisOrIso8601("bad-time", "testFieldName"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals( + "testFieldName must be a number of milliseconds since epoch or an ISO 8601 formatted date", + iae.getMessage()); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java new file mode 100644 index 0000000000..528bdd6f9a --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java @@ -0,0 +1,632 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Ports; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.cli.jar.ProcessingMode; +import com.google.cloud.tools.jib.cli.logging.HttpTraceLevel; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; +import java.time.Instant; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import picocli.CommandLine; +import picocli.CommandLine.MissingParameterException; + +@RunWith(JUnitParamsRunner.class) +public class JarTest { + + @Test + public void testParse_missingRequiredParams_targetImage() { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> CommandLine.populateCommand(new Jar(), "my-app.jar")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Missing required option: '--target='"); + } + + @Test + public void testParse_missingRequiredParams_jarfile() { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> CommandLine.populateCommand(new Jar(), "--target=test-image-ref")); + assertThat(mpe).hasMessageThat().isEqualTo("Missing required parameter: ''"); + } + + @Test + public void testParse_defaults() { + Jar jarCommand = CommandLine.populateCommand(new Jar(), "-t", "test-image-ref", "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + CommonContainerConfigCliOptions commonContainerConfigCliOptions = + jarCommand.commonContainerConfigCliOptions; + + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getFrom()).isEmpty(); + assertThat(jarCommand.getJvmFlags()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getExposedPorts()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getVolumes()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getEnvironment()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getLabels()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getUser()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getFormat()).hasValue(ImageFormat.Docker); + assertThat(commonContainerConfigCliOptions.getProgramArguments()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getEntrypoint()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getCreationTime()).isEmpty(); + assertThat(jarCommand.getMode()).isEqualTo(ProcessingMode.exploded); + } + + @Test + public void testParse_shortFormParams() { + Jar jarCommand = CommandLine.populateCommand(new Jar(), "-t=test-image-ref", "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + } + + @Test + public void testParse_longFormParams() { + // this test does not check credential helpers, scroll down for specialized credential helper + // tests + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--additional-tags=tag1,tag2,tag3", + "--allow-insecure-registries", + "--send-credentials-over-http", + "--project-cache=test-project-cache", + "--base-image-cache=test-base-image-cache", + "--verbosity=info", + "--stacktrace", + "--http-trace", + "--serialize", + "--image-metadata-out=path/to/json/jib-image.json", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()) + .isEqualTo(ImmutableList.of("tag1", "tag2", "tag3")); + assertThat(commonCliOptions.getProjectCache()).hasValue(Paths.get("test-project-cache")); + assertThat(commonCliOptions.getBaseImageCache()).hasValue(Paths.get("test-base-image-cache")); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isTrue(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isTrue(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.info); + assertThat(commonCliOptions.isStacktrace()).isTrue(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.config); + assertThat(commonCliOptions.isSerialize()).isTrue(); + assertThat(commonCliOptions.getImageJsonPath()) + .hasValue(Paths.get("path/to/json/jib-image.json")); + } + + @Test + public void testParse_credentialHelper() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--credential-helper=test-cred-helper", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelper() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-credential-helper=test-cred-helper", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromCredentialHelper() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--from-credential-helper=test-cred-helper", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_usernamePassword() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--username=test-username", + "--password=test-password", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePassword() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromUsernamePassword() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--from-username=test-username", + "--from-password=test-password", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + @Test + public void testParse_toAndFromUsernamePassword() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-username=test-username-1", + "--to-password=test-password-1", + "--from-username=test-username-2", + "--from-password=test-password-2", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username-1", "test-password-1")); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username-2", "test-password-2")); + } + + @Test + public void testParse_toAndFromCredentialHelper() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-credential-helper=to-test-helper", + "--from-credential-helper=from-test-helper", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("to-test-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("from-test-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePasswordAndFromCredentialHelper() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password", + "--from-credential-helper=test-cred-helper", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelperAndFromUsernamePassword() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--to-credential-helper=test-cred-helper", + "--from-username=test-username", + "--from-password=test-password", + "my-app.jar"); + CommonCliOptions commonCliOptions = jarCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + private Object usernamePasswordPairs() { + return new Object[][] { + {"--username", "--password"}, + {"--to-username", "--to-password"}, + {"--from-username", "--from-password"} + }; + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_usernameWithoutPassword(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + usernameField, + "test-username", + "my-app.jar")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Error: Missing required argument(s): " + passwordField); + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_passwordWithoutUsername(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + passwordField, + "test-password", + "my-app.jar")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Error: Missing required argument(s): " + usernameField + "="); + } + + public String[][] incompatibleCredentialOptions() { + return new String[][] { + {"--credential-helper=x", "--to-credential-helper=x"}, + {"--credential-helper=x", "--from-credential-helper=x"}, + {"--credential-helper=x", "--username=x", "--password=x"}, + {"--credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--credential-helper=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--from-username=x", "--from-password=x"}, + {"--username=x", "--password=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--to-credential-helper=x"}, + {"--username=x", "--password=x", "--from-credential-helper=x"}, + {"--from-credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--to-credential-helper=x", "--to-password=x", "--to-username=x"}, + }; + } + + @Test + @Parameters(method = "incompatibleCredentialOptions") + public void testParse_incompatibleCredentialOptions(String[] authArgs) { + CommandLine.MutuallyExclusiveArgsException meae = + assertThrows( + CommandLine.MutuallyExclusiveArgsException.class, + () -> + CommandLine.populateCommand( + new Jar(), ArrayUtils.addAll(authArgs, "--target=ignored", "my-app.jar"))); + assertThat(meae) + .hasMessageThat() + .containsMatch("^Error: (\\[)*(--(from-|to-)?credential-helper|\\[--(username|password))"); + } + + @Test + public void testParse_from() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--from=base-image-ref", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getFrom()).hasValue("base-image-ref"); + } + + @Test + public void testParse_jvmFlags() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--jvm-flags=jvm-flag1,jvm-flag2", "my-app.jar"); + assertThat(jarCommand.getJvmFlags()).isEqualTo(ImmutableList.of("jvm-flag1", "jvm-flag2")); + } + + @Test + public void testParse_exposedPorts() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--expose=8080,3306", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getExposedPorts()) + .isEqualTo(Ports.parse(ImmutableList.of("8080", "3306"))); + } + + @Test + public void testParse_volumes() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--volumes=/volume1,/volume2", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getVolumes()) + .isEqualTo( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + } + + @Test + public void testParse_environment() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--environment-variables=ENV_VAR1=value1,ENV_VAR2=value2", + "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getEnvironment()) + .isEqualTo(ImmutableMap.of("ENV_VAR1", "value1", "ENV_VAR2", "value2")); + } + + @Test + public void testParse_labels() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--labels=label1=value2,label2=value2", + "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getLabels()) + .isEqualTo(ImmutableMap.of("label1", "value2", "label2", "value2")); + } + + @Test + public void testParse_user() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--user=customUser", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getUser()).hasValue("customUser"); + } + + @Test + public void testParse_imageFormat() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--image-format=OCI", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getFormat()).hasValue(ImageFormat.OCI); + } + + @Test + public void testParse_invalidImageFormat() { + CommandLine.ParameterException exception = + assertThrows( + CommandLine.ParameterException.class, + () -> + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--image-format=unknown", "my-app.jar")); + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "Invalid value for option '--image-format': expected one of [Docker, OCI] (case-sensitive) but was 'unknown'"); + } + + @Test + public void testParse_programArguments() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--program-args=arg1,arg2", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getProgramArguments()) + .isEqualTo(ImmutableList.of("arg1", "arg2")); + } + + @Test + public void testParse_entrypoint() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--entrypoint=java -cp myClass", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getEntrypoint()) + .isEqualTo(ImmutableList.of("java", "-cp", "myClass")); + } + + @Test + public void testParse_creationTime_milliseconds() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--creation-time=23", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getCreationTime()) + .hasValue(Instant.ofEpochMilli(23)); + } + + @Test + public void testParse_creationTime_iso8601() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), + "--target=test-image-ref", + "--creation-time=2011-12-03T22:42:05Z", + "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.getCreationTime()) + .hasValue(Instant.parse("2011-12-03T22:42:05Z")); + } + + @Test + public void testParse_mode() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--mode=packaged", "my-app.jar"); + assertThat(jarCommand.getMode()).isEqualTo(ProcessingMode.packaged); + } + + @Test + public void testParse_invalidMode() { + CommandLine.ParameterException exception = + assertThrows( + CommandLine.ParameterException.class, + () -> + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--mode=unknown", "my-app.jar")); + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "Invalid value for option '--mode': expected one of [exploded, packaged] (case-sensitive) but was 'unknown'"); + } + + @Test + public void testValidate_nameMissingFail() { + Jar jarCommand = + CommandLine.populateCommand(new Jar(), "--target=tar://sometar.tar", "my-app.jar"); + CommandLine.ParameterException pex = + assertThrows(CommandLine.ParameterException.class, jarCommand.commonCliOptions::validate); + assertThat(pex) + .hasMessageThat() + .isEqualTo("Missing option: --name must be specified when using --target=tar://...."); + } + + @Test + public void testValidate_pass() { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=tar://sometar.tar", "--name=test.io/test/test", "my-app.jar"); + jarCommand.commonCliOptions.validate(); + // pass + } + + @Test + public void testIsJetty_noCustomBaseImage() throws InvalidImageReferenceException { + Jar jarCommand = + CommandLine.populateCommand(new Jar(), "--target=test-image-ref", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isTrue(); + } + + @Test + public void testIsJetty_nonJetty() throws InvalidImageReferenceException { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--from=base-image", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isFalse(); + } + + @Test + public void testIsJetty_customJetty() throws InvalidImageReferenceException { + Jar jarCommand = + CommandLine.populateCommand( + new Jar(), "--target=test-image-ref", "--from=jetty:tag", "my-app.jar"); + assertThat(jarCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isTrue(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java new file mode 100644 index 0000000000..72c78a0614 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.plugins.common.ImageMetadataOutput; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.Futures; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class JibCliTest { + + @Mock private GlobalConfig globalConfig; + @Mock private ConsoleLogger logger; + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private JibContainer mockJibContainer; + + @Test + public void testConfigureHttpLogging() { + Logger logger = JibCli.configureHttpLogging(Level.ALL); + assertThat(logger.getName()).isEqualTo("com.google.api.client.http.HttpTransport"); + assertThat(logger.getLevel()).isEqualTo(Level.ALL); + + assertThat(logger.getHandlers()).hasLength(1); + Handler handler = logger.getHandlers()[0]; + assertThat(handler).isInstanceOf(ConsoleHandler.class); + assertThat(handler.getLevel()).isEqualTo(Level.ALL); + } + + @Test + public void testLogTerminatingException() { + JibCli.logTerminatingException(logger, new IOException("test error message"), false); + + verify(logger) + .log(LogEvent.Level.ERROR, "\u001B[31;1mjava.io.IOException: test error message\u001B[0m"); + verifyNoMoreInteractions(logger); + } + + @Test + public void testLogTerminatingException_stackTrace() { + JibCli.logTerminatingException(logger, new IOException("test error message"), true); + + String stackTraceLine = + "at com.google.cloud.tools.jib.cli.JibCliTest.testLogTerminatingException_stackTrace"; + verify(logger).log(eq(LogEvent.Level.ERROR), contains(stackTraceLine)); + verify(logger) + .log(LogEvent.Level.ERROR, "\u001B[31;1mjava.io.IOException: test error message\u001B[0m"); + verifyNoMoreInteractions(logger); + } + + @Test + public void testNewUpdateChecker_noUpdateCheck() throws ExecutionException, InterruptedException { + when(globalConfig.isDisableUpdateCheck()).thenReturn(true); + Future> updateChecker = + JibCli.newUpdateChecker(globalConfig, Verbosity.info, ignored -> {}); + assertThat(updateChecker.get()).isEmpty(); + } + + @Test + public void testFinishUpdateChecker_correctMessageLogged() { + Future> updateCheckFuture = Futures.immediateFuture(Optional.of("2.0.0")); + JibCli.finishUpdateChecker(logger, updateCheckFuture); + verify(logger) + .log( + eq(LogEvent.Level.LIFECYCLE), + contains( + "A new version of Jib CLI (2.0.0) is available (currently using " + + VersionInfo.getVersionSimple() + + "). Download the latest Jib CLI version from " + + ProjectInfo.GITHUB_URL + + "/releases/tag/v2.0.0-cli")); + } + + @Test + public void testWriteImageJson() + throws InvalidImageReferenceException, IOException, DigestException { + String imageId = "sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9"; + String digest = "sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6fc"; + when(mockJibContainer.getTargetImage()) + .thenReturn(ImageReference.parse("eclipse-temurin:8-jre")); + when(mockJibContainer.getImageId()).thenReturn(DescriptorDigest.fromDigest(imageId)); + when(mockJibContainer.getDigest()).thenReturn(DescriptorDigest.fromDigest(digest)); + when(mockJibContainer.getTags()).thenReturn(ImmutableSet.of("latest", "tag-2")); + + Path outputPath = temporaryFolder.getRoot().toPath().resolve("jib-image.json"); + JibCli.writeImageJson(Optional.of(outputPath), mockJibContainer); + + String outputJson = new String(Files.readAllBytes(outputPath), StandardCharsets.UTF_8); + ImageMetadataOutput metadataOutput = + JsonTemplateMapper.readJson(outputJson, ImageMetadataOutput.class); + assertThat(metadataOutput.getImage()).isEqualTo("eclipse-temurin:8-jre"); + assertThat(metadataOutput.getImageId()).isEqualTo(imageId); + assertThat(metadataOutput.getImageDigest()).isEqualTo(digest); + assertThat(metadataOutput.getTags()).containsExactly("latest", "tag-2"); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/WarTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/WarTest.java new file mode 100644 index 0000000000..9bdc950c02 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/WarTest.java @@ -0,0 +1,608 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Ports; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.cli.logging.HttpTraceLevel; +import com.google.cloud.tools.jib.cli.logging.Verbosity; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; +import java.time.Instant; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import picocli.CommandLine; +import picocli.CommandLine.MissingParameterException; + +@RunWith(JUnitParamsRunner.class) +public class WarTest { + + @Test + public void testParse_missingRequiredParams_targetImage() { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> CommandLine.populateCommand(new War(), "my-app.war")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Missing required option: '--target='"); + } + + @Test + public void testParse_missingRequiredParams_warfile() { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> CommandLine.populateCommand(new War(), "--target=test-image-ref")); + assertThat(mpe).hasMessageThat().isEqualTo("Missing required parameter: ''"); + } + + @Test + public void testParse_defaults() { + War warCommand = CommandLine.populateCommand(new War(), "-t", "test-image-ref", "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + CommonContainerConfigCliOptions commonContainerConfigCliOptions = + warCommand.commonContainerConfigCliOptions; + + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getFrom()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getExposedPorts()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getVolumes()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getEnvironment()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getLabels()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getUser()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getFormat()).hasValue(ImageFormat.Docker); + assertThat(commonContainerConfigCliOptions.getProgramArguments()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getEntrypoint()).isEmpty(); + assertThat(commonContainerConfigCliOptions.getCreationTime()).isEmpty(); + assertThat(warCommand.getAppRoot()).isEmpty(); + } + + @Test + public void testParse_shortFormParams() { + War warCommand = CommandLine.populateCommand(new War(), "-t=test-image-ref", "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()).isEmpty(); + assertThat(commonCliOptions.getProjectCache()).isEmpty(); + assertThat(commonCliOptions.getBaseImageCache()).isEmpty(); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isFalse(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isFalse(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.lifecycle); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.isStacktrace()).isFalse(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.off); + assertThat(commonCliOptions.isSerialize()).isFalse(); + assertThat(commonCliOptions.getImageJsonPath()).isEmpty(); + } + + @Test + public void testParse_longFormParams() { + // this test does not check credential helpers, scroll down for specialized credential helper + // tests + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--additional-tags=tag1,tag2,tag3", + "--allow-insecure-registries", + "--send-credentials-over-http", + "--project-cache=test-project-cache", + "--base-image-cache=test-base-image-cache", + "--verbosity=info", + "--stacktrace", + "--http-trace", + "--serialize", + "--image-metadata-out=path/to/json/jib-image.json", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + assertThat(commonCliOptions.getTargetImage()).isEqualTo("test-image-ref"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getAdditionalTags()) + .isEqualTo(ImmutableList.of("tag1", "tag2", "tag3")); + assertThat(commonCliOptions.getProjectCache()).hasValue(Paths.get("test-project-cache")); + assertThat(commonCliOptions.getBaseImageCache()).hasValue(Paths.get("test-base-image-cache")); + assertThat(commonCliOptions.isAllowInsecureRegistries()).isTrue(); + assertThat(commonCliOptions.isSendCredentialsOverHttp()).isTrue(); + assertThat(commonCliOptions.getVerbosity()).isEqualTo(Verbosity.info); + assertThat(commonCliOptions.isStacktrace()).isTrue(); + assertThat(commonCliOptions.getHttpTrace()).isEqualTo(HttpTraceLevel.config); + assertThat(commonCliOptions.isSerialize()).isTrue(); + assertThat(commonCliOptions.getImageJsonPath()) + .hasValue(Paths.get("path/to/json/jib-image.json")); + } + + @Test + public void testParse_credentialHelper() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--credential-helper=test-cred-helper", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelper() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-credential-helper=test-cred-helper", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromCredentialHelper() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--from-credential-helper=test-cred-helper", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_usernamePassword() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--username=test-username", + "--password=test-password", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePassword() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_fromUsernamePassword() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--from-username=test-username", + "--from-password=test-password", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + @Test + public void testParse_toAndFromUsernamePassword() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-username=test-username-1", + "--to-password=test-password-1", + "--from-username=test-username-2", + "--from-password=test-password-2", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username-1", "test-password-1")); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username-2", "test-password-2")); + } + + @Test + public void testParse_toAndFromCredentialHelper() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-credential-helper=to-test-helper", + "--from-credential-helper=from-test-helper", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("to-test-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("from-test-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toUsernamePasswordAndFromCredentialHelper() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-username=test-username", + "--to-password=test-password", + "--from-credential-helper=test-cred-helper", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getFromCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + assertThat(commonCliOptions.getFromUsernamePassword()).isEmpty(); + } + + @Test + public void testParse_toCredentialHelperAndFromUsernamePassword() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--to-credential-helper=test-cred-helper", + "--from-username=test-username", + "--from-password=test-password", + "my-app.war"); + CommonCliOptions commonCliOptions = warCommand.commonCliOptions; + + assertThat(commonCliOptions.getCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getToCredentialHelper()).hasValue("test-cred-helper"); + assertThat(commonCliOptions.getFromCredentialHelper()).isEmpty(); + assertThat(commonCliOptions.getUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getToUsernamePassword()).isEmpty(); + assertThat(commonCliOptions.getFromUsernamePassword()) + .hasValue(Credential.from("test-username", "test-password")); + } + + private Object usernamePasswordPairs() { + return new Object[][] { + {"--username", "--password"}, + {"--to-username", "--to-password"}, + {"--from-username", "--from-password"} + }; + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_usernameWithoutPassword(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + usernameField, + "test-username", + "my-app.war")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Error: Missing required argument(s): " + passwordField); + } + + @Test + @Parameters(method = "usernamePasswordPairs") + public void testParse_passwordWithoutUsername(String usernameField, String passwordField) { + MissingParameterException mpe = + assertThrows( + MissingParameterException.class, + () -> + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + passwordField, + "test-password", + "my-app.war")); + assertThat(mpe) + .hasMessageThat() + .isEqualTo("Error: Missing required argument(s): " + usernameField + "="); + } + + public String[][] incompatibleCredentialOptions() { + return new String[][] { + {"--credential-helper=x", "--to-credential-helper=x"}, + {"--credential-helper=x", "--from-credential-helper=x"}, + {"--credential-helper=x", "--username=x", "--password=x"}, + {"--credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--credential-helper=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--from-username=x", "--from-password=x"}, + {"--username=x", "--password=x", "--to-username=x", "--to-password=x"}, + {"--username=x", "--password=x", "--to-credential-helper=x"}, + {"--username=x", "--password=x", "--from-credential-helper=x"}, + {"--from-credential-helper=x", "--from-username=x", "--from-password=x"}, + {"--to-credential-helper=x", "--to-password=x", "--to-username=x"}, + }; + } + + @Test + @Parameters(method = "incompatibleCredentialOptions") + public void testParse_incompatibleCredentialOptions(String[] authArgs) { + CommandLine.MutuallyExclusiveArgsException meae = + assertThrows( + CommandLine.MutuallyExclusiveArgsException.class, + () -> + CommandLine.populateCommand( + new War(), ArrayUtils.addAll(authArgs, "--target=ignored", "my-app.war"))); + assertThat(meae) + .hasMessageThat() + .containsMatch("^Error: (\\[)*(--(from-|to-)?credential-helper|\\[--(username|password))"); + } + + @Test + public void testParse_from() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--from=base-image-ref", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getFrom()).hasValue("base-image-ref"); + } + + @Test + public void testParse_appRoot() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--app-root=/path/to/app", "my-app.war"); + assertThat(warCommand.getAppRoot()).hasValue(AbsoluteUnixPath.get("/path/to/app")); + } + + @Test + public void testParse_exposedPorts() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--expose=8080,3306", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getExposedPorts()) + .isEqualTo(Ports.parse(ImmutableList.of("8080", "3306"))); + } + + @Test + public void testParse_volumes() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--volumes=/volume1,/volume2", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getVolumes()) + .isEqualTo( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + } + + @Test + public void testParse_environment() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--environment-variables=ENV_VAR1=value1,ENV_VAR2=value2", + "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getEnvironment()) + .isEqualTo(ImmutableMap.of("ENV_VAR1", "value1", "ENV_VAR2", "value2")); + } + + @Test + public void testParse_labels() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--labels=label1=value2,label2=value2", + "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getLabels()) + .isEqualTo(ImmutableMap.of("label1", "value2", "label2", "value2")); + } + + @Test + public void testParse_user() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--user=customUser", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getUser()).hasValue("customUser"); + } + + @Test + public void testParse_imageFormat() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--image-format=OCI", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getFormat()).hasValue(ImageFormat.OCI); + } + + @Test + public void testParse_invalidImageFormat() { + CommandLine.ParameterException exception = + assertThrows( + CommandLine.ParameterException.class, + () -> + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--image-format=unknown", "my-app.war")); + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "Invalid value for option '--image-format': expected one of [Docker, OCI] (case-sensitive) but was 'unknown'"); + } + + @Test + public void testParse_programArguments() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--program-args=arg1,arg2", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getProgramArguments()) + .isEqualTo(ImmutableList.of("arg1", "arg2")); + } + + @Test + public void testParse_entrypoint() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--entrypoint=java -cp myClass", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getEntrypoint()) + .isEqualTo(ImmutableList.of("java", "-cp", "myClass")); + } + + @Test + public void testParse_creationTime_milliseconds() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--creation-time=23", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getCreationTime()) + .hasValue(Instant.ofEpochMilli(23)); + } + + @Test + public void testParse_creationTime_iso8601() { + War warCommand = + CommandLine.populateCommand( + new War(), + "--target=test-image-ref", + "--creation-time=2011-12-03T22:42:05Z", + "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.getCreationTime()) + .hasValue(Instant.parse("2011-12-03T22:42:05Z")); + } + + @Test + public void testValidate_nameMissingFail() { + War warCommand = + CommandLine.populateCommand(new War(), "--target=tar://sometar.tar", "my-app.war"); + CommandLine.ParameterException pex = + assertThrows(CommandLine.ParameterException.class, warCommand.commonCliOptions::validate); + assertThat(pex) + .hasMessageThat() + .isEqualTo("Missing option: --name must be specified when using --target=tar://...."); + } + + @Test + public void testValidate_pass() { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=tar://sometar.tar", "--name=test.io/test/test", "my-app.war"); + warCommand.commonCliOptions.validate(); + // pass + } + + @Test + public void testIsJetty_noCustomBaseImage() throws InvalidImageReferenceException { + War warCommand = + CommandLine.populateCommand(new War(), "--target=test-image-ref", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isTrue(); + } + + @Test + public void testIsJetty_nonJetty() throws InvalidImageReferenceException { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--from=base-image", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isFalse(); + } + + @Test + public void testIsJetty_customJetty() throws InvalidImageReferenceException { + War warCommand = + CommandLine.populateCommand( + new War(), "--target=test-image-ref", "--from=jetty:tag", "my-app.war"); + assertThat(warCommand.commonContainerConfigCliOptions.isJettyBaseimage()).isTrue(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpecTest.java new file mode 100644 index 0000000000..342aee8dea --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpecTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.nio.file.Paths; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ArchiveLayerSpec}. */ +public class ArchiveLayerSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testArchiveLayerSpec_full() throws JsonProcessingException { + String data = + "name: layer name\n" + "archive: out/archive.tgz\n" + "mediaType: test.media.type"; + + ArchiveLayerSpec parsed = mapper.readValue(data, ArchiveLayerSpec.class); + Assert.assertEquals("layer name", parsed.getName()); + Assert.assertEquals(Paths.get("out/archive.tgz"), parsed.getArchive()); + Assert.assertEquals("test.media.type", parsed.getMediaType().get()); + } + + @Test + public void testArchiveLayerSpec_nameRequired() { + String data = "archive: out/archive"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'name'")); + } + } + + @Test + public void testArchiveLayerSpec_nameNonNull() { + String data = "name: null\n" + "archive: out/archive"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'name' cannot be null")); + } + } + + @Test + public void testArchiveLayerSpec_nameNonEmpty() { + String data = "name: ''\n" + "archive: out/archive"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'name' cannot be an empty string")); + } + } + + // With {@link LayerSpec.Deserializer#deserialize} this test seems pointless, but it still helps + // define the behavior of this class. + @Test + public void testArchiveLayerSpec_archiveRequired() { + String data = "name: layer name"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'archive'")); + } + } + + @Test + public void testArchiveLayerSpec_archiveNonNull() { + String data = "name: layer name\n" + "archive: null"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'archive' cannot be null")); + } + } + + @Test + public void testArchiveLayerSpec_archiveNonEmpty() { + String data = "name: layer name\n" + "archive: ''"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'archive' cannot be an empty string")); + } + } + + @Test + public void testArchiveLayerSpec_mediaTypeNonEmpty() { + String data = "name: layer name\n" + "archive: out/archive.tgz\n" + "mediaType: ' '"; + + try { + mapper.readValue(data, ArchiveLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'mediaType' cannot be an empty string")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpecTest.java new file mode 100644 index 0000000000..7f54f7174c --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpecTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.collect.ImmutableList; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link BaseImageSpec}. */ +public class BaseImageSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testBaseImageSpec_full() throws JsonProcessingException { + String data = + "image: gcr.io/example/jib\n" + + "platforms:\n" // trivial platform spec + + " - architecture: amd64\n" + + " os: linux\n"; + + BaseImageSpec baseImageSpec = mapper.readValue(data, BaseImageSpec.class); + Assert.assertEquals("gcr.io/example/jib", baseImageSpec.getImage()); + Assert.assertEquals("amd64", baseImageSpec.getPlatforms().get(0).getArchitecture()); + Assert.assertEquals("linux", baseImageSpec.getPlatforms().get(0).getOs()); + } + + @Test + public void testBaseImageSpec_imageRequired() { + String data = + "platforms:\n" // trivial platform spec + + " - architecture: amd64\n" + + " os: linux\n"; + try { + mapper.readValue(data, BaseImageSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'image'")); + } + } + + @Test + public void testBaseImageSpec_imageNotNull() { + String data = + "image: null\n" + + "platforms:\n" // trivial platform spec + + " - architecture: amd64\n" + + " os: linux\n"; + try { + mapper.readValue(data, BaseImageSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'image' cannot be null")); + } + } + + @Test + public void testBaseImageSpec_imageNotEmpty() { + String data = + "image: ''\n" + + "platforms:\n" // trivial platform spec + + " - architecture: amd64\n" + + " os: linux\n"; + try { + mapper.readValue(data, BaseImageSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'image' cannot be an empty string")); + } + } + + @Test + public void testBaseImageSpec_nullCollections() throws JsonProcessingException { + String data = "image: gcr.io/example/jib\n"; + + BaseImageSpec baseImageSpec = mapper.readValue(data, BaseImageSpec.class); + Assert.assertEquals(ImmutableList.of(), baseImageSpec.getPlatforms()); + } + + @Test + public void testBaseImageSpec_platformsNoNullEntries() { + String data = "image: gcr.io/example/jib\n" + "platforms: [null]\n"; + + try { + mapper.readValue(data, BaseImageSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'platforms' cannot contain null entries")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpecTest.java new file mode 100644 index 0000000000..cf3f019282 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpecTest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.Ports; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.common.collect.ImmutableList; +import java.nio.file.Paths; +import java.time.Instant; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link BuildFileSpec}. */ +@RunWith(JUnitParamsRunner.class) +public class BuildFileSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testBuildFileSpec_full() throws JsonProcessingException { + String data = + "apiVersion: v1alpha1\n" + + "kind: BuildFile\n" + + "from:\n" // trivial base image spec + + " image: gcr.io/example/jib\n" + + "creationTime: 1\n" + + "format: OCI\n" + + "environment:\n" + + " env_key: env_value\n" + + "labels:\n" + + " label_key: label_value\n" + + "volumes:\n" + + " - /my/volume\n" + + "exposedPorts:\n" + + " - 8080\n" + + "user: username\n" + + "workingDirectory: /workspace\n" + + "entrypoint:\n" + + " - java\n" + + " - -jar\n" + + "cmd:\n" + + " - myjar.jar\n" + + "layers:\n" // trivial layers + + " entries:\n" + + " - name: some layer\n" + + " archive: /something.tgz\n"; + + BuildFileSpec parsed = mapper.readValue(data, BuildFileSpec.class); + assertThat(parsed.getApiVersion()).isEqualTo("v1alpha1"); + assertThat(parsed.getKind()).isEqualTo("BuildFile"); + assertThat(parsed.getFrom().get().getImage()).isEqualTo("gcr.io/example/jib"); + assertThat(parsed.getCreationTime().get()).isEqualTo(Instant.ofEpochMilli(1)); + assertThat(parsed.getFormat().get()).isEqualTo(ImageFormat.OCI); + assertThat(parsed.getEnvironment()).containsExactly("env_key", "env_value"); + assertThat(parsed.getLabels()).containsExactly("label_key", "label_value"); + assertThat(parsed.getVolumes()).containsExactly(AbsoluteUnixPath.get("/my/volume")); + assertThat(parsed.getExposedPorts()).isEqualTo(Ports.parse(ImmutableList.of("8080"))); + assertThat(parsed.getUser().get()).isEqualTo("username"); + assertThat(parsed.getWorkingDirectory().get()).isEqualTo(AbsoluteUnixPath.get("/workspace")); + assertThat(parsed.getEntrypoint().get()).containsExactly("java", "-jar").inOrder(); + assertThat(parsed.getCmd().get()).containsExactly("myjar.jar"); + assertThat(((ArchiveLayerSpec) parsed.getLayers().get().getEntries().get(0)).getName()) + .isEqualTo("some layer"); + assertThat(((ArchiveLayerSpec) parsed.getLayers().get().getEntries().get(0)).getArchive()) + .isEqualTo(Paths.get("/something.tgz")); + } + + @Test + public void testBuildFileSpec_apiVersionRequired() { + String data = "kind: BuildFile\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .startsWith("Missing required creator property 'apiVersion'"); + } + + @Test + public void testBuildFileSpec_apiVersionNotNull() { + String data = "apiVersion: null\n" + "kind: BuildFile\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception).hasMessageThat().contains("Property 'apiVersion' cannot be null"); + } + + @Test + public void testBuildFileSpec_apiVersionNotEmpty() { + String data = "apiVersion: ''\n" + "kind: BuildFile\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .contains("Property 'apiVersion' cannot be an empty string"); + } + + @Test + public void testBuildFileSpec_kindRequired() { + String data = "apiVersion: v1alpha1\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception).hasMessageThat().startsWith("Missing required creator property 'kind'"); + } + + @Test + public void testBuildFileSpec_kindMustBeBuildFile() { + String data = "apiVersion: v1alpha1\n" + "kind: NotBuildFile\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .contains("Property 'kind' must be 'BuildFile' but is 'NotBuildFile'"); + } + + @Test + public void testBuildFileSpec_kindNotNull() { + String data = "apiVersion: v1alpha1\n" + "kind: null\n"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception).hasMessageThat().contains("Property 'kind' cannot be null"); + } + + @Test + public void testBuildFileSpec_nullCollections() throws JsonProcessingException { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n"; + + BuildFileSpec parsed = mapper.readValue(data, BuildFileSpec.class); + assertThat(parsed.getEnvironment()).isEmpty(); + assertThat(parsed.getLabels()).isEmpty(); + assertThat(parsed.getVolumes()).isEmpty(); + assertThat(parsed.getExposedPorts()).isEmpty(); + // entrypoint and cmd CAN be not present + assertThat(parsed.getEntrypoint()).isEmpty(); + assertThat(parsed.getCmd()).isEmpty(); + } + + @Test + @Parameters(value = {"volumes", "exposedPorts", "entrypoint", "cmd"}) + public void testBuildFileSpec_noNullEntries(String fieldName) { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": ['first', null]"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .contains("Property '" + fieldName + "' cannot contain null entries"); + } + + @Test + @Parameters(value = {"volumes", "exposedPorts", "entrypoint", "cmd"}) + public void testBuildFileSpec_noEmptyEntries(String fieldName) { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": ['first', ' ']"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .contains("Property '" + fieldName + "' cannot contain empty strings"); + } + + @Test + @Parameters(value = {"volumes", "exposedPorts", "entrypoint", "cmd"}) + public void testBuildFileSpec_emptyListOkay(String fieldName) throws JsonProcessingException { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": []"; + + assertThat(mapper.readValue(data, BuildFileSpec.class)).isNotNull(); + } + + @Test + @Parameters( + value = { + "volumes", + "exposedPorts", + "entrypoint", + "cmd", + "creationTime", + "format", + "user", + "workingDirectory", + "environment", + "labels" + }) + public void testBuildFileSpec_nullOkay(String fieldName) throws JsonProcessingException { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": null"; + + assertThat(mapper.readValue(data, BuildFileSpec.class)).isNotNull(); + } + + @Test + @Parameters(value = {"creationTime", "format", "user", "workingDirectory"}) + public void testBuildFileSpec_noEmptyValues(String fieldName) { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": ' '"; + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception) + .hasMessageThat() + .contains("Property '" + fieldName + "' cannot be an empty string"); + } + + @SuppressWarnings("unused") + private static String[][] invalidMapEntries() { + return new String[][] { + {"environment", " key: null", "' cannot contain null values"}, + {"environment", " key: ' '", "' cannot contain empty string values"}, + {"environment", " ' ': value", "' cannot contain empty string keys"}, + {"labels", " key: null", "' cannot contain null values"}, + {"labels", " key: ' '", "' cannot contain empty string values"}, + {"labels", " ' ': value", "' cannot contain empty string keys"}, + }; + } + + @Test + @Parameters(method = "invalidMapEntries") + public void testBuildFileSpec_invalidMapEntries( + String fieldName, String input, String errorMessage) { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ":\n" + input; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, BuildFileSpec.class)); + assertThat(exception).hasMessageThat().contains("Property '" + fieldName + errorMessage); + } + + // A quirk of our parser is that "null" keys are parsed as strings and not null, this test just + // formalizes that behavior. + @Test + @Parameters(value = {"environment", "labels"}) + public void testBuildFileSpec_yamlNullKeysPass(String fieldName) throws JsonProcessingException { + String data = + "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ":\n" + " null: value"; + + assertThat(mapper.readValue(data, BuildFileSpec.class)).isNotNull(); + } + + @Test + @Parameters(value = {"environment", "labels"}) + public void testBuildFileSpec_emptyMapOkay(String fieldName) throws JsonProcessingException { + String data = "apiVersion: v1alpha1\n" + "kind: BuildFile\n" + fieldName + ": {}"; + + assertThat(mapper.readValue(data, BuildFileSpec.class)).isNotNull(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFilesTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFilesTest.java new file mode 100644 index 0000000000..3ecc77d1ec --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFilesTest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.LayerObject; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.cli.Build; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Map; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class BuildFilesTest { + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock private ConsoleLogger consoleLogger; + @Mock private Build buildCli; + @Mock private CommonCliOptions commonCliOptions; + + @Before + public void setUp() { + Mockito.when(buildCli.getTemplateParameters()).thenReturn(ImmutableMap.of()); + } + + @Test + public void testToJibContainerBuilder_allProperties() + throws URISyntaxException, IOException, InvalidImageReferenceException { + Path buildfile = + Paths.get(Resources.getResource("buildfiles/projects/allProperties/jib.yaml").toURI()); + Path projectRoot = buildfile.getParent(); + JibContainerBuilder jibContainerBuilder = + BuildFiles.toJibContainerBuilder( + projectRoot, buildfile, buildCli, commonCliOptions, consoleLogger); + + ContainerBuildPlan resolved = jibContainerBuilder.toContainerBuildPlan(); + Assert.assertEquals("ubuntu", resolved.getBaseImage()); + Assert.assertEquals(Instant.ofEpochMilli(2000), resolved.getCreationTime()); + Assert.assertEquals(ImageFormat.OCI, resolved.getFormat()); + Assert.assertEquals( + ImmutableSet.of(new Platform("arm", "linux"), new Platform("amd64", "darwin")), + resolved.getPlatforms()); + Assert.assertEquals(ImmutableMap.of("KEY1", "v1", "KEY2", "v2"), resolved.getEnvironment()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2")), + resolved.getVolumes()); + Assert.assertEquals(ImmutableMap.of("label1", "l1", "label2", "l2"), resolved.getLabels()); + Assert.assertEquals( + ImmutableSet.of(Port.udp(123), Port.tcp(456), Port.tcp(789)), resolved.getExposedPorts()); + Assert.assertEquals("customUser", resolved.getUser()); + Assert.assertEquals(AbsoluteUnixPath.get("/home"), resolved.getWorkingDirectory()); + Assert.assertEquals(ImmutableList.of("sh", "script.sh"), resolved.getEntrypoint()); + Assert.assertEquals(ImmutableList.of("--param", "param"), resolved.getCmd()); + + Assert.assertEquals(1, resolved.getLayers().size()); + FileEntriesLayer resolvedLayer = (FileEntriesLayer) resolved.getLayers().get(0); + Assert.assertEquals("scripts", resolvedLayer.getName()); + Assert.assertEquals( + FileEntriesLayer.builder() + .addEntry( + projectRoot.resolve("project/script.sh"), AbsoluteUnixPath.get("/home/script.sh")) + .build() + .getEntries(), + resolvedLayer.getEntries()); + Assert.assertEquals(LayerObject.Type.FILE_ENTRIES, resolvedLayer.getType()); + } + + @Test + public void testToJibContainerBuilder_requiredProperties() + throws URISyntaxException, IOException, InvalidImageReferenceException { + Path buildfile = + Paths.get(Resources.getResource("buildfiles/projects/allDefaults/jib.yaml").toURI()); + JibContainerBuilder jibContainerBuilder = + BuildFiles.toJibContainerBuilder( + buildfile.getParent(), buildfile, buildCli, commonCliOptions, consoleLogger); + + ContainerBuildPlan resolved = jibContainerBuilder.toContainerBuildPlan(); + Assert.assertEquals("scratch", resolved.getBaseImage()); + Assert.assertEquals(ImmutableSet.of(new Platform("amd64", "linux")), resolved.getPlatforms()); + Assert.assertEquals(Instant.EPOCH, resolved.getCreationTime()); + Assert.assertEquals(ImageFormat.Docker, resolved.getFormat()); + Assert.assertTrue(resolved.getEnvironment().isEmpty()); + Assert.assertTrue(resolved.getLabels().isEmpty()); + Assert.assertTrue(resolved.getVolumes().isEmpty()); + Assert.assertTrue(resolved.getExposedPorts().isEmpty()); + Assert.assertNull(resolved.getUser()); + Assert.assertNull(resolved.getWorkingDirectory()); + Assert.assertNull(resolved.getEntrypoint()); + Assert.assertTrue(resolved.getLayers().isEmpty()); + } + + @Test + public void testToBuildFileSpec_withTemplating() + throws URISyntaxException, InvalidImageReferenceException, IOException { + Path buildfile = + Paths.get(Resources.getResource("buildfiles/projects/templating/valid.yaml").toURI()); + + Mockito.when(buildCli.getTemplateParameters()) + .thenReturn( + ImmutableMap.of( + "unused", "ignored", // keys that are defined but not used do not throw an error + "key", "templateKey", + "value", "templateValue", + "repeated", "repeatedValue")); + JibContainerBuilder jibContainerBuilder = + BuildFiles.toJibContainerBuilder( + buildfile.getParent(), buildfile, buildCli, commonCliOptions, consoleLogger); + + ContainerBuildPlan resolved = jibContainerBuilder.toContainerBuildPlan(); + Map expectedLabels = + ImmutableMap.builder() + .put("templateKey", "templateValue") + .put("label1", "repeatedValue") + .put("label2", "repeatedValue") + .put("label3", "${escaped}") + .put("label4", "free$") + .put("unmatched", "${") + .build(); + Assert.assertEquals(expectedLabels, resolved.getLabels()); + } + + @Test + public void testToBuildFileSpec_failWithMissingTemplateVariable() + throws URISyntaxException, InvalidImageReferenceException, IOException { + Path buildfile = + Paths.get(Resources.getResource("buildfiles/projects/templating/missingVar.yaml").toURI()); + + try { + BuildFiles.toJibContainerBuilder( + buildfile.getParent(), buildfile, buildCli, commonCliOptions, consoleLogger); + Assert.fail(); + } catch (IllegalArgumentException iae) { + MatcherAssert.assertThat( + iae.getMessage(), CoreMatchers.startsWith("Cannot resolve variable 'missingVar'")); + } + } + + @Test + public void testToBuildFileSpec_templateMultiLineBehavior() + throws URISyntaxException, InvalidImageReferenceException, IOException { + Path buildfile = + Paths.get(Resources.getResource("buildfiles/projects/templating/multiLine.yaml").toURI()); + + Mockito.when(buildCli.getTemplateParameters()) + .thenReturn(ImmutableMap.of("replace" + "\n" + "this", "creationTime: 1234")); + JibContainerBuilder jibContainerBuilder = + BuildFiles.toJibContainerBuilder( + buildfile.getParent(), buildfile, buildCli, commonCliOptions, consoleLogger); + ContainerBuildPlan resolved = jibContainerBuilder.toContainerBuildPlan(); + Assert.assertEquals(Instant.ofEpochMilli(1234), resolved.getCreationTime()); + } + + @Test + public void testToBuildFileSpec_alternativeRootContext() + throws URISyntaxException, InvalidImageReferenceException, IOException { + Path buildfile = + Paths.get( + Resources.getResource("buildfiles/projects/allProperties/altYamls/alt-jib.yaml") + .toURI()); + Path projectRoot = buildfile.getParent().getParent(); + JibContainerBuilder jibContainerBuilder = + BuildFiles.toJibContainerBuilder( + projectRoot, buildfile, buildCli, commonCliOptions, consoleLogger); + + ContainerBuildPlan resolved = jibContainerBuilder.toContainerBuildPlan(); + Assert.assertEquals( + FileEntriesLayer.builder() + .addEntry( + projectRoot.resolve("project/script.sh"), AbsoluteUnixPath.get("/home/script.sh")) + .build() + .getEntries(), + ((FileEntriesLayer) resolved.getLayers().get(0)).getEntries()); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/CopySpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/CopySpecTest.java new file mode 100644 index 0000000000..4806b414af --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/CopySpecTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import java.nio.file.Paths; +import java.time.Instant; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link CopySpec}. */ +@RunWith(JUnitParamsRunner.class) +public class CopySpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testCopySpec_full() throws JsonProcessingException { + String data = + "src: target/classes\n" + + "dest: /app/classes\n" + + "includes:\n" + + " - '**/*.in'\n" + + "excludes:\n" + + " - '**/*.ex'\n" + + "properties:\n" // only trivial test of file properties + + " timestamp: 1\n"; + + CopySpec parsed = mapper.readValue(data, CopySpec.class); + assertThat(parsed.getSrc()).isEqualTo(Paths.get("target/classes")); + assertThat(parsed.getDest()).isEqualTo(AbsoluteUnixPath.get("/app/classes")); + assertThat(parsed.getIncludes()).containsExactly("**/*.in"); + assertThat(parsed.getExcludes()).containsExactly("**/*.ex"); + assertThat(parsed.getProperties().get().getTimestamp().get()) + .isEqualTo(Instant.ofEpochMilli(1)); + } + + @Test + @Parameters( + value = { + "dest: /app/classes\n, Missing required creator property 'src'", + "src: target/classes\n, Missing required creator property 'dest'" + }) + public void testCopySpec_required(String data, String errorMessage) { + Exception exception = + assertThrows(JsonProcessingException.class, () -> mapper.readValue(data, CopySpec.class)); + assertThat(exception).hasMessageThat().startsWith(errorMessage); + } + + @Test + public void testCopySpec_destEndsWithSlash() throws JsonProcessingException { + String data = "src: target/classes\n" + "dest: /app/classes/"; + + CopySpec parsed = mapper.readValue(data, CopySpec.class); + assertThat(parsed.getDest()).isEqualTo(AbsoluteUnixPath.get("/app/classes")); + assertThat(parsed.isDestEndsWithSlash()).isTrue(); + } + + @Test + public void testCopySpec_destDoesNotEndWithSlash() throws JsonProcessingException { + String data = "src: target/classes\n" + "dest: /app/classes"; + + CopySpec parsed = mapper.readValue(data, CopySpec.class); + assertThat(parsed.getDest()).isEqualTo(AbsoluteUnixPath.get("/app/classes")); + assertThat(parsed.isDestEndsWithSlash()).isFalse(); + } + + @Test + @Parameters( + value = { + "src: null\ndest: /app/classes\n, Property 'src' cannot be null", + "src: ''\ndest: /app/classes\n, Property 'src' cannot be an empty string", + "src: target/classes\ndest: null\n, Property 'dest' cannot be null", + "src: target/classes\ndest: ''\n, Property 'dest' cannot be an empty string" + }) + public void testCopySpec_nullEmptyCheck(String data, String errorMessage) { + Exception exception = + assertThrows(JsonProcessingException.class, () -> mapper.readValue(data, CopySpec.class)); + assertThat(exception).hasMessageThat().contains(errorMessage); + } + + @Test + public void testCopySpec_nullCollections() throws JsonProcessingException { + String data = "src: target/classes\n" + "dest: /app/classes\n"; + + CopySpec parsed = mapper.readValue(data, CopySpec.class); + assertThat(parsed.getIncludes()).isEmpty(); + assertThat(parsed.getExcludes()).isEmpty(); + } + + @Test + @Parameters(value = {"includes", "excludes"}) + public void testCopySpec_noNullEntries(String fieldName) { + String data = + "src: target/classes\n" + "dest: /app/classes\n" + fieldName + ": ['first', null]"; + + Exception exception = + assertThrows(JsonProcessingException.class, () -> mapper.readValue(data, CopySpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Property '" + fieldName + "' cannot contain null entries"); + } + + @Test + @Parameters(value = {"includes", "excludes"}) + public void testCopySpec_noEmptyEntries(String fieldName) { + String data = "src: target/classes\n" + "dest: /app/classes\n" + fieldName + ": ['first', ' ']"; + + Exception exception = + assertThrows(JsonProcessingException.class, () -> mapper.readValue(data, CopySpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Property '" + fieldName + "' cannot contain empty strings"); + } + + @Test + @Parameters(value = {"includes", "excludes"}) + public void testCopySpec_emptyOkay(String fieldName) throws JsonProcessingException { + String data = "src: target/classes\n" + "dest: /app/classes\n" + fieldName + ": []"; + + assertThat(mapper.readValue(data, CopySpec.class)).isNotNull(); + } + + @Test + @Parameters(value = {"includes", "excludes"}) + public void testCopySpec_nullOkay(String fieldName) throws JsonProcessingException { + String data = "src: target/classes\n" + "dest: /app/classes\n" + fieldName + ": null"; + + assertThat(mapper.readValue(data, CopySpec.class)).isNotNull(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpecTest.java new file mode 100644 index 0000000000..b42551aae4 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpecTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import java.nio.file.Paths; +import java.time.Instant; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link FileLayerSpec}. */ +public class FileLayerSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testFileLayerSpec_full() throws JsonProcessingException { + String data = + "name: layer name\n" + + "files:\n" // trivial copy spec + + " - src: source\n" + + " dest: /dest\n" + + "properties:\n" // trivial file properties spec + + " timestamp: 1\n"; + + FileLayerSpec parsed = mapper.readValue(data, FileLayerSpec.class); + Assert.assertEquals("layer name", parsed.getName()); + Assert.assertEquals(Paths.get("source"), parsed.getFiles().get(0).getSrc()); + Assert.assertEquals(AbsoluteUnixPath.get("/dest"), parsed.getFiles().get(0).getDest()); + Assert.assertEquals(Instant.ofEpochMilli(1), parsed.getProperties().get().getTimestamp().get()); + } + + @Test + public void testFileLayerSpec_nameRequired() { + String data = "files:\n" + " - src: source\n" + " dest: /dest\n"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'name'")); + } + } + + @Test + public void testFileLayerSpec_nameNotNull() { + String data = "name: null\n" + "files:\n" + " - src: source\n" + " dest: /dest\n"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'name' cannot be null")); + } + } + + @Test + public void testFileLayerSpec_nameNotEmpty() { + String data = "name: ''\n" + "files:\n" + " - src: source\n" + " dest: /dest\n"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'name' cannot be an empty string")); + } + } + + @Test + public void testFileLayerSpec_filesRequired() { + String data = "name: layer name"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'files'")); + } + } + + @Test + public void testFileLayerSpec_filesNotNull() { + String data = "name: layer name\n" + "files: null"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'files' cannot be null")); + } + } + + @Test + public void testFileLayerSpec_filesNotEmpty() { + String data = "name: layer name\n" + "files: []\n"; + + try { + mapper.readValue(data, FileLayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'files' cannot be an empty collection")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpecTest.java new file mode 100644 index 0000000000..69ec668088 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpecTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import java.time.Instant; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link FilePropertiesSpec}. */ +@RunWith(JUnitParamsRunner.class) +public class FilePropertiesSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testFilePropertiesSpec_full() throws JsonProcessingException { + String data = + "filePermissions: 644\n" + + "directoryPermissions: 755\n" + + "user: goose\n" + + "group: birds\n" + + "timestamp: 1\n"; + + FilePropertiesSpec parsed = mapper.readValue(data, FilePropertiesSpec.class); + assertThat(parsed.getFilePermissions().get()).isEqualTo(FilePermissions.fromOctalString("644")); + assertThat(parsed.getDirectoryPermissions().get()) + .isEqualTo(FilePermissions.fromOctalString("755")); + assertThat(parsed.getUser().get()).isEqualTo("goose"); + assertThat(parsed.getGroup().get()).isEqualTo("birds"); + assertThat(parsed.getTimestamp().get()).isEqualTo(Instant.ofEpochMilli(1)); + } + + @Test + public void testFilePropertiesSpec_badFilePermissions() { + String data = "filePermissions: 888"; + + Exception exception = + assertThrows( + JsonMappingException.class, () -> mapper.readValue(data, FilePropertiesSpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("octalPermissions must be a 3-digit octal number (000-777)"); + } + + @Test + public void testFilePropertiesSpec_badDirectoryPermissions() { + String data = "directoryPermissions: 888"; + + Exception exception = + assertThrows( + JsonMappingException.class, () -> mapper.readValue(data, FilePropertiesSpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("octalPermissions must be a 3-digit octal number (000-777)"); + } + + @Test + public void testFilePropertiesSpec_timestampSpecIso8601() throws JsonProcessingException { + String data = "timestamp: 2020-06-08T14:54:36+00:00"; + + FilePropertiesSpec parsed = mapper.readValue(data, FilePropertiesSpec.class); + assertThat(parsed.getTimestamp().get()).isEqualTo(Instant.parse("2020-06-08T14:54:36Z")); + } + + @Test + public void testFilePropertiesSpec_badTimestamp() { + String data = "timestamp: hi"; + + Exception exception = + assertThrows( + JsonMappingException.class, () -> mapper.readValue(data, FilePropertiesSpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo( + "timestamp must be a number of milliseconds since epoch or an ISO 8601 formatted date"); + } + + @Test + public void testFilePropertiesSpec_failOnUnknown() { + String data = "badkey: badvalue"; + + Exception exception = + assertThrows( + UnrecognizedPropertyException.class, + () -> mapper.readValue(data, FilePropertiesSpec.class)); + assertThat(exception).hasMessageThat().contains("Unrecognized field \"badkey\""); + } + + @Test + @Parameters(value = {"filePermissions", "directoryPermissions", "user", "group", "timestamp"}) + public void testFilePropertiesSpec_noEmptyValues(String fieldName) { + String data = fieldName + ": ' '"; + + Exception exception = + assertThrows( + JsonProcessingException.class, () -> mapper.readValue(data, FilePropertiesSpec.class)); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Property '" + fieldName + "' cannot be an empty string"); + } + + @Test + @Parameters(value = {"filePermissions", "directoryPermissions", "user", "group", "timestamp"}) + public void testFilePropertiesSpec_nullOkay(String fieldName) throws JsonProcessingException { + String data = fieldName + ": null"; + + FilePropertiesSpec parsed = mapper.readValue(data, FilePropertiesSpec.class); + assertThat(parsed.getFilePermissions().isPresent()).isFalse(); + assertThat(parsed.getDirectoryPermissions().isPresent()).isFalse(); + assertThat(parsed.getUser().isPresent()).isFalse(); + assertThat(parsed.getGroup().isPresent()).isFalse(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStackTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStackTest.java new file mode 100644 index 0000000000..cfdaa98131 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStackTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; + +public class FilePropertiesStackTest { + + @Test + public void testDefaults() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + Assert.assertEquals(FilePermissions.fromOctalString("644"), testStack.getFilePermissions()); + Assert.assertEquals( + FilePermissions.fromOctalString("755"), testStack.getDirectoryPermissions()); + Assert.assertEquals("", testStack.getOwnership()); + Assert.assertEquals(Instant.ofEpochSecond(1), testStack.getModificationTime()); + } + + @Test + public void testPush_simple() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + + Assert.assertEquals(FilePermissions.fromOctalString("111"), testStack.getFilePermissions()); + Assert.assertEquals( + FilePermissions.fromOctalString("111"), testStack.getDirectoryPermissions()); + Assert.assertEquals("1:1", testStack.getOwnership()); + Assert.assertEquals(Instant.ofEpochMilli(1), testStack.getModificationTime()); + } + + @Test + public void testPush_stacking() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + testStack.push(new FilePropertiesSpec(null, "222", "2", null, null)); + testStack.push(new FilePropertiesSpec("333", null, "3", null, "3")); + + Assert.assertEquals(FilePermissions.fromOctalString("333"), testStack.getFilePermissions()); + Assert.assertEquals( + FilePermissions.fromOctalString("222"), testStack.getDirectoryPermissions()); + Assert.assertEquals("3:1", testStack.getOwnership()); + Assert.assertEquals(Instant.ofEpochMilli(3), testStack.getModificationTime()); + } + + @Test + public void testPush_tooMany() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + + try { + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + Assert.fail(); + } catch (IllegalStateException ise) { + Assert.assertEquals("Error in file properties stack push, stacking over 3", ise.getMessage()); + } + } + + @Test + public void testPop_toZero() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + testStack.pop(); + + Assert.assertEquals(FilePermissions.fromOctalString("644"), testStack.getFilePermissions()); + Assert.assertEquals( + FilePermissions.fromOctalString("755"), testStack.getDirectoryPermissions()); + Assert.assertEquals("", testStack.getOwnership()); + Assert.assertEquals(Instant.ofEpochSecond(1), testStack.getModificationTime()); + } + + @Test + public void testPop_toOlderState() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + testStack.push(new FilePropertiesSpec("111", "111", "1", "1", "1")); + testStack.push(new FilePropertiesSpec(null, "222", "2", null, null)); + + testStack.pop(); + + Assert.assertEquals(FilePermissions.fromOctalString("111"), testStack.getFilePermissions()); + Assert.assertEquals( + FilePermissions.fromOctalString("111"), testStack.getDirectoryPermissions()); + Assert.assertEquals("1:1", testStack.getOwnership()); + Assert.assertEquals(Instant.ofEpochMilli(1), testStack.getModificationTime()); + } + + @Test + public void testPop_nothingToPop() { + FilePropertiesStack testStack = new FilePropertiesStack(); + + try { + testStack.pop(); + Assert.fail(); + } catch (IllegalStateException ise) { + Assert.assertEquals("Error in file properties stack pop, popping at 0", ise.getMessage()); + } + } + + @Test + public void testGetOwnership_onlyUser() { + FilePropertiesStack testStack = new FilePropertiesStack(); + testStack.push(new FilePropertiesSpec(null, null, "u", null, null)); + + Assert.assertEquals("u", testStack.getOwnership()); + } + + @Test + public void testGetOwnership_onlyGroup() { + FilePropertiesStack testStack = new FilePropertiesStack(); + testStack.push(new FilePropertiesSpec(null, null, null, "g", null)); + + Assert.assertEquals(":g", testStack.getOwnership()); + } + + @Test + public void testGetOwnership_userAndGroup() { + FilePropertiesStack testStack = new FilePropertiesStack(); + testStack.push(new FilePropertiesSpec(null, null, "u", "g", null)); + + Assert.assertEquals("u:g", testStack.getOwnership()); + } + + @Test + public void testGetOwnership_noUserNoGroup() { + FilePropertiesStack testStack = new FilePropertiesStack(); + testStack.push(new FilePropertiesSpec(null, null, null, null, null)); + + Assert.assertEquals("", testStack.getOwnership()); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpecTest.java new file mode 100644 index 0000000000..9cf5001ce7 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpecTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LayerSpec}. */ +public class LayerSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void deserialize_toFileLayer() throws JsonProcessingException { + String data = + "name: layer name\n" + + "files:\n" // trivial copy spec + + " - src: source\n" + + " dest: /dest\n"; + + LayerSpec layerSpec = mapper.readValue(data, LayerSpec.class); + MatcherAssert.assertThat(layerSpec, CoreMatchers.instanceOf(FileLayerSpec.class)); + } + + @Test + public void deserialize_toArchiveLayer() throws JsonProcessingException { + String data = "name: layer name\n" + "archive: out/archive.tgz\n"; + + LayerSpec layerSpec = mapper.readValue(data, LayerSpec.class); + MatcherAssert.assertThat(layerSpec, CoreMatchers.instanceOf(ArchiveLayerSpec.class)); + } + + @Test + public void deserialize_error() { + String data = "name: layer name\n"; + + try { + mapper.readValue(data, LayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.endsWith("Could not parse entry into ArchiveLayer or FileLayer")); + } + } + + @Test + public void deserialize_nameMissing() { + String data = "archive: out/archive.tgz\n"; + + try { + mapper.readValue(data, LayerSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.endsWith("Could not parse layer entry, missing required property 'name'")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpecTest.java new file mode 100644 index 0000000000..007dfe8644 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpecTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.nio.file.Paths; +import java.time.Instant; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LayersSpec}. */ +public class LayersSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testLayersSpec_full() throws JsonProcessingException { + String data = + "entries:\n" // trivial layer + + " - name: some layer\n" + + " archive: /something.tgz\n" + + "properties:\n" // trivial file properties spec + + " timestamp: 1\n"; + + LayersSpec parsed = mapper.readValue(data, LayersSpec.class); + Assert.assertEquals("some layer", ((ArchiveLayerSpec) parsed.getEntries().get(0)).getName()); + Assert.assertEquals( + Paths.get("/something.tgz"), ((ArchiveLayerSpec) parsed.getEntries().get(0)).getArchive()); + Assert.assertEquals(Instant.ofEpochMilli(1), parsed.getProperties().get().getTimestamp().get()); + } + + @Test + public void testLayersSpec_entriesRequired() { + String data = + "properties:\n" // trivial file properties spec + + " timestamp: 1\n"; + + try { + mapper.readValue(data, LayersSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'entries'")); + } + } + + @Test + public void testLayersSpec_entriesNotNull() { + String data = + "entries: null\n" + + "properties:\n" // trivial file properties spec + + " timestamp: 1\n"; + + try { + mapper.readValue(data, LayersSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'entries' cannot be null")); + } + } + + @Test + public void testLayersSpec_entriesNotEmpty() { + String data = + "entries: []\n" + + "properties:\n" // trivial file properties spec + + " timestamp: 1\n"; + + try { + mapper.readValue(data, LayersSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'entries' cannot be an empty collection")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersTest.java new file mode 100644 index 0000000000..51be08a310 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersTest.java @@ -0,0 +1,331 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.List; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Layers}. */ +public class LayersTest { + + private static final String LAYERS_TEST_RESOURCE_DIR = "buildfiles/layers/"; + + public static List parseLayers(Path testDir, int expectedLayerCount) + throws IOException { + Path layersSpecYaml = testDir.resolve("layers.yaml"); + List layers = + Layers.toLayers( + layersSpecYaml.getParent(), + new ObjectMapper(new YAMLFactory()) + .readValue( + Files.newBufferedReader(layersSpecYaml, Charsets.UTF_8), LayersSpec.class)); + + Assert.assertEquals(expectedLayerCount, layers.size()); + return layers; + } + + private static Path getLayersTestRoot(String testName) throws URISyntaxException { + return Paths.get(Resources.getResource(LAYERS_TEST_RESOURCE_DIR + testName).toURI()); + } + + @Test + public void testToLayers_properties() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("propertiesTest"); + List layers = parseLayers(testRoot, 4); + + checkLayer( + layers.get(0), + "level 0 passthrough", + ImmutableSet.of( + newEntry(testRoot, "dir", "/app", "700", 0, "0:0"), + newEntry(testRoot, "dir/file.txt", "/app/file.txt", "000", 0, "0:0"))); + + checkLayer( + layers.get(1), + "level 1 overrides", + ImmutableSet.of( + newEntry(testRoot, "dir", "/app", "711", 1000, "1:1"), + newEntry(testRoot, "dir/file.txt", "/app/file.txt", "111", 1000, "1:1"))); + + checkLayer( + layers.get(2), + "level 2 overrides", + ImmutableSet.of( + newEntry(testRoot, "dir", "/app", "722", 2000, "2:2"), + newEntry(testRoot, "dir/file.txt", "/app/file.txt", "222", 2000, "2:2"))); + + checkLayer( + layers.get(3), + "partial overrides", + ImmutableSet.of( + newEntry(testRoot, "dir", "/app", "711", 2000, "0:2"), + newEntry(testRoot, "dir/file.txt", "/app/file.txt", "111", 2000, "0:2"))); + } + + @Test + public void testToLayers_includeExcludes() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("includesExcludesTest"); + List layers = parseLayers(testRoot, 6); + + checkLayer( + layers.get(0), + "includes and excludes", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/ie", "755", 0, "0:0"), + newEntry(testRoot, "project/includedDir/", "/target/ie/includedDir/", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/ie/includedDir/include.me", + "644", + 0, + "0:0"))); + + checkLayer( + layers.get(1), + "includes only", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/io", "755", 0, "0:0"), + newEntry(testRoot, "project/includedDir/", "/target/io/includedDir/", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/io/includedDir/include.me", + "644", + 0, + "0:0"))); + + checkLayer( + layers.get(2), + "excludes only", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/eo", "755", 0, "0:0"), + newEntry(testRoot, "project/excludedDir", "/target/eo/excludedDir", "755", 0, "0:0"), + newEntry(testRoot, "project/includedDir", "/target/eo/includedDir", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/eo/includedDir/include.me", + "644", + 0, + "0:0"), + newEntry(testRoot, "project/wild.card", "/target/eo/wild.card", "644", 0, "0:0"))); + + checkLayer( + layers.get(3), + "excludes only shortcut", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/eo", "755", 0, "0:0"), + newEntry(testRoot, "project/excludedDir", "/target/eo/excludedDir", "755", 0, "0:0"), + newEntry(testRoot, "project/includedDir", "/target/eo/includedDir", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/eo/includedDir/include.me", + "644", + 0, + "0:0"), + newEntry(testRoot, "project/wild.card", "/target/eo/wild.card", "644", 0, "0:0"))); + + checkLayer( + layers.get(4), + "exclude dir and contents", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/edac", "755", 0, "0:0"), + newEntry( + testRoot, "project/includedDir/", "/target/edac/includedDir/", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/edac/includedDir/include.me", + "644", + 0, + "0:0"), + newEntry(testRoot, "project/wild.card", "/target/edac/wild.card", "644", 0, "0:0"))); + + checkLayer( + layers.get(5), + "excludes only wrong", + ImmutableSet.of( + newEntry(testRoot, "project", "/target/eo", "755", 0, "0:0"), + newEntry(testRoot, "project/excludedDir", "/target/eo/excludedDir", "755", 0, "0:0"), + newEntry( + testRoot, + "project/excludedDir/exclude.me", + "/target/eo/excludedDir/exclude.me", + "644", + 0, + "0:0"), + newEntry(testRoot, "project/includedDir", "/target/eo/includedDir", "755", 0, "0:0"), + newEntry( + testRoot, + "project/includedDir/include.me", + "/target/eo/includedDir/include.me", + "644", + 0, + "0:0"), + newEntry(testRoot, "project/wild.card", "/target/eo/wild.card", "644", 0, "0:0"))); + } + + @Test + public void testToLayers_file() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("fileTest/default"); + List layers = parseLayers(testRoot, 1); + + checkLayer( + layers.get(0), + "default", + ImmutableSet.of( + newEntry(testRoot, "toFile.txt", "/target/toFile.txt", "644", 0, "0:0"), + newEntry(testRoot, "toDir.txt", "/target/dir/toDir.txt", "644", 0, "0:0"))); + } + + @Test + public void testToLayers_fileWithIncludes() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("fileTest/failWithIncludes"); + try { + parseLayers(testRoot, 0); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + Assert.assertEquals( + "Cannot apply includes/excludes on single file copy directives.", uoe.getMessage()); + } + } + + @Test + public void testToLayers_fileWithExcludes() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("fileTest/failWithExcludes"); + try { + parseLayers(testRoot, 0); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + Assert.assertEquals( + "Cannot apply includes/excludes on single file copy directives.", uoe.getMessage()); + } + } + + private static FileEntry newEntry( + Path testRoot, + String src, + String dest, + String octalPermissions, + int millis, + String ownership) { + return new FileEntry( + testRoot.resolve(src), + AbsoluteUnixPath.get(dest), + FilePermissions.fromOctalString(octalPermissions), + Instant.ofEpochMilli(millis), + ownership); + } + + private static void checkLayer( + FileEntriesLayer layer, String expectedName, Set expectedLayerEntries) { + Assert.assertEquals(expectedName, layer.getName()); + + try { + Assert.assertEquals(expectedLayerEntries, ImmutableSet.copyOf(layer.getEntries())); + } catch (AssertionError ae) { + System.out.println("ACTUAL"); + layer + .getEntries() + .forEach( + entry -> { + System.out.println("src: " + entry.getSourceFile()); + System.out.println("dest: " + entry.getExtractionPath()); + System.out.println("permission: " + entry.getPermissions().toOctalString()); + System.out.println("time: " + entry.getModificationTime()); + System.out.println("ownership: " + entry.getOwnership()); + }); + System.out.println("EXCPECTED"); + expectedLayerEntries.forEach( + entry -> { + System.out.println("src: " + entry.getSourceFile()); + System.out.println("dest: " + entry.getExtractionPath()); + System.out.println("permission: " + entry.getPermissions().toOctalString()); + System.out.println("time: " + entry.getModificationTime()); + System.out.println("ownership: " + entry.getOwnership()); + }); + throw ae; + } + } + + @Test + public void testToLayers_pathDoesNotExist() throws IOException, URISyntaxException { + Path testRoot = getLayersTestRoot("pathDoesNotExist"); + try { + parseLayers(testRoot, 0); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + Assert.assertEquals( + "Cannot create FileLayers from non-file, non-directory: " + + testRoot.resolve("something-that-does-not-exist").toString(), + uoe.getMessage()); + } + } + + @Test + public void testToLayers_archiveLayersNotSupported() throws URISyntaxException, IOException { + Path testRoot = getLayersTestRoot("archiveLayerTest"); + try { + parseLayers(testRoot, 0); + Assert.fail(); + } catch (UnsupportedOperationException uoe) { + Assert.assertEquals("Only FileLayers are supported at this time.", uoe.getMessage()); + } + } + + @Test + public void testToLayers_writeToRoot() throws IOException, URISyntaxException { + // this test defines the current behavior of writing to root, perhaps we should ignore + // root at this level or we should ignore it at the builder level + Path testRoot = getLayersTestRoot("writeToRoot"); + List layers = parseLayers(testRoot, 2); + + checkLayer( + layers.get(0), + "root writer", + ImmutableSet.of( + newEntry(testRoot, "dir", "/", "755", 1000, ""), + newEntry(testRoot, "dir/file.txt", "/file.txt", "644", 1000, ""))); + + checkLayer( + layers.get(1), + "root parent fill", + ImmutableSet.of( + newEntry(testRoot, ".", "/", "755", 1000, ""), + newEntry(testRoot, "./dir", "/dir", "755", 1000, ""), + newEntry(testRoot, "./dir/file.txt", "/dir/file.txt", "644", 1000, ""))); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpecTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpecTest.java new file mode 100644 index 0000000000..5e268ec903 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpecTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link PlatformSpec}. */ +public class PlatformSpecTest { + + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + @Test + public void testPlatformSpec_full() throws JsonProcessingException { + String data = "architecture: amd64\n" + "os: linux\n"; + + PlatformSpec parsed = mapper.readValue(data, PlatformSpec.class); + Assert.assertEquals("amd64", parsed.getArchitecture()); + Assert.assertEquals("linux", parsed.getOs()); + } + + @Test + public void testPlatformSpec_osRequired() { + String data = "architecture: amd64\n"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.startsWith("Missing required creator property 'os'")); + } + } + + @Test + public void testPlatformSpec_osNotNull() { + String data = "architecture: amd64\n" + "os: null"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'os' cannot be null")); + } + } + + @Test + public void testPlatformSpec_osNotEmpty() { + String data = "architecture: amd64\n" + "os: ''"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'os' cannot be an empty string")); + } + } + + @Test + public void testPlatformSpec_architectureRequired() { + String data = "os: linux\n"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.startsWith("Missing required creator property 'architecture'")); + } + } + + @Test + public void testPlatformSpec_architectureNotNull() { + String data = "architecture: null\n" + "os: linux"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), CoreMatchers.containsString("Property 'architecture' cannot be null")); + } + } + + @Test + public void testPlatformSpec_architectureNotEmpty() { + String data = "architecture: ''\n" + "os: linux"; + + try { + mapper.readValue(data, PlatformSpec.class); + Assert.fail(); + } catch (JsonProcessingException jpe) { + MatcherAssert.assertThat( + jpe.getMessage(), + CoreMatchers.containsString("Property 'architecture' cannot be an empty string")); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ValidatorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ValidatorTest.java new file mode 100644 index 0000000000..8924dadcb6 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ValidatorTest.java @@ -0,0 +1,256 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.buildfile; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Validator}. */ +public class ValidatorTest { + + @Test + public void testCheckNotNullAndNotEmpty_stringPass() { + Validator.checkNotNullAndNotEmpty("value", "ignored"); + // pass + } + + @Test + public void testCheckNotNullAndNotEmpty_stringFailNull() { + try { + Validator.checkNotNullAndNotEmpty((String) null, "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot be null", npe.getMessage()); + } + } + + @Test + public void testCheckNotNullAndNotEmpty_stringFailEmpty() { + try { + Validator.checkNotNullAndNotEmpty(" ", "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot be an empty string", iae.getMessage()); + } + } + + @Test + public void testCheckNullOrNotEmpty_valuePass() { + Validator.checkNullOrNotEmpty("value", "test"); + } + + @Test + public void testCheckNullOrNotEmpty_nullPass() { + Validator.checkNullOrNotEmpty(null, "test"); + } + + @Test + public void testCheckNullOrNotEmpty_fail() { + try { + Validator.checkNullOrNotEmpty(" ", "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot be an empty string", iae.getMessage()); + } + } + + @Test + public void testCheckNotEmpty_collectionPass() { + Validator.checkNotNullAndNotEmpty(ImmutableList.of("value"), "ignored"); + // pass + } + + @Test + public void testCheckNotEmpty_collectionFailNull() { + try { + Validator.checkNotNullAndNotEmpty((Collection) null, "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot be null", npe.getMessage()); + } + } + + @Test + public void testCheckNotEmpty_collectionFailEmpty() { + try { + Validator.checkNotNullAndNotEmpty(ImmutableList.of(), "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot be an empty collection", iae.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_nullMapPass() { + Validator.checkNullOrNonNullNonEmptyEntries((Map) null, "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_emptyMapPass() { + Validator.checkNullOrNonNullNonEmptyEntries(ImmutableMap.of(), "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_mapWithValuesPass() { + Validator.checkNullOrNonNullNonEmptyEntries( + ImmutableMap.of("key1", "val1", "key2", "val2"), "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_mapNullKeyFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(Collections.singletonMap(null, "val1"), "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot contain null keys", npe.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_mapEmptyKeyFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(Collections.singletonMap(" ", "val1"), "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot contain empty string keys", iae.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_mapNullValueFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(Collections.singletonMap("key1", null), "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot contain null values", npe.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_mapEmptyValueFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(Collections.singletonMap("key1", " "), "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot contain empty string values", iae.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_nullPass() { + Validator.checkNullOrNonNullNonEmptyEntries((List) null, "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_emptyPass() { + Validator.checkNullOrNonNullNonEmptyEntries(ImmutableList.of(), "test"); + // pass + } + + @Test + public void testCheckNullNonNullNonEmptyEntries_valuesPass() { + Validator.checkNullOrNonNullNonEmptyEntries(ImmutableList.of("first", "second"), "test"); + // pass + } + + @Test + public void testCheckNullNonNullNonEmptyEntries_nullValueFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(Arrays.asList("first", null), "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot contain null entries", npe.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullNonEmptyEntries_emptyValueFail() { + try { + Validator.checkNullOrNonNullNonEmptyEntries(ImmutableList.of("first", " "), "test"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals("Property 'test' cannot contain empty strings", iae.getMessage()); + } + } + + @Test + public void testCheckNullOrNonNullEntries_nullPass() { + Validator.checkNullOrNonNullEntries(null, "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullEntries_emptyPass() { + Validator.checkNullOrNonNullEntries(ImmutableList.of(), "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullEntries_valuesPass() { + Validator.checkNullOrNonNullEntries(ImmutableList.of(new Object(), new Object()), "test"); + // pass + } + + @Test + public void testCheckNullOrNonNullEntries_nullFail() { + try { + Validator.checkNullOrNonNullEntries(Arrays.asList(new Object(), null), "test"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot contain null entries", npe.getMessage()); + } + // pass + } + + @Test + public void testCheckEquals_pass() { + Validator.checkEquals("value", "ignored", "value"); + // pass + } + + @Test + public void testCheckEquals_failsNull() { + try { + Validator.checkEquals(null, "test", "something"); + Assert.fail(); + } catch (NullPointerException npe) { + Assert.assertEquals("Property 'test' cannot be null", npe.getMessage()); + } + } + + @Test + public void testCheckEquals_failsNotEquals() { + try { + Validator.checkEquals("somethingElse", "test", "something"); + Assert.fail(); + } catch (IllegalArgumentException iae) { + Assert.assertEquals( + "Property 'test' must be 'something' but is 'somethingElse'", iae.getMessage()); + } + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarFilesTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarFilesTest.java new file mode 100644 index 0000000000..7e7a597ef0 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarFilesTest.java @@ -0,0 +1,337 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.cli.CommonContainerConfigCliOptions; +import com.google.cloud.tools.jib.cli.Jar; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Optional; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link JarFiles}. */ +@RunWith(JUnitParamsRunner.class) +public class JarFilesTest { + + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule().silent(); + + @Mock private StandardExplodedProcessor mockStandardExplodedProcessor; + @Mock private StandardPackagedProcessor mockStandardPackagedProcessor; + @Mock private SpringBootExplodedProcessor mockSpringBootExplodedProcessor; + @Mock private SpringBootPackagedProcessor mockSpringBootPackagedProcessor; + @Mock private Jar mockJarCommand; + @Mock private CommonCliOptions mockCommonCliOptions; + @Mock private CommonContainerConfigCliOptions mockCommonContainerConfigCliOptions; + @Mock private ConsoleLogger mockLogger; + + @Test + @Parameters( + value = { + "8, eclipse-temurin:8-jre", + "9, eclipse-temurin:11-jre", + "11, eclipse-temurin:11-jre", + "13, eclipse-temurin:17-jre", + "17, eclipse-temurin:17-jre", + "21, eclipse-temurin:21-jre", + "25, eclipse-temurin:25-jre", + }) + public void testToJibContainer_defaultBaseImage(int javaVersion, String expectedBaseImage) + throws IOException, InvalidImageReferenceException { + when(mockStandardExplodedProcessor.getJavaVersion()).thenReturn(javaVersion); + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockStandardExplodedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo(expectedBaseImage); + } + + @Test + public void testToJibContainerBuilder_explodedStandard_basicInfo() + throws IOException, InvalidImageReferenceException { + when(mockStandardExplodedProcessor.getJavaVersion()).thenReturn(8); + FileEntriesLayer layer = + FileEntriesLayer.builder() + .setName("classes") + .addEntry( + Paths.get("path/to/tempDirectory/class1.class"), + AbsoluteUnixPath.get("/app/explodedJar/class1.class")) + .build(); + when(mockStandardExplodedProcessor.createLayers()).thenReturn(Arrays.asList(layer)); + when(mockStandardExplodedProcessor.computeEntrypoint(anyList())) + .thenReturn( + ImmutableList.of("java", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld")); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.empty()); + + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockStandardExplodedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("eclipse-temurin:8-jre"); + assertThat(buildPlan.getPlatforms()).isEqualTo(ImmutableSet.of(new Platform("amd64", "linux"))); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.EPOCH); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.Docker); + assertThat(buildPlan.getEnvironment()).isEmpty(); + assertThat(buildPlan.getLabels()).isEmpty(); + assertThat(buildPlan.getVolumes()).isEmpty(); + assertThat(buildPlan.getExposedPorts()).isEmpty(); + assertThat(buildPlan.getUser()).isNull(); + assertThat(buildPlan.getWorkingDirectory()).isNull(); + assertThat(buildPlan.getEntrypoint()) + .containsExactly("java", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld") + .inOrder(); + assertThat(buildPlan.getLayers()).hasSize(1); + assertThat(buildPlan.getLayers().get(0).getName()).isEqualTo("classes"); + assertThat(((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()) + .containsExactlyElementsIn( + FileEntriesLayer.builder() + .addEntry( + Paths.get("path/to/tempDirectory/class1.class"), + AbsoluteUnixPath.get("/app/explodedJar/class1.class")) + .build() + .getEntries()); + } + + @Test + public void testToJibContainerBuilder_packagedStandard_basicInfo() + throws IOException, InvalidImageReferenceException { + when(mockStandardPackagedProcessor.getJavaVersion()).thenReturn(8); + FileEntriesLayer layer = + FileEntriesLayer.builder() + .setName("jar") + .addEntry( + Paths.get("path/to/standardJar.jar"), AbsoluteUnixPath.get("/app/standardJar.jar")) + .build(); + when(mockStandardPackagedProcessor.createLayers()).thenReturn(Arrays.asList(layer)); + when(mockStandardPackagedProcessor.computeEntrypoint(anyList())) + .thenReturn(ImmutableList.of("java", "-jar", "/app/standardJar.jar")); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.empty()); + + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockStandardPackagedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("eclipse-temurin:8-jre"); + assertThat(buildPlan.getPlatforms()).isEqualTo(ImmutableSet.of(new Platform("amd64", "linux"))); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.EPOCH); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.Docker); + assertThat(buildPlan.getEnvironment()).isEmpty(); + assertThat(buildPlan.getLabels()).isEmpty(); + assertThat(buildPlan.getVolumes()).isEmpty(); + assertThat(buildPlan.getExposedPorts()).isEmpty(); + assertThat(buildPlan.getUser()).isNull(); + assertThat(buildPlan.getWorkingDirectory()).isNull(); + assertThat(buildPlan.getEntrypoint()) + .containsExactly("java", "-jar", "/app/standardJar.jar") + .inOrder(); + assertThat(buildPlan.getLayers().get(0).getName()).isEqualTo("jar"); + assertThat(((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()) + .isEqualTo( + FileEntriesLayer.builder() + .addEntry( + Paths.get("path/to/standardJar.jar"), + AbsoluteUnixPath.get("/app/standardJar.jar")) + .build() + .getEntries()); + } + + @Test + public void testToJibContainerBuilder_explodedLayeredSpringBoot_basicInfo() + throws IOException, InvalidImageReferenceException { + when(mockSpringBootExplodedProcessor.getJavaVersion()).thenReturn(8); + FileEntriesLayer layer = + FileEntriesLayer.builder() + .setName("classes") + .addEntry( + Paths.get("path/to/tempDirectory/BOOT-INF/classes/class1.class"), + AbsoluteUnixPath.get("/app/BOOT-INF/classes/class1.class")) + .build(); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.empty()); + when(mockSpringBootExplodedProcessor.createLayers()).thenReturn(Arrays.asList(layer)); + when(mockSpringBootExplodedProcessor.computeEntrypoint(anyList())) + .thenReturn( + ImmutableList.of("java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher")); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.empty()); + + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockSpringBootExplodedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("eclipse-temurin:8-jre"); + assertThat(buildPlan.getPlatforms()).isEqualTo(ImmutableSet.of(new Platform("amd64", "linux"))); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.EPOCH); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.Docker); + assertThat(buildPlan.getEnvironment()).isEmpty(); + assertThat(buildPlan.getLabels()).isEmpty(); + assertThat(buildPlan.getVolumes()).isEmpty(); + assertThat(buildPlan.getExposedPorts()).isEmpty(); + assertThat(buildPlan.getUser()).isNull(); + assertThat(buildPlan.getWorkingDirectory()).isNull(); + assertThat(buildPlan.getEntrypoint()) + .containsExactly("java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher") + .inOrder(); + assertThat(buildPlan.getLayers()).hasSize(1); + assertThat(buildPlan.getLayers().get(0).getName()).isEqualTo("classes"); + assertThat(((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()) + .containsExactlyElementsIn( + FileEntriesLayer.builder() + .addEntry( + Paths.get("path/to/tempDirectory/BOOT-INF/classes/class1.class"), + AbsoluteUnixPath.get("/app/BOOT-INF/classes/class1.class")) + .build() + .getEntries()); + } + + @Test + public void testToJibContainerBuilder_packagedSpringBoot_basicInfo() + throws IOException, InvalidImageReferenceException { + when(mockSpringBootPackagedProcessor.getJavaVersion()).thenReturn(8); + FileEntriesLayer layer = + FileEntriesLayer.builder() + .setName("jar") + .addEntry( + Paths.get("path/to/spring-boot.jar"), AbsoluteUnixPath.get("/app/spring-boot.jar")) + .build(); + when(mockSpringBootPackagedProcessor.createLayers()).thenReturn(Arrays.asList(layer)); + when(mockSpringBootPackagedProcessor.computeEntrypoint(anyList())) + .thenReturn(ImmutableList.of("java", "-jar", "/app/spring-boot.jar")); + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.empty()); + + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockSpringBootPackagedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("eclipse-temurin:8-jre"); + assertThat(buildPlan.getPlatforms()).isEqualTo(ImmutableSet.of(new Platform("amd64", "linux"))); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.EPOCH); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.Docker); + assertThat(buildPlan.getEnvironment()).isEmpty(); + assertThat(buildPlan.getLabels()).isEmpty(); + assertThat(buildPlan.getVolumes()).isEmpty(); + assertThat(buildPlan.getExposedPorts()).isEmpty(); + assertThat(buildPlan.getUser()).isNull(); + assertThat(buildPlan.getWorkingDirectory()).isNull(); + assertThat(buildPlan.getEntrypoint()) + .containsExactly("java", "-jar", "/app/spring-boot.jar") + .inOrder(); + assertThat(buildPlan.getLayers()).hasSize(1); + assertThat(buildPlan.getLayers().get(0).getName()).isEqualTo("jar"); + assertThat(((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()) + .isEqualTo( + FileEntriesLayer.builder() + .addEntry( + Paths.get("path/to/spring-boot.jar"), + AbsoluteUnixPath.get("/app/spring-boot.jar")) + .build() + .getEntries()); + } + + @Test + public void testToJibContainerBuilder_optionalParameters() + throws IOException, InvalidImageReferenceException { + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.of("base-image")); + when(mockCommonContainerConfigCliOptions.getExposedPorts()) + .thenReturn(ImmutableSet.of(Port.udp(123))); + when(mockCommonContainerConfigCliOptions.getVolumes()) + .thenReturn( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + when(mockCommonContainerConfigCliOptions.getEnvironment()) + .thenReturn(ImmutableMap.of("key1", "value1")); + when(mockCommonContainerConfigCliOptions.getLabels()) + .thenReturn(ImmutableMap.of("label", "mylabel")); + when(mockCommonContainerConfigCliOptions.getUser()).thenReturn(Optional.of("customUser")); + when(mockCommonContainerConfigCliOptions.getFormat()).thenReturn(Optional.of(ImageFormat.OCI)); + when(mockCommonContainerConfigCliOptions.getProgramArguments()) + .thenReturn(ImmutableList.of("arg1")); + when(mockCommonContainerConfigCliOptions.getEntrypoint()) + .thenReturn(ImmutableList.of("custom", "entrypoint")); + when(mockCommonContainerConfigCliOptions.getCreationTime()) + .thenReturn(Optional.of(Instant.ofEpochSecond(5))); + + JibContainerBuilder containerBuilder = + JarFiles.toJibContainerBuilder( + mockStandardExplodedProcessor, + mockJarCommand, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("base-image"); + assertThat(buildPlan.getExposedPorts()).isEqualTo(ImmutableSet.of(Port.udp(123))); + assertThat(buildPlan.getVolumes()) + .isEqualTo( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + assertThat(buildPlan.getEnvironment()).isEqualTo(ImmutableMap.of("key1", "value1")); + assertThat(buildPlan.getLabels()).isEqualTo(ImmutableMap.of("label", "mylabel")); + assertThat(buildPlan.getUser()).isEqualTo("customUser"); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.OCI); + assertThat(buildPlan.getCmd()).isEqualTo(ImmutableList.of("arg1")); + assertThat(buildPlan.getEntrypoint()).isEqualTo(ImmutableList.of("custom", "entrypoint")); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.ofEpochSecond(5)); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessorTest.java new file mode 100644 index 0000000000..7440739fda --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessorTest.java @@ -0,0 +1,271 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link SpringBootExplodedProcessor}. */ +public class SpringBootExplodedProcessorTest { + + private static final String SPRING_BOOT_LAYERED = "jar/spring-boot/springboot_layered.jar"; + private static final String SPRING_BOOT_LAYERED_WITH_EMPTY_LAYER = + "jar/spring-boot/springboot_layered_singleEmptyLayer.jar"; + private static final String SPRING_BOOT_LAYERED_WITH_ALL_EMPTY_LAYERS_LISTED = + "jar/spring-boot/springboot_layered_allEmptyLayers.jar"; + private static final String SPRING_BOOT_NOT_LAYERED = "jar/spring-boot/springboot_notLayered.jar"; + private static final Integer JAR_JAVA_VERSION = 0; // any value + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testCreateLayers_layered_allListed() throws IOException, URISyntaxException { + // BOOT-INF/layers.idx for this spring-boot jar is as follows: + // - "dependencies": + // - "BOOT-INF/lib/dependency1.jar" + // - "BOOT-INF/lib/dependency2.jar" + // - "spring-boot-loader": + // - "org/" + // - "snapshot-dependencies": + // - "BOOT-INF/lib/dependency-SNAPSHOT-3.jar" + // - "application": + // - "BOOT-INF/classes/" + // - "META-INF/" + Path springBootJar = Paths.get(Resources.getResource(SPRING_BOOT_LAYERED).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + SpringBootExplodedProcessor springBootExplodedModeProcessor = + new SpringBootExplodedProcessor(springBootJar, destDir, JAR_JAVA_VERSION); + + List layers = springBootExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(4); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer loaderLayer = layers.get(1); + FileEntriesLayer snapshotLayer = layers.get(2); + FileEntriesLayer applicationLayer = layers.get(3); + + assertThat(nonSnapshotLayer.getName()).isEqualTo("dependencies"); + assertThat( + nonSnapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency1.jar"), + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency2.jar")); + + assertThat(loaderLayer.getName()).isEqualTo("spring-boot-loader"); + assertThat( + loaderLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/data/data1.class"), + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/launcher1.class")); + + assertThat(snapshotLayer.getName()).isEqualTo("snapshot-dependencies"); + assertThat( + snapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly(AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency3-SNAPSHOT.jar")); + + assertThat(applicationLayer.getName()).isEqualTo("application"); + assertThat( + applicationLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/classes/class1.class"), + AbsoluteUnixPath.get("/app/BOOT-INF/classes/classDirectory/class2.class"), + AbsoluteUnixPath.get("/app/META-INF/MANIFEST.MF")); + } + + @Test + public void testCreateLayers_layered_singleEmptyLayerListed() + throws IOException, URISyntaxException { + // BOOT-INF/layers.idx for this spring-boot jar is as follows: + // - "dependencies": + // - "BOOT-INF/lib/dependency1.jar" + // - "BOOT-INF/lib/dependency2.jar" + // - "spring-boot-loader": + // - "org/" + // - "snapshot-dependencies": + // - "application": + // - "BOOT-INF/classes/" + // - "META-INF/" + Path springBootJar = + Paths.get(Resources.getResource(SPRING_BOOT_LAYERED_WITH_EMPTY_LAYER).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + SpringBootExplodedProcessor springBootExplodedModeProcessor = + new SpringBootExplodedProcessor(springBootJar, destDir, JAR_JAVA_VERSION); + + List layers = springBootExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(3); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer loaderLayer = layers.get(1); + FileEntriesLayer applicationLayer = layers.get(2); + + assertThat(nonSnapshotLayer.getName()).isEqualTo("dependencies"); + assertThat( + nonSnapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency1.jar"), + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency2.jar")); + + assertThat(loaderLayer.getName()).isEqualTo("spring-boot-loader"); + assertThat( + loaderLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/data/data1.class"), + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/launcher1.class")); + + assertThat(applicationLayer.getName()).isEqualTo("application"); + assertThat( + applicationLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/classes/class1.class"), + AbsoluteUnixPath.get("/app/BOOT-INF/classes/classDirectory/class2.class"), + AbsoluteUnixPath.get("/app/META-INF/MANIFEST.MF")); + } + + @Test + public void testCreateLayers_layered_allEmptyLayersListed() + throws IOException, URISyntaxException { + // BOOT-INF/layers.idx for this spring-boot jar is as follows: + // - "dependencies": + // - "spring-boot-loader": + // - "snapshot-dependencies": + // - "application": + Path springBootJar = + Paths.get(Resources.getResource(SPRING_BOOT_LAYERED_WITH_ALL_EMPTY_LAYERS_LISTED).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + SpringBootExplodedProcessor springBootExplodedModeProcessor = + new SpringBootExplodedProcessor(springBootJar, destDir, JAR_JAVA_VERSION); + + List layers = springBootExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(0); + } + + @Test + public void testCreateLayers_nonLayered() throws IOException, URISyntaxException { + Path springBootJar = Paths.get(Resources.getResource(SPRING_BOOT_NOT_LAYERED).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + SpringBootExplodedProcessor springBootExplodedModeProcessor = + new SpringBootExplodedProcessor(springBootJar, destDir, JAR_JAVA_VERSION); + + List layers = springBootExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(5); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer loaderLayer = layers.get(1); + FileEntriesLayer snapshotLayer = layers.get(2); + FileEntriesLayer resourcesLayer = layers.get(3); + FileEntriesLayer classesLayer = layers.get(4); + + assertThat(nonSnapshotLayer.getName()).isEqualTo("dependencies"); + assertThat( + nonSnapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency1.jar"), + AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency2.jar")); + + assertThat(loaderLayer.getName()).isEqualTo("spring-boot-loader"); + assertThat( + loaderLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/data/data1.class"), + AbsoluteUnixPath.get("/app/org/springframework/boot/loader/launcher1.class")); + + assertThat(snapshotLayer.getName()).isEqualTo("snapshot dependencies"); + assertThat(snapshotLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/BOOT-INF/lib/dependency3-SNAPSHOT.jar")); + + assertThat(resourcesLayer.getName()).isEqualTo("resources"); + assertThat(resourcesLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/META-INF/MANIFEST.MF")); + + assertThat(classesLayer.getName()).isEqualTo("classes"); + assertThat( + classesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .containsExactly( + AbsoluteUnixPath.get("/app/BOOT-INF/classes/class1.class"), + AbsoluteUnixPath.get("/app/BOOT-INF/classes/classDirectory/class2.class")); + } + + @Test + public void testComputeEntrypoint() { + SpringBootExplodedProcessor bootProcessor = + new SpringBootExplodedProcessor( + Paths.get("ignored"), Paths.get("ignored"), JAR_JAVA_VERSION); + ImmutableList actualEntrypoint = bootProcessor.computeEntrypoint(new ArrayList<>()); + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of("java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher")); + } + + @Test + public void testComputeEntrypoint_withJvmFlags() { + SpringBootExplodedProcessor bootProcessor = + new SpringBootExplodedProcessor( + Paths.get("ignored"), Paths.get("ignored"), JAR_JAVA_VERSION); + ImmutableList actualEntrypoint = + bootProcessor.computeEntrypoint(ImmutableList.of("-jvm-flag")); + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of( + "java", "-jvm-flag", "-cp", "/app", "org.springframework.boot.loader.JarLauncher")); + } + + @Test + public void testGetJavaVersion() { + SpringBootExplodedProcessor springBootExplodedProcessor = + new SpringBootExplodedProcessor(Paths.get("ignore"), Paths.get("ignore"), 8); + assertThat(springBootExplodedProcessor.getJavaVersion()).isEqualTo(8); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessorTest.java new file mode 100644 index 0000000000..92358992bf --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessorTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.Test; + +/** Tests for {@link SpringBootPackagedProcessor}. */ +public class SpringBootPackagedProcessorTest { + + private static final String SPRING_BOOT_JAR = "jar/spring-boot/springboot_sample.jar"; + private static final Integer JAR_JAVA_VERSION = 0; // any value + + @Test + public void testCreateLayers() throws URISyntaxException { + Path springBootJar = Paths.get(Resources.getResource(SPRING_BOOT_JAR).toURI()); + SpringBootPackagedProcessor springBootProcessor = + new SpringBootPackagedProcessor(springBootJar, JAR_JAVA_VERSION); + + List layers = springBootProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(1); + + FileEntriesLayer jarLayer = layers.get(0); + + assertThat(jarLayer.getName()).isEqualTo("jar"); + assertThat(jarLayer.getEntries().size()).isEqualTo(1); + assertThat(jarLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/springboot_sample.jar")); + } + + @Test + public void testComputeEntrypoint() throws URISyntaxException { + Path springBootJar = Paths.get(Resources.getResource(SPRING_BOOT_JAR).toURI()); + SpringBootPackagedProcessor springBootProcessor = + new SpringBootPackagedProcessor(springBootJar, JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + springBootProcessor.computeEntrypoint(ImmutableList.of()); + + assertThat(actualEntrypoint) + .isEqualTo(ImmutableList.of("java", "-jar", "/app/springboot_sample.jar")); + } + + @Test + public void testComputeEntrypoint_jvmFlag() throws URISyntaxException { + Path springBootJar = Paths.get(Resources.getResource(SPRING_BOOT_JAR).toURI()); + SpringBootPackagedProcessor springBootProcessor = + new SpringBootPackagedProcessor(springBootJar, JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + springBootProcessor.computeEntrypoint(ImmutableList.of("-jvm-flag")); + + assertThat(actualEntrypoint) + .isEqualTo(ImmutableList.of("java", "-jvm-flag", "-jar", "/app/springboot_sample.jar")); + } + + @Test + public void testGetJavaVersion() { + SpringBootPackagedProcessor springBootPackagedProcessor = + new SpringBootPackagedProcessor(Paths.get("ignore"), 8); + assertThat(springBootPackagedProcessor.getJavaVersion()).isEqualTo(8); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessorTest.java new file mode 100644 index 0000000000..65cd04b2a8 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessorTest.java @@ -0,0 +1,281 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link StandardExplodedProcessor}. */ +public class StandardExplodedProcessorTest { + + private static final String STANDARD_JAR_WITHOUT_CLASS_PATH_MANIFEST = + "jar/standard/standardJarWithoutClassPath.jar"; + private static final String STANDARD_JAR_WITH_CLASS_PATH_MANIFEST = + "jar/standard/standardJarWithClassPath.jar"; + private static final String STANDARD_JAR_WITH_ONLY_CLASSES = + "jar/standard/standardJarWithOnlyClasses.jar"; + private static final String STANDARD_JAR_EMPTY = "jar/standard/emptyStandardJar.jar"; + private static final String STANDARD_SINGLE_DEPENDENCY_JAR = "jar/standard/singleDepJar.jar"; + private static final Integer JAR_JAVA_VERSION = 0; // any value + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testCreateLayers_emptyJar() throws IOException, URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_EMPTY).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, destDir, JAR_JAVA_VERSION); + + List layers = standardExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(1); + + FileEntriesLayer resourcesLayer = layers.get(0); + + assertThat(resourcesLayer.getEntries().size()).isEqualTo(1); + assertThat(resourcesLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/explodedJar/META-INF/MANIFEST.MF")); + } + + @Test + public void testCreateLayers_withClassPathInManifest() throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, destDir, JAR_JAVA_VERSION); + + List layers = standardExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(4); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer snapshotLayer = layers.get(1); + FileEntriesLayer resourcesLayer = layers.get(2); + FileEntriesLayer classesLayer = layers.get(3); + + // Validate dependencies layers. + assertThat(nonSnapshotLayer.getName()).isEqualTo("dependencies"); + assertThat( + nonSnapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .isEqualTo( + ImmutableList.of( + AbsoluteUnixPath.get("/app/dependencies/dependency1"), + AbsoluteUnixPath.get("/app/dependencies/dependency2"), + AbsoluteUnixPath.get("/app/dependencies/dependency4"))); + assertThat(snapshotLayer.getName()).isEqualTo("snapshot dependencies"); + assertThat(snapshotLayer.getEntries().size()).isEqualTo(1); + assertThat(snapshotLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/dependencies/dependency3-SNAPSHOT-1.jar")); + + // Validate resources layer. + assertThat(resourcesLayer.getName()).isEqualTo("resources"); + List actualResourcesPaths = + resourcesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList()); + assertThat(actualResourcesPaths) + .containsExactly( + AbsoluteUnixPath.get("/app/explodedJar/META-INF/MANIFEST.MF"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/resource1.txt"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/directory3/resource2.sql"), + AbsoluteUnixPath.get("/app/explodedJar/directory4/resource3.txt"), + AbsoluteUnixPath.get("/app/explodedJar/resource4.sql")); + + // Validate classes layer. + assertThat(classesLayer.getName()).isEqualTo("classes"); + List actualClassesPaths = + classesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList()); + assertThat(actualClassesPaths) + .containsExactly( + AbsoluteUnixPath.get("/app/explodedJar/class5.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/class1.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/class2.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/class4.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/directory3/class3.class")); + } + + @Test + public void testCreateLayers_withoutClassPathInManifest() throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITHOUT_CLASS_PATH_MANIFEST).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, destDir, JAR_JAVA_VERSION); + + List layers = standardExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(2); + + FileEntriesLayer resourcesLayer = layers.get(0); + FileEntriesLayer classesLayer = layers.get(1); + + // Validate resources layer. + assertThat(resourcesLayer.getName()).isEqualTo("resources"); + List actualResourcesPaths = + resourcesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList()); + assertThat(actualResourcesPaths) + .containsExactly( + AbsoluteUnixPath.get("/app/explodedJar/META-INF/MANIFEST.MF"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/resource1.txt"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/directory3/resource2.sql"), + AbsoluteUnixPath.get("/app/explodedJar/directory4/resource3.txt"), + AbsoluteUnixPath.get("/app/explodedJar/resource4.sql")); + + // Validate classes layer. + assertThat(classesLayer.getName()).isEqualTo("classes"); + List actualClassesPaths = + classesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList()); + assertThat(actualClassesPaths) + .containsExactly( + AbsoluteUnixPath.get("/app/explodedJar/class5.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/class1.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory1/class2.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/class4.class"), + AbsoluteUnixPath.get("/app/explodedJar/directory2/directory3/class3.class")); + } + + @Test + public void testCreateLayers_withoutClassPathInManifest_containsOnlyClasses() + throws IOException, URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_WITH_ONLY_CLASSES).toURI()); + Path destDir = temporaryFolder.newFolder().toPath(); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, destDir, JAR_JAVA_VERSION); + + List layers = standardExplodedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(2); + + FileEntriesLayer resourcesLayer = layers.get(0); + FileEntriesLayer classesLayer = layers.get(1); + + // Validate resources layer. + assertThat(resourcesLayer.getEntries().size()).isEqualTo(1); + assertThat(resourcesLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/explodedJar/META-INF/MANIFEST.MF")); + + // Validate classes layer. + List actualClassesPath = + classesLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList()); + assertThat(actualClassesPath) + .containsExactly( + AbsoluteUnixPath.get("/app/explodedJar/class1.class"), + AbsoluteUnixPath.get("/app/explodedJar/class2.class")); + } + + @Test + public void testCreateLayers_dependencyDoesNotExist() throws URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_SINGLE_DEPENDENCY_JAR).toURI()); + Path destDir = temporaryFolder.getRoot().toPath(); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, destDir, JAR_JAVA_VERSION); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> standardExplodedModeProcessor.createLayers()); + + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "Dependency required by the JAR (as specified in `Class-Path` in the JAR manifest) doesn't exist: " + + standardJar.getParent().resolve("dependency.jar")); + } + + @Test + public void testComputeEntrypoint_noMainClass() throws URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_EMPTY).toURI()); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, Paths.get("ignore"), JAR_JAVA_VERSION); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> standardExplodedModeProcessor.computeEntrypoint(ImmutableList.of())); + + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "`Main-Class:` attribute for an application main class not defined in the input JAR's manifest " + + "(`META-INF/MANIFEST.MF` in the JAR)."); + } + + @Test + public void testComputeEntrypoint_withMainClass() throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, Paths.get("ignore"), JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + standardExplodedModeProcessor.computeEntrypoint(ImmutableList.of()); + + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of("java", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld")); + } + + @Test + public void testComputeEntrypoint_withMainClass_jvmFlags() + throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + StandardExplodedProcessor standardExplodedModeProcessor = + new StandardExplodedProcessor(standardJar, Paths.get("ignore"), JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + standardExplodedModeProcessor.computeEntrypoint(ImmutableList.of("-jvm-flag")); + + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of( + "java", "-jvm-flag", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld")); + } + + @Test + public void testGetJavaVersion() { + StandardExplodedProcessor standardExplodedProcessor = + new StandardExplodedProcessor(Paths.get("ignore"), Paths.get("ignore"), 8); + assertThat(standardExplodedProcessor.getJavaVersion()).isEqualTo(8); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessorTest.java new file mode 100644 index 0000000000..47a43e0cc1 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessorTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.jar; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; + +/** Tests for {@link StandardPackagedProcessor}. */ +public class StandardPackagedProcessorTest { + + private static final String STANDARD_JAR_EMPTY = "jar/standard/emptyStandardJar.jar"; + private static final String STANDARD_SINGLE_DEPENDENCY_JAR = "jar/standard/singleDepJar.jar"; + private static final String STANDARD_JAR_WITH_CLASS_PATH_MANIFEST = + "jar/standard/standardJarWithClassPath.jar"; + private static final Integer JAR_JAVA_VERSION = 0; // any value + + @Test + public void testCreateLayers_emptyJar() throws IOException, URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_EMPTY).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + List layers = standardPackagedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(1); + + FileEntriesLayer jarLayer = layers.get(0); + assertThat(jarLayer.getName()).isEqualTo("jar"); + assertThat(jarLayer.getEntries().size()).isEqualTo(1); + assertThat(jarLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/emptyStandardJar.jar")); + } + + @Test + public void testCreateLayers_withClassPathInManifest() throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + List layers = standardPackagedModeProcessor.createLayers(); + + assertThat(layers.size()).isEqualTo(3); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer snapshotLayer = layers.get(1); + FileEntriesLayer jarLayer = layers.get(2); + + // Validate dependencies layers. + assertThat(nonSnapshotLayer.getName()).isEqualTo("dependencies"); + assertThat( + nonSnapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .isEqualTo( + ImmutableList.of( + AbsoluteUnixPath.get("/app/dependency1"), + AbsoluteUnixPath.get("/app/dependency2"), + AbsoluteUnixPath.get("/app/directory/dependency4"))); + assertThat(snapshotLayer.getName()).isEqualTo("snapshot dependencies"); + assertThat( + snapshotLayer.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .isEqualTo(ImmutableList.of(AbsoluteUnixPath.get("/app/dependency3-SNAPSHOT-1.jar"))); + + // Validate jar layer. + assertThat(jarLayer.getName()).isEqualTo("jar"); + assertThat(jarLayer.getEntries().size()).isEqualTo(1); + assertThat(jarLayer.getEntries().get(0).getExtractionPath()) + .isEqualTo(AbsoluteUnixPath.get("/app/standardJarWithClassPath.jar")); + } + + @Test + public void testCreateLayers_dependencyDoesNotExist() throws URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_SINGLE_DEPENDENCY_JAR).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> standardPackagedModeProcessor.createLayers()); + + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "Dependency required by the JAR (as specified in `Class-Path` in the JAR manifest) doesn't exist: " + + standardJar.getParent().resolve("dependency.jar")); + } + + @Test + public void testComputeEntrypoint_noMainClass() throws URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_EMPTY).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> standardPackagedModeProcessor.computeEntrypoint(ImmutableList.of())); + + assertThat(exception) + .hasMessageThat() + .isEqualTo( + "`Main-Class:` attribute for an application main class not defined in the input JAR's manifest " + + "(`META-INF/MANIFEST.MF` in the JAR)."); + } + + @Test + public void testComputeEntrypoint_withMainClass() throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + standardPackagedModeProcessor.computeEntrypoint(ImmutableList.of()); + + assertThat(actualEntrypoint) + .isEqualTo(ImmutableList.of("java", "-jar", "/app/standardJarWithClassPath.jar")); + } + + @Test + public void testComputeEntrypoint_withMainClass_jvmFlags() + throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + StandardPackagedProcessor standardPackagedModeProcessor = + new StandardPackagedProcessor(standardJar, JAR_JAVA_VERSION); + + ImmutableList actualEntrypoint = + standardPackagedModeProcessor.computeEntrypoint(ImmutableList.of("-jvm-flag")); + + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of("java", "-jvm-flag", "-jar", "/app/standardJarWithClassPath.jar")); + } + + @Test + public void testGetJavaVersion() { + StandardPackagedProcessor standardPackagedProcessor = + new StandardPackagedProcessor(Paths.get("ignore"), 8); + assertThat(standardPackagedProcessor.getJavaVersion()).isEqualTo(8); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/logging/CliLoggerTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/logging/CliLoggerTest.java new file mode 100644 index 0000000000..596cf2f522 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/logging/CliLoggerTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.logging; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.LogEvent.Level; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import java.io.PrintWriter; +import java.time.Duration; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link CliLogger}. */ +@RunWith(MockitoJUnitRunner.class) +public class CliLoggerTest { + + @Rule + public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties(); + + @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @Mock private PrintWriter mockOut; + @Mock private PrintWriter mockErr; + + private void createLoggerAndSendMessages(Verbosity verbosity, ConsoleOutput consoleOutput) { + SingleThreadedExecutor executor = new SingleThreadedExecutor(); + ConsoleLogger logger = + CliLogger.newLogger( + verbosity, HttpTraceLevel.off, consoleOutput, mockOut, mockErr, executor); + + logger.log(Level.DEBUG, "debug"); + logger.log(Level.INFO, "info"); + logger.log(Level.LIFECYCLE, "lifecycle"); + logger.log(Level.PROGRESS, "progress"); + logger.log(Level.WARN, "warn"); + logger.log(Level.ERROR, "error"); + + executor.shutDownAndAwaitTermination(Duration.ofSeconds(3)); + } + + @Test + public void testLog_quiet_plainConsole() { + createLoggerAndSendMessages(Verbosity.quiet, ConsoleOutput.plain); + + Mockito.verifyNoInteractions(mockOut); + Mockito.verifyNoInteractions(mockErr); + } + + @Test + public void testLog_error_plainConsole() { + createLoggerAndSendMessages(Verbosity.error, ConsoleOutput.plain); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verifyNoInteractions(mockOut); + } + + @Test + public void testLog_warn_plainConsole() { + createLoggerAndSendMessages(Verbosity.warn, ConsoleOutput.plain); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_lifecycle_plainConsole() { + createLoggerAndSendMessages(Verbosity.lifecycle, ConsoleOutput.plain); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verify(mockOut).println("progress"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_info_plainConsole() { + createLoggerAndSendMessages(Verbosity.info, ConsoleOutput.plain); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verify(mockOut).println("progress"); + Mockito.verify(mockOut).println("info"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_debug_plainConsole() { + createLoggerAndSendMessages(Verbosity.debug, ConsoleOutput.plain); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verify(mockOut).println("progress"); + Mockito.verify(mockOut).println("info"); + Mockito.verify(mockOut).println("debug"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_quiet_richConsole() { + createLoggerAndSendMessages(Verbosity.quiet, ConsoleOutput.rich); + + Mockito.verifyNoInteractions(mockOut); + Mockito.verifyNoInteractions(mockErr); + } + + @Test + public void testLog_error_richConsole() { + createLoggerAndSendMessages(Verbosity.error, ConsoleOutput.rich); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verifyNoInteractions(mockOut); + } + + @Test + public void testLog_warn_richConsole() { + createLoggerAndSendMessages(Verbosity.warn, ConsoleOutput.rich); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_lifecycle_richConsole() { + createLoggerAndSendMessages(Verbosity.lifecycle, ConsoleOutput.rich); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_info_richConsole() { + createLoggerAndSendMessages(Verbosity.info, ConsoleOutput.rich); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verify(mockOut).println("info"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testLog_debug_richConsole() { + createLoggerAndSendMessages(Verbosity.debug, ConsoleOutput.rich); + + Mockito.verify(mockErr).println("[ERROR] error"); + Mockito.verifyNoMoreInteractions(mockErr); + Mockito.verify(mockOut).println("[WARN] warn"); + Mockito.verify(mockOut).println("lifecycle"); + Mockito.verify(mockOut).println("info"); + Mockito.verify(mockOut).println("debug"); + Mockito.verifyNoMoreInteractions(mockOut); + } + + @Test + public void testIsRichConsole_true() { + assertThat(CliLogger.isRichConsole(ConsoleOutput.rich, HttpTraceLevel.off)).isTrue(); + } + + @Test + public void testIsRichConsole_falseIfHttpTrace() { + assertThat(CliLogger.isRichConsole(ConsoleOutput.rich, HttpTraceLevel.config)).isFalse(); + } + + @Test + public void testIsRichConsole_false() { + assertThat(CliLogger.isRichConsole(ConsoleOutput.plain, HttpTraceLevel.off)).isFalse(); + } + + @Test + public void testIsRightConsole_autoWindowsTrue() { + System.setProperty("os.name", "windows"); + assertThat(CliLogger.isRichConsole(ConsoleOutput.auto, HttpTraceLevel.off)).isTrue(); + } + + @Test + public void testIsRightConsole_autoDumbTermFalse() { + environmentVariables.set("TERM", "dumb"); + assertThat(CliLogger.isRichConsole(ConsoleOutput.auto, HttpTraceLevel.off)).isFalse(); + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessorTest.java new file mode 100644 index 0000000000..e18b5ff487 --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessorTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.war; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import com.google.common.io.Resources; +import com.google.common.truth.Correspondence; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.StringJoiner; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class StandardWarExplodedProcessorTest { + + private static final AbsoluteUnixPath APP_ROOT = AbsoluteUnixPath.get("/my/app"); + private static final Correspondence EXTRACTION_PATH_OF = + Correspondence.transforming( + entry -> entry.getExtractionPath().toString(), "has extractionPath of"); + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testCreateLayers_allLayers_correctExtractionPaths() + throws IOException, URISyntaxException { + // Prepare war file for test + Path tempDirectory = temporaryFolder.getRoot().toPath(); + Path warContents = Paths.get(Resources.getResource("war/standard/allLayers").toURI()); + Path standardWar = zipUpDirectory(warContents, tempDirectory.resolve("standardWar.war")); + + Path explodedWarDestination = temporaryFolder.newFolder("exploded-war").toPath(); + StandardWarExplodedProcessor processor = + new StandardWarExplodedProcessor(standardWar, explodedWarDestination, APP_ROOT); + List layers = processor.createLayers(); + + assertThat(layers.size()).isEqualTo(4); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer snapshotLayer = layers.get(1); + FileEntriesLayer resourcesLayer = layers.get(2); + FileEntriesLayer classesLayer = layers.get(3); + + assertThat(nonSnapshotLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependency-1.0.0.jar"); + assertThat(snapshotLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"); + assertThat(resourcesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF/web.xml", + "/my/app/WEB-INF/classes/package/test.properties"); + assertThat(classesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/WEB-INF/classes/MyClass2.class", + "/my/app/WEB-INF/classes/package/MyClass.class"); + } + + @Test + public void testCreateLayers_webInfLibDoesNotExist_correctExtractionPaths() + throws IOException, URISyntaxException { + // Prepare war file for test + Path tempDirectory = temporaryFolder.getRoot().toPath(); + Path warContents = Paths.get(Resources.getResource("war/standard/noWebInfLib").toURI()); + Path standardWar = zipUpDirectory(warContents, tempDirectory.resolve("noDependenciesWar.war")); + + Path explodedWarDestination = temporaryFolder.newFolder("exploded-war").toPath(); + StandardWarExplodedProcessor processor = + new StandardWarExplodedProcessor(standardWar, explodedWarDestination, APP_ROOT); + List layers = processor.createLayers(); + + assertThat(layers.size()).isEqualTo(2); + + FileEntriesLayer resourcesLayer = layers.get(0); + FileEntriesLayer classesLayer = layers.get(1); + + assertThat(resourcesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/META-INF/context.xml"); + assertThat(classesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/WEB-INF/classes/MyClass2.class", + "/my/app/WEB-INF/classes/package/MyClass.class"); + } + + @Test + public void testCreateLayers_webInfClassesDoesNotExist_correctExtractionPaths() + throws IOException, URISyntaxException { + // Prepare war file for test + Path tempDirectory = temporaryFolder.getRoot().toPath(); + Path warContents = Paths.get(Resources.getResource("war/standard/noWebInfClasses").toURI()); + Path standardWar = zipUpDirectory(warContents, tempDirectory.resolve("noClassesWar.war")); + + Path explodedWarDestination = temporaryFolder.newFolder("exploded-war").toPath(); + StandardWarExplodedProcessor processor = + new StandardWarExplodedProcessor(standardWar, explodedWarDestination, APP_ROOT); + List layers = processor.createLayers(); + + assertThat(layers.size()).isEqualTo(3); + + FileEntriesLayer nonSnapshotLayer = layers.get(0); + FileEntriesLayer snapshotLayer = layers.get(1); + FileEntriesLayer resourcesLayer = layers.get(2); + + assertThat(nonSnapshotLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependency-1.0.0.jar"); + assertThat(snapshotLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"); + assertThat(resourcesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/META-INF/context.xml"); + } + + @Test + public void testComputeEntrypoint() { + StandardWarExplodedProcessor processor = + new StandardWarExplodedProcessor(Paths.get("ignore"), Paths.get("ignore"), APP_ROOT); + UnsupportedOperationException exception = + assertThrows( + UnsupportedOperationException.class, + () -> processor.computeEntrypoint(ImmutableList.of())); + assertThat(exception) + .hasMessageThat() + .isEqualTo("Computing the entrypoint is currently not supported."); + } + + @Test + public void testGetJavaVersion() { + StandardWarExplodedProcessor processor = + new StandardWarExplodedProcessor(Paths.get("ignore"), Paths.get("ignore"), APP_ROOT); + UnsupportedOperationException exception = + assertThrows(UnsupportedOperationException.class, () -> processor.getJavaVersion()); + assertThat(exception) + .hasMessageThat() + .isEqualTo("Getting the java version from a WAR file is currently not supported."); + } + + private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOException { + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(targetZip))) { + for (Path source : new DirectoryWalker(sourceRoot).filterRoot().walk()) { + + StringJoiner pathJoiner = new StringJoiner("/", "", ""); + sourceRoot.relativize(source).forEach(element -> pathJoiner.add(element.toString())); + String zipEntryPath = + Files.isDirectory(source) ? pathJoiner.toString() + '/' : pathJoiner.toString(); + + ZipEntry entry = new ZipEntry(zipEntryPath); + zipOut.putNextEntry(entry); + if (!Files.isDirectory(source)) { + try (InputStream in = Files.newInputStream(source)) { + ByteStreams.copy(in, zipOut); + } + } + zipOut.closeEntry(); + } + } + return targetZip; + } +} diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/WarFilesTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/WarFilesTest.java new file mode 100644 index 0000000000..5ad6ae139f --- /dev/null +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/WarFilesTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2021 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cli.war; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.cli.CommonCliOptions; +import com.google.cloud.tools.jib.cli.CommonContainerConfigCliOptions; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link WarFiles}. */ +@RunWith(MockitoJUnitRunner.class) +public class WarFilesTest { + + @Mock private StandardWarExplodedProcessor mockStandardWarExplodedProcessor; + @Mock private CommonCliOptions mockCommonCliOptions; + @Mock private CommonContainerConfigCliOptions mockCommonContainerConfigCliOptions; + @Mock private ConsoleLogger mockLogger; + + @Test + public void testToJibContainerBuilder_explodedStandard_basicInfo() + throws IOException, InvalidImageReferenceException { + FileEntriesLayer layer = + FileEntriesLayer.builder() + .setName("classes") + .addEntry( + Paths.get("path/to/tempDirectory/WEB-INF/classes/class1.class"), + AbsoluteUnixPath.get("/my/app/WEB-INF/classes/class1.class")) + .build(); + when(mockStandardWarExplodedProcessor.createLayers()).thenReturn(Arrays.asList(layer)); + when(mockCommonContainerConfigCliOptions.isJettyBaseimage()).thenReturn(true); + + JibContainerBuilder containerBuilder = + WarFiles.toJibContainerBuilder( + mockStandardWarExplodedProcessor, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("jetty"); + assertThat(buildPlan.getEntrypoint()) + .containsExactly("java", "-jar", "/usr/local/jetty/start.jar", "--module=ee10-deploy") + .inOrder(); + assertThat(buildPlan.getLayers()).hasSize(1); + assertThat(buildPlan.getLayers().get(0).getName()).isEqualTo("classes"); + assertThat(((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()) + .containsExactlyElementsIn( + FileEntriesLayer.builder() + .addEntry( + Paths.get("path/to/tempDirectory/WEB-INF/classes/class1.class"), + AbsoluteUnixPath.get("/my/app/WEB-INF/classes/class1.class")) + .build() + .getEntries()); + } + + @Test + public void testToJibContainerBuilder_optionalParameters() + throws IOException, InvalidImageReferenceException { + when(mockCommonContainerConfigCliOptions.getFrom()).thenReturn(Optional.of("base-image")); + when(mockCommonContainerConfigCliOptions.getExposedPorts()) + .thenReturn(ImmutableSet.of(Port.udp(123))); + when(mockCommonContainerConfigCliOptions.getVolumes()) + .thenReturn( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + when(mockCommonContainerConfigCliOptions.getEnvironment()) + .thenReturn(ImmutableMap.of("key1", "value1")); + when(mockCommonContainerConfigCliOptions.getLabels()) + .thenReturn(ImmutableMap.of("label", "mylabel")); + when(mockCommonContainerConfigCliOptions.getUser()).thenReturn(Optional.of("customUser")); + when(mockCommonContainerConfigCliOptions.getFormat()).thenReturn(Optional.of(ImageFormat.OCI)); + when(mockCommonContainerConfigCliOptions.getProgramArguments()) + .thenReturn(ImmutableList.of("arg1")); + when(mockCommonContainerConfigCliOptions.getEntrypoint()) + .thenReturn(ImmutableList.of("custom", "entrypoint")); + when(mockCommonContainerConfigCliOptions.getCreationTime()) + .thenReturn(Optional.of(Instant.ofEpochSecond(5))); + + JibContainerBuilder containerBuilder = + WarFiles.toJibContainerBuilder( + mockStandardWarExplodedProcessor, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getBaseImage()).isEqualTo("base-image"); + assertThat(buildPlan.getExposedPorts()).isEqualTo(ImmutableSet.of(Port.udp(123))); + assertThat(buildPlan.getVolumes()) + .isEqualTo( + ImmutableSet.of(AbsoluteUnixPath.get("/volume1"), AbsoluteUnixPath.get("/volume2"))); + assertThat(buildPlan.getEnvironment()).isEqualTo(ImmutableMap.of("key1", "value1")); + assertThat(buildPlan.getLabels()).isEqualTo(ImmutableMap.of("label", "mylabel")); + assertThat(buildPlan.getUser()).isEqualTo("customUser"); + assertThat(buildPlan.getFormat()).isEqualTo(ImageFormat.OCI); + assertThat(buildPlan.getCmd()).isEqualTo(ImmutableList.of("arg1")); + assertThat(buildPlan.getEntrypoint()).isEqualTo(ImmutableList.of("custom", "entrypoint")); + assertThat(buildPlan.getCreationTime()).isEqualTo(Instant.ofEpochSecond(5)); + } + + @Test + public void testToJibContainerBuilder_nonJettyBaseImageSpecifiedAndNoEntrypoint() + throws IOException, InvalidImageReferenceException { + JibContainerBuilder containerBuilder = + WarFiles.toJibContainerBuilder( + mockStandardWarExplodedProcessor, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(mockCommonContainerConfigCliOptions.isJettyBaseimage()).isFalse(); + assertThat(buildPlan.getEntrypoint()).isNull(); + } + + @Test + public void testToJibContainerBuilder_noProgramArgumentsSpecified() + throws IOException, InvalidImageReferenceException { + JibContainerBuilder containerBuilder = + WarFiles.toJibContainerBuilder( + mockStandardWarExplodedProcessor, + mockCommonCliOptions, + mockCommonContainerConfigCliOptions, + mockLogger); + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + + assertThat(buildPlan.getCmd()).isNull(); + } +} diff --git a/jib-cli/src/test/resources/buildfiles/layers/archiveLayerTest/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/archiveLayerTest/layers.yaml new file mode 100644 index 0000000000..b233d90831 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/archiveLayerTest/layers.yaml @@ -0,0 +1,3 @@ +entries: + - name: "default" + archive: "path/to/archive.tgz" diff --git a/jib-cli/src/test/resources/buildfiles/layers/fileTest/default/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/fileTest/default/layers.yaml new file mode 100644 index 0000000000..fcd5f7034e --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/fileTest/default/layers.yaml @@ -0,0 +1,14 @@ +properties: + # file properties applied to all layers + filePermissions: "644" + directoryPermissions: "755" + user: "0" + group: "0" + timestamp: "0" +entries: + - name: "default" + files: + - src: "toFile.txt" + dest: "/target/toFile.txt" + - src: "toDir.txt" + dest: "/target/dir/" diff --git a/jib-core/src/test/resources/application/resources/resourceA b/jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toDir.txt similarity index 100% rename from jib-core/src/test/resources/application/resources/resourceA rename to jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toDir.txt diff --git a/jib-core/src/test/resources/application/resources/resourceB b/jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toFile.txt similarity index 100% rename from jib-core/src/test/resources/application/resources/resourceB rename to jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toFile.txt diff --git a/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/layers.yaml new file mode 100644 index 0000000000..168e9e8495 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/layers.yaml @@ -0,0 +1,7 @@ +entries: + - name: "default" + files: + - src: "toFile.txt" + dest: "/target/toFile.txt" + excludes: + - "**/*" diff --git a/jib-core/src/test/resources/cache/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/toFile.txt similarity index 100% rename from jib-core/src/test/resources/cache/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz rename to jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/toFile.txt diff --git a/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/layers.yaml new file mode 100644 index 0000000000..29aac675f2 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/layers.yaml @@ -0,0 +1,7 @@ +entries: + - name: "default" + files: + - src: "toFile.txt" + dest: "/target/toFile.txt" + includes: + - "**/*" diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotAClassButMightBe.class b/jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/toFile.txt similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotAClassButMightBe.class rename to jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/toFile.txt diff --git a/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/layers.yaml new file mode 100644 index 0000000000..ef5011cc0e --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/layers.yaml @@ -0,0 +1,51 @@ +properties: + # file properties applied to all layers + filePermissions: "644" + directoryPermissions: "755" + user: "0" + group: "0" + timestamp: "0" +entries: + - name: "includes and excludes" + files: + - src: "project" + dest: "/target/ie" + excludes: + - "**/exclude.me" + includes: + - "**/*.me" + - name: "includes only" + files: + - src: "project" + dest: "/target/io" + includes: + - "**/include.me" + - name: "excludes only" + files: + - src: "project" + dest: "/target/eo" + excludes: + - "**/excludedDir/**" + - name: "excludes only shortcut" + files: + - src: "project" + dest: "/target/eo" + excludes: + # equivalent to "**/excludedDir/**" + - "**/excludedDir/" + - name: "exclude dir and contents" + files: + - src: "project" + dest: "/target/edac" + excludes: + - "**/excludedDir/**" + - "**/excludedDir" + - name: "excludes only wrong" + files: + - src: "project" + dest: "/target/eo" + # this is not a safe excludes of directories + # only excludes the directory, but doesn't match any of the file contents + # and in the end, it will be included anyway because its children are included + excludes: + - "**/excludedDir" diff --git a/jib-core/src/test/resources/directoryA/.gitkeep b/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/excludedDir/exclude.me similarity index 100% rename from jib-core/src/test/resources/directoryA/.gitkeep rename to jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/excludedDir/exclude.me diff --git a/jib-gradle-plugin/src/test/resources/application/classes/some.class b/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/includedDir/include.me similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/classes/some.class rename to jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/includedDir/include.me diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/libraryA.jar b/jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/wild.card similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/libraryA.jar rename to jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/wild.card diff --git a/jib-cli/src/test/resources/buildfiles/layers/pathDoesNotExist/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/pathDoesNotExist/layers.yaml new file mode 100644 index 0000000000..2eb9d5db66 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/pathDoesNotExist/layers.yaml @@ -0,0 +1,5 @@ +entries: + - name: "default" + files: + - src: "something-that-does-not-exist" + dest: "/target/" diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/libraryB.jar b/jib-cli/src/test/resources/buildfiles/layers/propertiesTest/dir/file.txt similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/libraryB.jar rename to jib-cli/src/test/resources/buildfiles/layers/propertiesTest/dir/file.txt diff --git a/jib-cli/src/test/resources/buildfiles/layers/propertiesTest/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/propertiesTest/layers.yaml new file mode 100644 index 0000000000..b2ec28086a --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/propertiesTest/layers.yaml @@ -0,0 +1,47 @@ +properties: + filePermissions: "000" + directoryPermissions: "700" + user: "0" + group: "0" + timestamp: "0" +entries: + - name: "level 0 passthrough" + files: + - src: "dir" + dest: "/app/" + - name: "level 1 overrides" + properties: + filePermissions: "111" + directoryPermissions: "711" + user: "1" + group: "1" + timestamp: "1000" + files: + - src: "dir" + dest: "/app/" + - name: "level 2 overrides" + properties: + filePermissions: "111" + directoryPermissions: "711" + user: "1" + group: "1" + timestamp: "1000" + files: + - src: "dir" + dest: "/app/" + properties: + filePermissions: "222" + directoryPermissions: "722" + user: "2" + group: "2" + timestamp: "2000" + - name: "partial overrides" + properties: + filePermissions: "111" + directoryPermissions: "711" + files: + - src: "dir" + dest: "/app/" + properties: + group: "2" + timestamp: "2000" diff --git a/jib-gradle-plugin/src/test/resources/application/resources/resourceA b/jib-cli/src/test/resources/buildfiles/layers/writeToRoot/dir/file.txt similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/resourceA rename to jib-cli/src/test/resources/buildfiles/layers/writeToRoot/dir/file.txt diff --git a/jib-cli/src/test/resources/buildfiles/layers/writeToRoot/layers.yaml b/jib-cli/src/test/resources/buildfiles/layers/writeToRoot/layers.yaml new file mode 100644 index 0000000000..e0346d3e96 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/layers/writeToRoot/layers.yaml @@ -0,0 +1,11 @@ +entries: + - name: "root writer" + files: + - src: "dir" + dest: "/" + - name: "root parent fill" + files: + - src: "." + dest: "/" + includes: + - "**/file.txt" diff --git a/jib-cli/src/test/resources/buildfiles/projects/allDefaults/jib.yaml b/jib-cli/src/test/resources/buildfiles/projects/allDefaults/jib.yaml new file mode 100644 index 0000000000..6ea1b980ba --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/allDefaults/jib.yaml @@ -0,0 +1,3 @@ +apiVersion: jib/v1alpha1 +kind: BuildFile + diff --git a/jib-cli/src/test/resources/buildfiles/projects/allProperties/altYamls/alt-jib.yaml b/jib-cli/src/test/resources/buildfiles/projects/allProperties/altYamls/alt-jib.yaml new file mode 100644 index 0000000000..6071b758f1 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/allProperties/altYamls/alt-jib.yaml @@ -0,0 +1,50 @@ +# this buildfile doesn't necessarily work, just useful for testing parsing and translation +apiVersion: jib/v1alpha1 +kind: BuildFile + +# "FROM" with detail for manifest lists or multiple architectures +from: + image: "ubuntu" + # optional: if missing, then defaults to `linux/amd64` + platforms: + - architecture: "arm" + os: "linux" + - architecture: "amd64" + os: "darwin" + +# potentially simple form of "FROM" (based on ability to define schema) +# from: "gcr.io/distroless/java:8" + +creationTime: 2000 # millis since epoch or iso8601 creation time +format: OCI # Docker or OCI + +environment: + "KEY1": "v1" + "KEY2": "v2" +labels: + "label1": "l1" + "label2": "l2" +volumes: + - "/volume1" + - "/volume2" + +exposedPorts: + - "123/udp" + - "456" + - "789/tcp" + +user: "customUser" +workingDirectory: "/home" +entrypoint: + - "sh" + - "script.sh" +cmd: + - "--param" + - "param" + +layers: + entries: + - name: "scripts" + files: + - src: "project/script.sh" + dest: "/home/script.sh" diff --git a/jib-cli/src/test/resources/buildfiles/projects/allProperties/jib.yaml b/jib-cli/src/test/resources/buildfiles/projects/allProperties/jib.yaml new file mode 100644 index 0000000000..6071b758f1 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/allProperties/jib.yaml @@ -0,0 +1,50 @@ +# this buildfile doesn't necessarily work, just useful for testing parsing and translation +apiVersion: jib/v1alpha1 +kind: BuildFile + +# "FROM" with detail for manifest lists or multiple architectures +from: + image: "ubuntu" + # optional: if missing, then defaults to `linux/amd64` + platforms: + - architecture: "arm" + os: "linux" + - architecture: "amd64" + os: "darwin" + +# potentially simple form of "FROM" (based on ability to define schema) +# from: "gcr.io/distroless/java:8" + +creationTime: 2000 # millis since epoch or iso8601 creation time +format: OCI # Docker or OCI + +environment: + "KEY1": "v1" + "KEY2": "v2" +labels: + "label1": "l1" + "label2": "l2" +volumes: + - "/volume1" + - "/volume2" + +exposedPorts: + - "123/udp" + - "456" + - "789/tcp" + +user: "customUser" +workingDirectory: "/home" +entrypoint: + - "sh" + - "script.sh" +cmd: + - "--param" + - "param" + +layers: + entries: + - name: "scripts" + files: + - src: "project/script.sh" + dest: "/home/script.sh" diff --git a/jib-cli/src/test/resources/buildfiles/projects/allProperties/project/script.sh b/jib-cli/src/test/resources/buildfiles/projects/allProperties/project/script.sh new file mode 100644 index 0000000000..fba40123d8 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/allProperties/project/script.sh @@ -0,0 +1,2 @@ +env +echo "string from file" diff --git a/jib-cli/src/test/resources/buildfiles/projects/templating/missingVar.yaml b/jib-cli/src/test/resources/buildfiles/projects/templating/missingVar.yaml new file mode 100644 index 0000000000..f168aa0f50 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/templating/missingVar.yaml @@ -0,0 +1,5 @@ +apiVersion: jib/v1alpha1 +kind: BuildFile + +labels: + "label1": "${missingVar}" diff --git a/jib-cli/src/test/resources/buildfiles/projects/templating/multiLine.yaml b/jib-cli/src/test/resources/buildfiles/projects/templating/multiLine.yaml new file mode 100644 index 0000000000..8698df70f6 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/templating/multiLine.yaml @@ -0,0 +1,5 @@ +apiVersion: jib/v1alpha1 +kind: BuildFile + +${replace +this} diff --git a/jib-cli/src/test/resources/buildfiles/projects/templating/valid.yaml b/jib-cli/src/test/resources/buildfiles/projects/templating/valid.yaml new file mode 100644 index 0000000000..97e88c15e2 --- /dev/null +++ b/jib-cli/src/test/resources/buildfiles/projects/templating/valid.yaml @@ -0,0 +1,10 @@ +apiVersion: jib/v1alpha1 +kind: BuildFile + +labels: + "${key}": "${value}" + "label1": "${repeated}" + "label2": "${repeated}" + "label3": "$${escaped}" + "label4": "free$" + "unmatched": "${" diff --git a/jib-cli/src/test/resources/jar/java18.jar b/jib-cli/src/test/resources/jar/java18.jar new file mode 100644 index 0000000000..3466c62aa4 Binary files /dev/null and b/jib-cli/src/test/resources/jar/java18.jar differ diff --git a/jib-cli/src/test/resources/jar/spring-boot/springboot_layered.jar b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered.jar new file mode 100644 index 0000000000..b12d52c097 Binary files /dev/null and b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered.jar differ diff --git a/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_allEmptyLayers.jar b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_allEmptyLayers.jar new file mode 100644 index 0000000000..bf512abada Binary files /dev/null and b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_allEmptyLayers.jar differ diff --git a/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_singleEmptyLayer.jar b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_singleEmptyLayer.jar new file mode 100644 index 0000000000..345d6b2271 Binary files /dev/null and b/jib-cli/src/test/resources/jar/spring-boot/springboot_layered_singleEmptyLayer.jar differ diff --git a/jib-cli/src/test/resources/jar/spring-boot/springboot_notLayered.jar b/jib-cli/src/test/resources/jar/spring-boot/springboot_notLayered.jar new file mode 100644 index 0000000000..ce5ebcfd77 Binary files /dev/null and b/jib-cli/src/test/resources/jar/spring-boot/springboot_notLayered.jar differ diff --git a/jib-cli/src/test/resources/jar/spring-boot/springboot_sample.jar b/jib-cli/src/test/resources/jar/spring-boot/springboot_sample.jar new file mode 100644 index 0000000000..6c420cbd3c Binary files /dev/null and b/jib-cli/src/test/resources/jar/spring-boot/springboot_sample.jar differ diff --git a/jib-gradle-plugin/src/test/resources/application/resources/resourceB b/jib-cli/src/test/resources/jar/standard/dependency1 similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/resourceB rename to jib-cli/src/test/resources/jar/standard/dependency1 diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/libraryA.jar b/jib-cli/src/test/resources/jar/standard/dependency2 similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/libraryA.jar rename to jib-cli/src/test/resources/jar/standard/dependency2 diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/libraryB.jar b/jib-cli/src/test/resources/jar/standard/dependency3-SNAPSHOT-1.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/libraryB.jar rename to jib-cli/src/test/resources/jar/standard/dependency3-SNAPSHOT-1.jar diff --git a/jib-maven-plugin/src/test/resources/application/output/directory/somefile b/jib-cli/src/test/resources/jar/standard/directory/dependency4 similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/directory/somefile rename to jib-cli/src/test/resources/jar/standard/directory/dependency4 diff --git a/jib-cli/src/test/resources/jar/standard/emptyStandardJar.jar b/jib-cli/src/test/resources/jar/standard/emptyStandardJar.jar new file mode 100644 index 0000000000..1c4f76bd62 Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/emptyStandardJar.jar differ diff --git a/jib-cli/src/test/resources/jar/standard/jarWithInvalidClass.jar b/jib-cli/src/test/resources/jar/standard/jarWithInvalidClass.jar new file mode 100644 index 0000000000..048cf58ebe Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/jarWithInvalidClass.jar differ diff --git a/jib-cli/src/test/resources/jar/standard/singleDepJar.jar b/jib-cli/src/test/resources/jar/standard/singleDepJar.jar new file mode 100644 index 0000000000..c3ed17630e Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/singleDepJar.jar differ diff --git a/jib-cli/src/test/resources/jar/standard/standardJarWithClassPath.jar b/jib-cli/src/test/resources/jar/standard/standardJarWithClassPath.jar new file mode 100644 index 0000000000..98913ed02a Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/standardJarWithClassPath.jar differ diff --git a/jib-cli/src/test/resources/jar/standard/standardJarWithOnlyClasses.jar b/jib-cli/src/test/resources/jar/standard/standardJarWithOnlyClasses.jar new file mode 100644 index 0000000000..87853bf7eb Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/standardJarWithOnlyClasses.jar differ diff --git a/jib-cli/src/test/resources/jar/standard/standardJarWithoutClassPath.jar b/jib-cli/src/test/resources/jar/standard/standardJarWithoutClassPath.jar new file mode 100644 index 0000000000..eb51bed80e Binary files /dev/null and b/jib-cli/src/test/resources/jar/standard/standardJarWithoutClassPath.jar differ diff --git a/jib-maven-plugin/src/test/resources/application/output/package/some.class b/jib-cli/src/test/resources/war/standard/allLayers/META-INF/context.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/package/some.class rename to jib-cli/src/test/resources/war/standard/allLayers/META-INF/context.xml diff --git a/jib-maven-plugin/src/test/resources/application/output/resourceA b/jib-cli/src/test/resources/war/standard/allLayers/Test.jsp similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/resourceA rename to jib-cli/src/test/resources/war/standard/allLayers/Test.jsp diff --git a/jib-maven-plugin/src/test/resources/application/output/resourceB b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/MyClass2.class similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/resourceB rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/MyClass2.class diff --git a/jib-maven-plugin/src/test/resources/application/output/some.class b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/package/MyClass.class similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/some.class rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/package/MyClass.class diff --git a/jib-maven-plugin/src/test/resources/application/source/HelloWorld.java b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/package/test.properties similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/HelloWorld.java rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/package/test.properties diff --git a/jib-core/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-core/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-maven-plugin/src/test/resources/application/source/package/some.java b/jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/web.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/package/some.java rename to jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/web.xml diff --git a/jib-maven-plugin/src/test/resources/application/source/some.java b/jib-cli/src/test/resources/war/standard/noWebInfClasses/META-INF/context.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/some.java rename to jib-cli/src/test/resources/war/standard/noWebInfClasses/META-INF/context.xml diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-cli/src/test/resources/war/standard/noWebInfClasses/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-cli/src/test/resources/war/standard/noWebInfClasses/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-cli/src/test/resources/war/standard/noWebInfClasses/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-cli/src/test/resources/war/standard/noWebInfClasses/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-cli/src/test/resources/war/standard/noWebInfClasses/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar differ diff --git a/jib-cli/src/test/resources/war/standard/noWebInfLib/META-INF/context.xml b/jib-cli/src/test/resources/war/standard/noWebInfLib/META-INF/context.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-cli/src/test/resources/war/standard/noWebInfLib/WEB-INF/classes/MyClass2.class b/jib-cli/src/test/resources/war/standard/noWebInfLib/WEB-INF/classes/MyClass2.class new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-cli/src/test/resources/war/standard/noWebInfLib/WEB-INF/classes/package/MyClass.class b/jib-cli/src/test/resources/war/standard/noWebInfLib/WEB-INF/classes/package/MyClass.class new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/.gitignore b/jib-core/.gitignore deleted file mode 100644 index 898930fb52..0000000000 --- a/jib-core/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -build -target -out -*.iml -*.ipr -*.iws -.idea -.gradle -/.settings -/.classpath -/.project diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md new file mode 100644 index 0000000000..7d94150be4 --- /dev/null +++ b/jib-core/CHANGELOG.md @@ -0,0 +1,390 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +### Changed + +### Fixed + +## 0.28.2 + +### Changed +- deps: Update Jackson BOM version to 2.21.1 (#4495) + +### Fixed +- fix: ignore unknown "artifactType" field in manifest descriptors (#4509) + +## 0.28.1 + +### Added +- feat: support Java 25 main methods + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.9 + +## 0.28.0 + +### Added +- feat: add default base image for Java 25 (#4436) + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.8 for java 25 support + +## 0.27.3 + +### Fixed +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4265) + +## 0.27.2 +- fix: allow pushing images with different arch/os to docker daemon [#4265](https://github.com/GoogleContainerTools/jib/issues/4265) +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4265) + +## 0.27.1 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + + +## 0.27.0 + +### Changed +- deps: bump org.apache.commons:commons-compress from 1.21 to 1.26.0 ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) + +### Fixed +- fix: set PAX headers to address build reproducibility issue ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) + +## 0.26.0 + +### Fixed +- fix: support parsing manifest JSON containing `LayerSources:` from latest Docker. ([#4171](https://github.com/GoogleContainerTools/jib/pull/4171)) + +## 0.25.0 + +### Changed +- deps: bump com.github.luben:zstd-jni from 1.5.5-2 to 1.5.5-4. ([#4049](https://github.com/GoogleContainerTools/jib/pull/4049/)) +- deps: bump com.fasterxml.jackson:jackson-bom from 2.15.0 to 2.15.2. ([#4055](https://github.com/GoogleContainerTools/jib/pull/4055)) +- deps: bump com.google.guava:guava from 32.0.1-jre to 32.1.2-jre ([#4078](https://github.com/GoogleContainerTools/jib/pull/4078)) +- deps: bump org.slf4j:slf4j-simple from 2.0.7 to 2.0.9. ([#4098](https://github.com/GoogleContainerTools/jib/pull/4098)) + +### Fixed +- fix: fix WWW-Authenticate header parsing for Basic authentication ([#4035](https://github.com/GoogleContainerTools/jib/pull/4035/)) + +## 0.24.0 + +### Changed +- Replaced deprecated usages of `com.google.api.client.util.Base64` with `java.util.Base64` ([#3872](https://github.com/GoogleContainerTools/jib/pull/3872)) +- Replaced deprecated usages of `ObjectMapper.configure` in jackson ([#3890](https://github.com/GoogleContainerTools/jib/pull/3890)) + +### Fixed +- Fixed `V22ManifestListTemplate` cast to allow pulling an OCI index manifest from cache ([#3974](https://github.com/GoogleContainerTools/jib/pull/3974)) +- Specified `CompressorStreamFactory` to decompress compressed layer until EOF in `CacheStorageWriter` ([#3983](https://github.com/GoogleContainerTools/jib/pull/3983)) +- Fixed multithreading issue from `DockerClientResolver.resolve` by not sharing a static `ServiceLoader` instance ([#3993](https://github.com/GoogleContainerTools/jib/pull/3993)) + +Thanks to our community contributors @Sineaggi, @rquinio, @patrickpichler, @erdi! + +## 0.23.0 + +### Changed +- Upgraded Google HTTP libraries to 1.42.2 ([#3745](https://github.com/GoogleContainerTools/jib/pull/3745)) +- Re-synchronized jackson dependencies with BOM to use latest versions ([#3768](https://github.com/GoogleContainerTools/jib/pull/3768)) + +### Fixed +- Fixed partially cached base image authorization issue by adding check for existence of layers in cache ([#3767](https://github.com/GoogleContainerTools/jib/pull/3767)) + +## 0.22.0 + +### Added +- Better error messaging when environment map in `container.environment` contains null values ([#3672](https://github.com/GoogleContainerTools/jib/pull/3672)). +- DockerClient interface which is used to make calls to the Docker daemon. This allows for custom implementations to be introduced via SPI ([#3703](https://github.com/GoogleContainerTools/jib/pull/3703)). +- Support for OCI image index manifests ([#3715](https://github.com/GoogleContainerTools/jib/pull/3715)). +- Support for base image layer compressed with zstd ([#3717](https://github.com/GoogleContainerTools/jib/pull/3717)) + +### Changed +- Upgraded slf4j-api to 2.0.0 ([#3734](https://github.com/GoogleContainerTools/jib/pull/3734), [#3735](https://github.com/GoogleContainerTools/jib/pull/3735)). +- Upgraded nullaway to 0.9.9. ([#3720](https://github.com/GoogleContainerTools/jib/pull/3720)) +- Jib now throws an exception when the base image doesn't support target platforms during multi-platform build ([#3707](https://github.com/GoogleContainerTools/jib/pull/3707)). +- Jib now only checks for file existence instead of running the executable passed into `dockerClient.executable` for the purpose of verifying if docker is installed correctly. Users are responsible for ensuring that the docker executable specified through this property is valid and has the correct permissions ([#3744](https://github.com/GoogleContainerTools/jib/pull/3744)). + +Thanks to our community contributors, @oliver-brm, @eddumelendez, @rquinio, @gsquared94! + +## 0.21.0 + +### Added +- Support for configuration of credential helper with environment variables ([#3575](https://github.com/GoogleContainerTools/jib/pull/3575)). +- Support architecture suffixes in tags when publishing multi-platform images ([#3523](https://github.com/GoogleContainerTools/jib/pull/3523)). + +### Changed +- Upgraded jackson-databind to 2.13.2.2 ([#3612](https://github.com/GoogleContainerTools/jib/pull/3612)). +- Added helpful pointers for unsupported class file version exception cause ([#3499](https://github.com/GoogleContainerTools/jib/pull/3499)). + + +## 0.20.0 + +### Added + +- Increased robustness in registry communications by retrying HTTP requests (to the effect of retrying image pushes or pulls) on I/O exceptions with exponential backoffs. ([#3351](https://github.com/GoogleContainerTools/jib/pull/3351)) + +### Changed + +- Downgraded Google HTTP libraries to 1.34.0 to resolve network issues. ([#3415](https://github.com/GoogleContainerTools/jib/pull/3415), [#3058](https://github.com/GoogleContainerTools/jib/issues/3058), [#3409](https://github.com/GoogleContainerTools/jib/issues/3409)) + +## 0.19.0 + +### Added + +- For Google Artifact Registry (`*-docker.pkg.dev`), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) last like it has been doing for `gcr.io`. ([#3241](https://github.com/GoogleContainerTools/jib/pull/3241)) + +### Changed + +- `JavaContainerBuilder#fromDistroless()` and `JavaContainerBuilder#fromDistrolessJetty()` are deprecated. To migrate, check the Javadoc. ([#3123](https://github.com/GoogleContainerTools/jib/pull/3123)) +- Timestamps of file entries in a built `TarImage` are set to the epoch, making the tarball reproducible. ([#3158](https://github.com/GoogleContainerTools/jib/issues/3158)) + +## 0.18.0 + +### Added + +- New method: `Containerizer#addRegistryMirrors` for configuring registry mirrors for base images. This is useful when hitting [Docker Hub rate limits](https://www.docker.com/increase-rate-limits). Only public mirrors (such as `mirror.gcr.io`) are supported. ([#2999](https://github.com/GoogleContainerTools/jib/issues/2999)) + +## 0.17.0 + +### Fixed + +- Updated jackson dependency version causing compatibility issues. ([#2931](https://github.com/GoogleContainerTools/jib/issues/2931)) +- Fixed `NullPointerException` when pulling an OCI base image whose manifest does not have `mediaType` information. ([#2819](https://github.com/GoogleContainerTools/jib/issues/2819)) +- Fixed build failure when using a Docker daemon base image (`docker://...`) that has duplicate layers. ([#2829](https://github.com/GoogleContainerTools/jib/issues/2829)) + +## 0.16.0 + +### Added + +- Allow setting platform when building image from scratch. ([#2765](https://github.com/GoogleContainerTools/jib/issues/2765)) +- New system property `jib.skipExistingImages` (false by default) to skip pushing images (manifests) if the image already exists in the registry. ([#2360](https://github.com/GoogleContainerTools/jib/issues/2360)) +- _Incubating feature_: can now configure multiple platforms (such as architectures) to build multiple images as a bundle and push as a manifest list (also known as a fat manifest). As an incubating feature, there are certain limitations. For example, OCI image indices are not supported, and building a manifest list is supported only for registry pushing (using `RegistryImage`). ([#2523](https://github.com/GoogleContainerTools/jib/issues/2523), [#1567](https://github.com/GoogleContainerTools/jib/issues/1567)) + +### Changed + +- Upgraded jib-build-plan to 0.4.0. ([#2660](https://github.com/GoogleContainerTools/jib/pull/2660)) +- Previous locally cached base image manifests will be ignored, as the caching mechanism changed to enable multi-platform image building. ([#2730](https://github.com/GoogleContainerTools/jib/pull/2730), [#2711](https://github.com/GoogleContainerTools/jib/pull/2711)) +- Upgraded the ASM library to 9.0 to resolve an issue when auto-inferring main class in Java 15+. ([#2776](https://github.com/GoogleContainerTools/jib/pull/2776)) + +### Fixed + +- Fixed `NullPointerException` when the `"auths":` section in `~/.docker/config.json` has an entry with no `"auth":` field. ([#2535](https://github.com/GoogleContainerTools/jib/issues/2535)) +- Fixed `NullPointerException` to return a helpful message when a server does not provide any message in certain error cases (400 Bad Request, 404 Not Found, and 405 Method Not Allowed). ([#2532](https://github.com/GoogleContainerTools/jib/issues/2532)) +- Now supports sending client certificate (for example, via the `javax.net.ssl.keyStore` and `javax.net.ssl.keyStorePassword` system properties) and thus enabling mutual TLS authentication. ([#2585](https://github.com/GoogleContainerTools/jib/issues/2585), [#2226](https://github.com/GoogleContainerTools/jib/issues/2226)) +- Fixed `NullPointerException` during input validation (in Java 9+) when configuring Jib parameters using certain immutable collections (such as `List.of()`). ([#2702](https://github.com/GoogleContainerTools/jib/issues/2702)) +- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784)) +- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134)) + +## 0.15.0 + +### Added + +- Now sets configured file ownership when creating layer tars. ([#2499](https://github.com/GoogleContainerTools/jib/pull/2499)) + +### Changed + +- `Ports.parse(List ports)` now returns a `Set`(as a `HashSet`) instead of `ImmutableSet` ([#2513](https://github.com/GoogleContainerTools/jib/pull/2513)) +- Previous locally cached application layers will be ignored because of changes to the caching selectors. ([#2499](https://github.com/GoogleContainerTools/jib/pull/2499)) + +### Fixed + +- Fixed authentication failure with Azure Container Registry when using an identity token defined in the `auths` section of Docker config (`~/.docker/config.json`). ([#2488](https://github.com/GoogleContainerTools/jib/pull/2488)) +- Now adding the Jib Core dependency transitively exposes the Build Plan API. ([#2507](https://github.com/GoogleContainerTools/jib/issues/2507)) + +## 0.14.0 + +### Added + +- Multiple additions to `ImageReference` to separate `tag` and `digest`. ([#1481](https://github.com/GoogleContainerTools/jib/issues/1481)) + - `of(registry, repository, tag, digest)` to create an image from a tag and digest. + - `isValidDigest(digest)` to check if a string is a valid digest. + - `getDigest()` to get the digest. + - `parse()` now supports image reference strings containing both a tag and a digest. + - `getQualifier()` to return the digest, or the tag if no digest is set. + - `withQualifier()` to change the image's tag or digest (old behavior of `withTag()`) +- New public API package hierarchy `com.google.cloud.tools.jib.api.buildplan` for [Container Build Plan Specification](https://github.com/GoogleContainerTools/jib/blob/2b2fe82ad0552ba3ce1de308a0fda24aab684365/proposals/container-build-plan-spec.md) and API. +- `ContainerBuildPlan`, `FileEntriesLayer`, `FileEntry`, and `LayerObject` in `com.google.cloud.tools.jib.api.buildplan` as part of the Container Build Plan API. ([#2338](https://github.com/GoogleContainerTools/jib/pull/2338), [#2328](https://github.com/GoogleContainerTools/jib/pull/2328)) + +### Changed + +- `ImageReference#toStringWithTag` has been renamed to `toStringWithQualifier`. +- `ImageReference#isValidTag` no longer returns `true` for digests. +- `ImageReference#isTagDigest` has been removed; use `#getDigest` with `Optional#isPresent()` to check if an `ImageReference` uses a digest. +- `ImageReference#withTag` has been removed; use `withQualifier()` instead. +- `ImageReference#isDefaultTag` and `usesDefaultTag` no longer return `true` for `null` or empty tags. +- The following classes in `com.google.cloud.tools.jib.api` has been moved to `com.google.cloud.tools.jib.api.buildplan`: `AbsoluteUnixPath`, `RelativeUnixPath`, `Port`, `FilePermissions`, and `ImageFormat`. ([#2328](https://github.com/GoogleContainerTools/jib/pull/2328)) +- `LayerConfiguration` and `LayerEntry` are deprecated. Use `FileEntriesLayer` and `FileEntry` in `com.google.cloud.tools.jib.api.buildplan`. ([#2334](https://github.com/GoogleContainerTools/jib/pull/2334)) + +### Fixed + +- Fixed the problem not inheriting `USER` container configuration from a base image. ([#2421](https://github.com/GoogleContainerTools/jib/pull/2421)) +- Fixed wrong capitalization of JSON properties in a loadable Docker manifest when building a tar image. ([#2430](https://github.com/GoogleContainerTools/jib/issues/2430)) +- Fixed an issue when using a base image whose image creation timestamp contains timezone offset. ([#2428](https://github.com/GoogleContainerTools/jib/issues/2428)) + +## 0.13.1 + +### Fixed + +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) + +## 0.13.0 + +### Added + +- New method: `JibContainerBuilder#describeContainer` which returns new class: `JibContainerDescription`, containing a selection of information used for the Jib build. ([#2115](https://github.com/GoogleContainerTools/jib/issues/2115)) + +### Changed + +- Each local base image layer is pushed immediately after being compressed, rather than waiting for all layers to finish compressing before starting to push. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- `Containerizer.DEFAULT_BASE_CACHE_DIRECTORY` has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` + +### Fixed + +- `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) +- Fixed inefficient communications when interacting with insecure registries and servers (when `Containerizer#setAllowInsecureRegistries(boolean)` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) +- Building a tarball with `OCI` format now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) + +## 0.12.0 + +### Added + +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) +- `Containerizer#setAlwaysCacheBaseImage(boolean)` controls the optimization to skip downloading base image layers that exist in a target registry. ([#1870](https://github.com/GoogleContainerTools/jib/pull/1870)) + +### Changed + +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `DockerDaemonImage` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) +- Now ignores `jib.alwaysCacheBaseImage` system property. Use `Containerizer#setAlwaysCacheBaseImage(boolean)` instead. ([#1870](https://github.com/GoogleContainerTools/jib/pull/1870)) + +### Fixed + +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) +- Fixed `TarImage` base image failing if tar does not contain explicit directory entries. ([#2067](https://github.com/GoogleContainerTools/jib/issues/2067)) + +## 0.11.0 + +### Added + +- `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) +- `Jib#from(String)` accepts strings prefixed with `docker://`, `tar://`, or `registry://` to specify image type. + +### Changed + +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) +- `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)`. ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) + +### Fixed + +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) +- Fixed the regression of slow network operations introduced at 0.10.1. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when the `Containerizer` is set to allow insecure registries. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) + +## 0.10.1 + +### Added + +- `JavaContainerBuilder#setLastModifiedTimeProvider` to set file timestamps. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) + +### Changed + +- `JibContainerBuilder#addDependencies` is now split into three methods: `addDependencies`, `addSnapshotDependencies`, `addProjectDependencies`. ([#1773](https://github.com/GoogleContainerTools/jib/pull/1773)) +- For building and pushing to a registry, Jib now skips downloading and caching base image layers if the layers already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) + +### Fixed + +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + +## 0.10.0 + +### Added + +- `Containerizer#addEventHandler` for adding event handlers. + +### Changed + +- Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package. +- Event handlers are now added directly to the `Containerizer` rather than adding them to an `EventHandlers` object first. +- Removed multiple classes to simplify the event system (`JibEventType`, `BuildStepType`, `EventDispatcher`, `DefaultEventDispatcher`, `LayerCountEvent`) +- MainClassFinder now uses a static method instead of requiring instantiation. + +## 0.9.2 + +### Added + +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- `Containerizer#setOfflineMode` to retrieve the base image from Jib's cache rather than a container registry. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) + +### Fixed + +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) + +## 0.9.1 + +### Added + +- Overloads for `LayerConfiguration#addEntryRecursive` that take providers to set file permissions and file modification time on a per-file basis. ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) + +### Changed + +- `LayerConfiguration` takes file modification time as an `Instant` instead of a `long`. + +### Fixed + +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + +## 0.9.0 + +### Added + +- `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers. ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) +- `Jib#fromScratch()` to start building from an empty base image. ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) +- Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files. + +### Changed + +- Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint. +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) + +### Fixed + +- `ImageReference` assumes `registry-1.docker.io` as the registry if the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) + +## 0.1.2 + +### Added + +- `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to. ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) +- `LayerCountEvent` that is dispatched at the beginning of certain pull/build/push build steps to indicate the number of layers being processed. ([#1461](https://github.com/GoogleContainerTools/jib/pull/1461)) + +### Changed + +- `JibContainerBuilder#containerize()` throws multiple sub-types of `RegistryException` rather than wrapping them in an `ExecutionException`. ([#1440](https://github.com/GoogleContainerTools/jib/issues/1440)) + +### Fixed + +- `MainClassFinder` failure when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + +## 0.1.1 + +### Added + +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Adds `JavaContainerBuilder` for building opinionated containers for Java applications. ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) diff --git a/jib-core/README.md b/jib-core/README.md index fe751f04e8..44f051d858 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -1,3 +1,133 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-core)](https://github.com/igrigorik/ga-beacon) +![experimental](https://img.shields.io/badge/stability-beta-orange.svg) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core) +[![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) -Jib core library. NOT intended for use outside of `jib-maven-plugin` or `jib-gradle-plugin` in its current state. We are working on making this into a library for Java. \ No newline at end of file +# Jib Core - Java library for building containers + +Jib Core is a Java library for building Docker and [OCI](https://github.com/opencontainers/image-spec) container images. It implements a general-purpose container builder that can be used to build containers without a Docker daemon, for any application. The implementation is pure Java. + +*The API is currently in beta and may change substantially.* + +Jib Core powers the popular Jib plugins for Maven and Gradle. The plugins build containers specifically for JVM languages and separate the application into multiple layers to optimize for fast rebuilds.\ +For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin).\ +For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). + +For information about the Jib project, see the [Jib project README](../README.md). + +## Adding Jib Core to your build + +Add Jib Core as a dependency using Maven: + +```xml + + com.google.cloud.tools + jib-core + 0.28.2 + +``` + +Add Jib Core as a dependency using Gradle: + +```groovy +dependencies { + compile 'com.google.cloud.tools:jib-core:0.28.2' +} +``` + +## Examples + +```java +Jib.from("busybox") + .addLayer(Arrays.asList(Paths.get("helloworld.sh")), AbsoluteUnixPath.get("/")) + .setEntrypoint("sh", "/helloworld.sh") + .containerize( + Containerizer.to(RegistryImage.named("gcr.io/my-project/hello-from-jib") + .addCredential("myusername", "mypassword"))); +``` + +1. [`Jib.from("busybox")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. +1. [`.addLayer(...)`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.api.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. +1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. +1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. +1. [`.addCredential`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers can be used with [`.addCredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.api.CredentialRetriever-). +1. [`Containerizer.to`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. +1. [`.containerize`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. + +See [examples](examples/README.md) for links to more jib-core samples. We welcome contributions for additional examples and tutorials! + +## API overview + +[`Jib`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core + +[`JibContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build + +[`Containerizer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to + +[`JibContainer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container + +Three types define what Jib can accept as either the base image or as the build target: +- [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) - an image on a container registry +- [`DockerDaemonImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - an image in the Docker daemon +- [`TarImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/TarImage.html) - an image saved as a tarball archive on the filesystem + +Other useful classes: +- [`ImageReference`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references +- [`LayerConfiguration`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/LayerConfiguration.html) - configures a container layer to build +- [`CredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry +- [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers + +Java-specific API: +- [`JavaContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JavaContainerBuilder.html) - configures a `JibContainerBuilder` for Java-specific applications +- [`MainClassFinder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/MainClassFinder.html) - find the main Java class in a given list of class files + +## API reference + +[API reference](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/package-summary.html) + +## How Jib Core works + +The Jib Core system consists 3 main parts: + +- an execution orchestrator that executes an asynchronous pipeline of containerization steps, +- an image manipulator capable of handling Docker and OCI image formats, and +- a registry client that implements the [Docker Registry V2 API](https://docs.docker.com/registry/spec/api/). + +Some other parts of Jib Core internals include: + +- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) +- an [eventing system](#events) to react to events from Jib Core during its execution (add handlers with [`Containerizer.addEventHandler`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#addEventHandler-java.lang.Class-java.util.function.Consumer-)) +- support for fully-concurrent multi-threaded executions + +## Events + +Throughout the build process, Jib Core dispatches events that provide useful information. These events implement the type [`JibEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibEvent.html), and can be handled by registering event handlers with the containerizer. + +```java +Jib.from(...) + ... + .containerize( + Containerizer.to(...) + ... + .addEventHandler(LogEvent.class, logEvent -> System.out.println(logEvent.getLevel() + ": " + logEvent.getMessage()) + .addEventHandler(TimerEvent.class, timeEvent -> ...)); +``` + +When Jib dispatches events, the event handlers you defined for that event type will be called. The following are the types of events you can listen for in Jib core (see [API reference](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/package-summary.html) for more information): + +- [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. +- [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) (*Incubating*) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. +- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) (*Incubating*) - Indicates the amount of progress build steps have made. Each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. + +## Frequently Asked Questions (FAQ) + +See the [Jib project FAQ](../docs/faq.md). + +## Upcoming features + +- Extensions to make building Java and other language-specific containers easier + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. + +## Community + +See the [Jib project README](/../../#community). diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 09d5bc6942..f31340e269 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -1,125 +1,81 @@ plugins { - id 'java' - id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.7.1' - id 'net.ltgt.apt' version '0.13' - id 'net.ltgt.errorprone' version '0.0.13' - id 'biz.aQute.bnd.builder' version '4.0.0' + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' } -group 'com.google.cloud.tools' - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -repositories { - mavenCentral() -} - -sourceSets { - integrationTest { - java { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/java') - } - resources.srcDir file('src/integration-test/resources') - } -} - -configurations { - integrationTestCompile.extendsFrom testCompile - integrationTestRuntime.extendsFrom testRuntime +java { + // Feature to handle base image layers compressed with zstd instead of gzip + // Will need to re-assess the optional dependency when zstd becomes widely used + registerFeature('zstdSupport') { + usingSourceSet(sourceSets.main) + } } dependencies { - // Make sure these are consistent with jib-maven-plugin. - compile 'com.google.http-client:google-http-client:1.23.0' - compile 'org.apache.commons:commons-compress:1.17' - compile 'com.google.guava:guava:23.5-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.slf4j:slf4j-api:1.7.25' - compile 'org.javassist:javassist:3.22.0-GA' - - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.12.0' - - // NullAway errorprone plugin - apt 'com.uber.nullaway:nullaway:0.4.2' - errorprone 'com.google.errorprone:error_prone_core:2.2.0' + api dependencyStrings.BUILD_PLAN + implementation dependencyStrings.GOOGLE_HTTP_CLIENT + implementation dependencyStrings.GOOGLE_HTTP_CLIENT_APACHE_V2 + implementation dependencyStrings.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP + + implementation dependencyStrings.COMMONS_COMPRESS + zstdSupportImplementation dependencyStrings.ZSTD_JNI + implementation dependencyStrings.GUAVA + implementation(platform(dependencyStrings.JACKSON_BOM)) + implementation dependencyStrings.JACKSON_DATABIND + implementation dependencyStrings.JACKSON_DATATYPE_JSR310 + implementation dependencyStrings.ASM + + testImplementation dependencyStrings.JUNIT + testImplementation dependencyStrings.TRUTH + testImplementation dependencyStrings.TRUTH8 + testImplementation dependencyStrings.MOCKITO_CORE + testImplementation dependencyStrings.SLF4J_API + testImplementation dependencyStrings.SYSTEM_RULES + testImplementation dependencyStrings.ZSTD_JNI + + integrationTestImplementation dependencyStrings.JBCRYPT } jar { - manifest { - attributes 'Implementation-Version': version - - // OSGi metadata - attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib' - attributes 'Bundle-Name': 'Jib library for building Docker and OCI images' - attributes 'Bundle-Vendor': 'Google LLC' - attributes 'Bundle-DocURL': 'https://github.com/GoogleContainerTools/jib' - attributes 'Bundle-License': 'https://www.apache.org/licenses/LICENSE-2.0' - attributes 'Export-Package': 'com.google.cloud.tools.jib.*' - } -} - -task wrapper(type: Wrapper) { - gradleVersion = '4.6' -} - -// Integration tests must be run explicitly -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - systemProperty '_JIB_DISABLE_USER_AGENT', true -} -integrationTest.dependsOn test - -test { - testLogging { - showStandardStreams = true - exceptionFormat = 'full' - } + manifest { + attributes 'Implementation-Version': archiveVersion + attributes 'Automatic-Module-Name': 'com.google.cloud.tools.jib' + + // OSGi metadata + attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib' + attributes 'Bundle-Name': 'Jib library for building Docker and OCI images' + attributes 'Bundle-Vendor': 'Google LLC' + attributes 'Bundle-DocURL': 'https://github.com/GoogleContainerTools/jib' + attributes 'Bundle-License': 'https://www.apache.org/licenses/LICENSE-2.0' + attributes 'Export-Package': 'com.google.cloud.tools.jib.*' + } } -// Adds NullAway errorprone checks. -tasks.withType(JavaCompile) { - if (!name.toLowerCase().contains("test")) { - options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] +/* RELEASE */ +configureMavenRelease() + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Jib Core' + description = 'Build container images.' + } + from components.java } + } } -// Fail build on javadoc warnings -tasks.withType(Javadoc) { - options.addBooleanOption('Xwerror', true) -} -assemble.dependsOn javadoc - -tasks.withType(Test) { - reports.html.setDestination file("${reporting.baseDir}/${name}") +// Release plugin (git release commits and version updates) +release { + tagTemplate = 'v$version-core' + git { + requireBranch = /^core-release-v\d+.*$/ //regex + } } +/* RELEASE */ -/* GOOGLE JAVA FORMAT */ -googleJavaFormat { - toolVersion = '1.6' -} -check.dependsOn verifyGoogleJavaFormat -/* GOOGLE JAVA FORMAT */ - -/* CHECKSTYLE */ -checkstyle { - toolVersion = "7.6.1" - - // get the google_checks.xml file from the checkstyle jar and take out the java checks - def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - - config = resources.text.fromString(googleChecksNoJava) - - maxErrors = 0 - maxWarnings = 0 -} -/* CHECKSTYLE */ +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +/* ECLIPSE */ diff --git a/jib-core/examples/README.md b/jib-core/examples/README.md new file mode 100644 index 0000000000..c7ad83c2fe --- /dev/null +++ b/jib-core/examples/README.md @@ -0,0 +1,21 @@ +# Jib Core examples + +Please [file an issue](/../../issues/new) if you find any problems with the examples or would like to request other examples. + +For examples on using Jib plugins, see [examples](../../examples). + +### [sbt](https://index.scala-lang.org/schmitch/sbt-jib) plugin + +Jib Core is used in a third-party project, [sbt-jib](https://github.com/schmitch/sbt-jib), that brings the benefits of Jib to [sbt](https://www.scala-sbt.org/) users. + +### Cram + +See [Cram](https://github.com/briandealwis/cram) for a simple example of a command-line utility that uses Jib Core to build Docker containers from file system contents. + +### Using Jib Core in Gradle builds + +See [build.gradle](build.gradle) for examples using Jib Core to build and manipulate container images within a `build.gradle` script. + +### AutoJib - make your application self-containerize + +See [AutoJib](https://github.com/coollog/autojib) for an example of a library that, when added as a dependency to a Java application, can make your application containerize itself. diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md new file mode 100644 index 0000000000..ab1ef3218c --- /dev/null +++ b/jib-core/examples/build.gradle/README.md @@ -0,0 +1,52 @@ +# Examples using Jib Core in Gradle builds + +Jib Core is a containerization library for JVM languages, so it works well in Groovy as well. You can use Jib Core directly within a Gradle `build.gradle` to make tasks that build and manipulate container images. + +For example, the following snippet is a simple example that creates a Gradle task that adds an environment variable to an existing image: + +`build.gradle`: + +```groovy +// Imports Jib Core as a library to use in this build script. +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + classpath 'com.google.cloud.tools:jib-core:0.28.2' + } +} + +import com.google.cloud.tools.jib.api.Jib +import com.google.cloud.tools.jib.api.Containerizer +import com.google.cloud.tools.jib.api.ImageReference +import com.google.cloud.tools.jib.api.RegistryImage +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory + +// Creates a task called 'dojib'. +task('dojib') { + doLast { + def targetImage = '' + Jib + // Starts with the existing image. + .from(' logger.log(LogLevel.valueOf(logEvent.getLevel().name()), logEvent.getMessage())) + .dockerConfig()))) + println 'done' + } +} +``` diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 783163c04f..a9d5db2df1 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.0-SNAPSHOT \ No newline at end of file +version = 0.28.3-SNAPSHOT diff --git a/jib-core/gradle/wrapper/gradle-wrapper.jar b/jib-core/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f6b961fd5a..0000000000 Binary files a/jib-core/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/jib-core/kokoro/release_build.sh b/jib-core/kokoro/release_build.sh new file mode 100644 index 0000000000..1b8b7151ff --- /dev/null +++ b/jib-core/kokoro/release_build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Fail on any error. +set -o errexit +# Display commands to stderr. +set -o xtrace + +cd github/jib +./gradlew :jib-core:prepareRelease diff --git a/jib-core/settings.gradle b/jib-core/settings.gradle deleted file mode 100644 index 3b44f1557e..0000000000 --- a/jib-core/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'jib-core' diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java index e77ce7aa69..384b414775 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; @@ -29,12 +30,23 @@ public class Command { private final List command; + private Path workingDir = null; /** Instantiate with a command. */ public Command(String... command) { this.command = Arrays.asList(command); } + /** Instantiate with a command. */ + public Command(List command) { + this.command = command; + } + + public Command setWorkingDir(Path workingDir) { + this.workingDir = workingDir; + return this; + } + /** Runs the command. */ public String run() throws IOException, InterruptedException { return run(null); @@ -42,7 +54,11 @@ public String run() throws IOException, InterruptedException { /** Runs the command and pipes in {@code stdin}. */ public String run(@Nullable byte[] stdin) throws IOException, InterruptedException { - Process process = new ProcessBuilder(command).start(); + ProcessBuilder processBuilder = new ProcessBuilder(command); + if (workingDir != null) { + processBuilder.directory(workingDir.toFile()); + } + Process process = processBuilder.start(); if (stdin != null) { // Write out stdin. @@ -57,7 +73,10 @@ public String run(@Nullable byte[] stdin) throws IOException, InterruptedExcepti String output = CharStreams.toString(inputStreamReader); if (process.waitFor() != 0) { - throw new RuntimeException("Command '" + String.join(" ", command) + "' failed"); + String stderr = + CharStreams.toString( + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)); + throw new RuntimeException("Command '" + String.join(" ", command) + "' failed: " + stderr); } return output; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java new file mode 100644 index 0000000000..7c3e0ef844 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib; + +import com.google.common.base.Strings; +import org.junit.Assert; + +/** Configuration for integration tests. */ +public class IntegrationTestingConfiguration { + + public static String getTestRepositoryLocation() { + String projectId = System.getenv("JIB_INTEGRATION_TESTING_PROJECT"); + if (!Strings.isNullOrEmpty(projectId)) { + return "gcr.io/" + projectId; + } + String location = System.getenv("JIB_INTEGRATION_TESTING_LOCATION"); + if (Strings.isNullOrEmpty(location)) { + Assert.fail( + "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " + + "GCP project to use for integration testing or " + + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); + } + return location; + } + + private IntegrationTestingConfiguration() {} +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java new file mode 100644 index 0000000000..1fae4f37e1 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -0,0 +1,360 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// TODO: now it looks like we can move everything here into JibIntegrationTest. +/** Integration tests for {@link Containerizer}. */ +public class ContainerizerIntegrationTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + private final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + + /** + * Helper class to hold a {@link ProgressEventHandler} and verify that it handles a full progress. + */ + private static class ProgressChecker { + + private final ProgressEventHandler progressEventHandler = + new ProgressEventHandler( + update -> { + lastProgress = update.getProgress(); + areTasksFinished = update.getUnfinishedLeafTasks().isEmpty(); + }); + + private volatile double lastProgress = 0.0; + private volatile boolean areTasksFinished = false; + + private void checkCompletion() { + Assert.assertEquals(1.0, lastProgress, DOUBLE_ERROR_MARGIN); + Assert.assertTrue(areTasksFinished); + } + } + + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + + private static final Logger logger = LoggerFactory.getLogger(ContainerizerIntegrationTest.class); + private static final String DISTROLESS_DIGEST = + "sha256:f488c213f278bc5f9ffe3ddf30c5dbb2303a15a74146b738d12453088e662880"; + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + public static ImmutableList fakeLayerConfigurations; + + @BeforeClass + public static void setUp() throws URISyntaxException, IOException { + fakeLayerConfigurations = + ImmutableList.of( + makeLayerConfiguration("core/application/dependencies", "/app/libs/"), + makeLayerConfiguration("core/application/resources", "/app/resources/"), + makeLayerConfiguration("core/application/classes", "/app/classes/")); + } + + /** + * Lists the files in the {@code resourcePath} resources directory and builds a {@link + * FileEntriesLayer} from those files. + */ + private static FileEntriesLayer makeLayerConfiguration( + String resourcePath, String pathInContainer) throws URISyntaxException, IOException { + try (Stream fileStream = + Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { + FileEntriesLayer.Builder layerConfigurationBuilder = FileEntriesLayer.builder(); + fileStream.forEach( + sourceFile -> + layerConfigurationBuilder.addEntry( + sourceFile, AbsoluteUnixPath.get(pathInContainer + sourceFile.getFileName()))); + return layerConfigurationBuilder.build(); + } + } + + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + String dockerConfigEnv = + new Command("docker", "inspect", "-f", "{{.Config.Env}}", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/tcp\":{},\"2001/tcp\":{},\"2002/tcp\":{},\"3000/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + MatcherAssert.assertThat(dockerConfigEnv, CoreMatchers.containsString("env1=envvalue1")); + MatcherAssert.assertThat(dockerConfigEnv, CoreMatchers.containsString("env2=envvalue2")); + MatcherAssert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); + MatcherAssert.assertThat(history, CoreMatchers.containsString("bazel build ...")); + } + + private static void assertLayerSize(int expected, String imageReference) + throws IOException, InterruptedException { + Command command = + new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); + String layers = command.run().trim(); + Assert.assertEquals(expected, Splitter.on(",").splitToList(layers).size()); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private ProgressChecker progressChecker = new ProgressChecker(); + + @Test + public void testSteps_forBuildToDockerRegistry() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + System.setProperty("jib.alwaysCacheBaseImage", "true"); + String imageReference = dockerHost + ":" + "5000/testimage:testtag"; + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + Containerizer containerizer = + Containerizer.to(RegistryImage.named(imageReference)) + .setBaseImageLayersCache(cacheDirectory) + .setApplicationLayersCache(cacheDirectory); + + long lastTime = System.nanoTime(); + JibContainer image1 = + buildImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + containerizer, + Collections.emptyList()); + + progressChecker.checkCompletion(); + progressChecker = new ProgressChecker(); // to reset + + logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + + lastTime = System.nanoTime(); + JibContainer image2 = + buildImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + containerizer, + Collections.emptyList()); + + logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + + Assert.assertEquals(image1, image2); + + localRegistry.pull(imageReference); + assertDockerInspect(imageReference); + assertLayerSize(7, imageReference); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); + + String imageReferenceByDigest = dockerHost + ":5000/testimage@" + image1.getDigest(); + localRegistry.pull(imageReferenceByDigest); + assertDockerInspect(imageReferenceByDigest); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReferenceByDigest).run()); + } + + @Test + public void testSteps_forBuildToDockerRegistry_multipleTags() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + buildImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + Containerizer.to(RegistryImage.named(dockerHost + ":5000/testimage:testtag")), + Arrays.asList("testtag2", "testtag3")); + + String imageReference = dockerHost + ":5000/testimage:testtag"; + localRegistry.pull(imageReference); + assertDockerInspect(imageReference); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); + + String imageReference2 = dockerHost + ":5000/testimage:testtag2"; + localRegistry.pull(imageReference2); + assertDockerInspect(imageReference2); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReference2).run()); + + String imageReference3 = dockerHost + ":5000/testimage:testtag3"; + localRegistry.pull(imageReference3); + assertDockerInspect(imageReference3); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReference3).run()); + } + + @Test + public void testSteps_forBuildToDockerRegistry_skipExistingDigest() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); + + JibContainer image1 = + buildImage( + ImageReference.scratch(), + Containerizer.to(RegistryImage.named(dockerHost + ":5000/testimagerepo:testtag")), + Collections.singletonList("testtag2")); + + // Test that the initial image with the original tag has been pushed. + localRegistry.pull(dockerHost + ":5000/testimagerepo:testtag"); + // Test that any additional tags have also been pushed with the original image. + localRegistry.pull(dockerHost + ":5000/testimagerepo:testtag2"); + + // Push the same image with a different tag, with SKIP_EXISTING_IMAGES enabled. + JibContainer image2 = + buildImage( + ImageReference.scratch(), + Containerizer.to(RegistryImage.named(dockerHost + ":5000/testimagerepo:new_testtag")), + Collections.emptyList()); + + // Test that the pull request throws an exception, indicating that the new tag was not pushed. + try { + localRegistry.pull(dockerHost + ":5000/testimagerepo:new_testtag"); + Assert.fail( + "jib.skipExistingImages was enabled and digest was already pushed, " + + "hence new_testtag shouldn't have been pushed."); + } catch (RuntimeException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "manifest for " + dockerHost + ":5000/testimagerepo:new_testtag not found")); + } + + // Test that both images have the same properties. + Assert.assertEquals(image1.getDigest(), image2.getDigest()); + Assert.assertEquals(image1.getImageId(), image2.getImageId()); + + // Test that the first image was pushed while the second one was skipped + Assert.assertTrue(image1.isImagePushed()); + Assert.assertFalse(image2.isImagePushed()); + } + + @Test + public void testBuildToDockerRegistry_dockerHubBaseImage() + throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + // We use eclipse-temurin instead of openjdk due to its deprecation + // see https://hub.docker.com/_/openjdk#deprecation-notice + buildImage( + ImageReference.parse("eclipse-temurin:8-jre-alpine"), + Containerizer.to(RegistryImage.named(dockerHost + ":5000/testimage:testtag")), + Collections.emptyList()); + + String imageReference = dockerHost + ":5000/testimage:testtag"; + new Command("docker", "pull", imageReference).run(); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); + } + + @Test + public void testBuildToDockerDaemon_multipleTags() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + buildImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + Containerizer.to(DockerDaemonImage.named("testdocker")), + Arrays.asList("testtag2", "testtag3")); + + progressChecker.checkCompletion(); + + assertLayerSize(7, "testdocker"); + assertDockerInspect("testdocker"); + assertDockerInspect("testdocker:testtag2"); + assertDockerInspect("testdocker:testtag3"); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testdocker").run()); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", "testdocker:testtag2").run()); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", "testdocker:testtag3").run()); + } + + @Test + public void testBuildTarball() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); + buildImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + Containerizer.to(TarImage.at(outputPath).named("testtar")), + Collections.emptyList()); + + progressChecker.checkCompletion(); + + new Command("docker", "load", "--input", outputPath.toString()).run(); + assertLayerSize(7, "testtar"); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); + } + + private JibContainer buildImage( + ImageReference baseImage, Containerizer containerizer, List additionalTags) + throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, + ExecutionException { + JibContainerBuilder containerBuilder = + Jib.from(baseImage) + .setEntrypoint( + Arrays.asList( + "java", "-cp", "/app/resources:/app/classes:/app/libs/*", "HelloWorld")) + .setProgramArguments(Collections.singletonList("An argument.")) + .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) + .setExposedPorts(Ports.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) + .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) + .setFileEntriesLayers(fakeLayerConfigurations); + + containerizer + .setAllowInsecureRegistries(true) + .setToolName("jib-integration-test") + .addEventHandler(ProgressEvent.class, progressChecker.progressEventHandler); + additionalTags.forEach(containerizer::withAdditionalTag); + + return containerBuilder.containerize(containerizer); + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java new file mode 100644 index 0000000000..bf60f5d025 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -0,0 +1,436 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.cloud.tools.jib.registry.ManifestPullerIntegrationTest; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Integration tests for {@link Jib}. */ +public class JibIntegrationTest { + + /** A known oci index sha for gcr.io/distroless/base. */ + public static final String KNOWN_OCI_INDEX_SHA = + "sha256:2c50b819aa3bfaf6ae72e47682f6c5abc0f647cf3f4224a4a9be97dd30433909"; + + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private String imageToDelete; + + private final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + + private final RegistryClient registryClient = + RegistryClient.factory( + EventHandlers.NONE, + dockerHost + ":5000", + "jib-scratch", + new FailoverHttpClient(true, true, ignored -> {})) + .newRegistryClient(); + + private final RegistryClient distrolessRegistryClient = + RegistryClient.factory( + EventHandlers.NONE, + dockerHost + ":5000", + "jib-distroless", + new FailoverHttpClient(true, true, ignored -> {})) + .newRegistryClient(); + + /** + * Pulls a built image and attempts to run it. + * + * @param imageReference the image reference of the built image + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference) + throws IOException, InterruptedException { + localRegistry.pull(imageReference); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + @BeforeClass + public static void setUpClass() throws IOException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + + @Before + public void setUp() { + System.setProperty("sendCredentialsOverHttp", "true"); + } + + @After + public void tearDown() throws IOException, InterruptedException { + System.clearProperty("sendCredentialsOverHttp"); + if (imageToDelete != null) { + new Command("docker", "rmi", imageToDelete).run(); + } + } + + @Test + public void testBasic_helloWorld() + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { + String toImage = dockerHost + ":5000/basic-helloworld"; + JibContainer jibContainer = + Jib.from(dockerHost + ":5000/busybox") + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + imageToDelete = toImage; + } + + @Test + public void testBasic_dockerDaemonBaseImage() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + String toImage = dockerHost + ":5000/basic-dockerdaemon"; + JibContainer jibContainer = + Jib.from("docker://" + dockerHost + ":5000/busybox") + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + imageToDelete = toImage; + } + + @Test + public void testBasic_dockerDaemonBaseImageToDockerDaemon() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + String toImage = dockerHost + ":5000/docker-to-docker"; + Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox")) + .setEntrypoint("echo", "Hello World") + .containerize(Containerizer.to(DockerDaemonImage.named(toImage))); + + String output = new Command("docker", "run", "--rm", toImage).run(); + Assert.assertEquals("Hello World\n", output); + imageToDelete = toImage; + } + + @Test + public void testBasic_tarBaseImage_dockerSavedCommand() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + Path path = temporaryFolder.getRoot().toPath().resolve("docker-save.tar"); + new Command("docker", "save", dockerHost + ":5000/busybox", "-o=" + path).run(); + + String toImage = dockerHost + ":5000/basic-dockersavedcommand"; + JibContainer jibContainer = + Jib.from("tar://" + path) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + imageToDelete = toImage; + } + + @Test + public void testBasic_tarBaseImage_dockerSavedFile() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException, URISyntaxException { + // tar saved with 'docker save busybox -o busybox.tar' + Path path = Paths.get(Resources.getResource("core/busybox-docker.tar").toURI()); + + String toImage = dockerHost + ":5000/basic-dockersavedfile"; + JibContainer jibContainer = + Jib.from(TarImage.at(path).named("ignored")) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + } + + @Test + public void testBasic_tarBaseImage_jibImage() + throws InvalidImageReferenceException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException, IOException, URISyntaxException { + Path outputPath = temporaryFolder.getRoot().toPath().resolve("jib-image.tar"); + Jib.from(dockerHost + ":5000/busybox") + .addLayer( + Collections.singletonList(Paths.get(Resources.getResource("core/hello").toURI())), "/") + .containerize( + Containerizer.to(TarImage.at(outputPath).named("ignored")) + .setAllowInsecureRegistries(true)); + + String toImage = dockerHost + ":5000/basic-jibtar"; + JibContainer jibContainer = + Jib.from(TarImage.at(outputPath).named("ignored")) + .setEntrypoint("cat", "/hello") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + } + + @Test + public void testBasic_tarBaseImage_jibImageToDockerDaemon() + throws InvalidImageReferenceException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException, IOException, URISyntaxException { + // tar saved with Jib.from("busybox").addLayer(...("core/hello")).containerize(TarImage.at...) + Path path = Paths.get(Resources.getResource("core/busybox-jib.tar").toURI()); + + String toImage = dockerHost + ":5000/basic-jibtar-to-docker"; + JibContainer jibContainer = + Jib.from(TarImage.at(path).named("ignored")) + .setEntrypoint("cat", "/hello") + .containerize( + Containerizer.to(RegistryImage.named(toImage)).setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage)); + Assert.assertEquals( + "Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest())); + imageToDelete = toImage; + } + + @Test + public void testScratch_defaultPlatform() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Jib.fromScratch() + .containerize( + Containerizer.to(RegistryImage.named(dockerHost + ":5000/jib-scratch:default-platform")) + .setAllowInsecureRegistries(true)); + + V22ManifestTemplate manifestTemplate = + registryClient.pullManifest("default-platform", V22ManifestTemplate.class).getManifest(); + String containerConfig = + Blobs.writeToString( + registryClient.pullBlob( + manifestTemplate.getContainerConfiguration().getDigest(), + ignored -> {}, + ignored -> {})); + + Assert.assertTrue(manifestTemplate.getLayers().isEmpty()); + Assert.assertTrue(containerConfig.contains("\"architecture\":\"amd64\"")); + Assert.assertTrue(containerConfig.contains("\"os\":\"linux\"")); + } + + @Test + public void testScratch_singlePlatform() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Jib.fromScratch() + .setPlatforms(ImmutableSet.of(new Platform("arm64", "windows"))) + .containerize( + Containerizer.to(RegistryImage.named(dockerHost + ":5000/jib-scratch:single-platform")) + .setAllowInsecureRegistries(true)); + + V22ManifestTemplate manifestTemplate = + registryClient.pullManifest("single-platform", V22ManifestTemplate.class).getManifest(); + String containerConfig = + Blobs.writeToString( + registryClient.pullBlob( + manifestTemplate.getContainerConfiguration().getDigest(), + ignored -> {}, + ignored -> {})); + + Assert.assertTrue(manifestTemplate.getLayers().isEmpty()); + Assert.assertTrue(containerConfig.contains("\"architecture\":\"arm64\"")); + Assert.assertTrue(containerConfig.contains("\"os\":\"windows\"")); + } + + @Test + public void testScratch_multiPlatform() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Jib.fromScratch() + .setPlatforms( + ImmutableSet.of(new Platform("arm64", "windows"), new Platform("amd32", "windows"))) + .containerize( + Containerizer.to(RegistryImage.named(dockerHost + ":5000/jib-scratch:multi-platform")) + .setAllowInsecureRegistries(true)); + + V22ManifestListTemplate manifestList = + (V22ManifestListTemplate) registryClient.pullManifest("multi-platform").getManifest(); + Assert.assertEquals(2, manifestList.getManifests().size()); + ManifestDescriptorTemplate.Platform platform1 = + manifestList.getManifests().get(0).getPlatform(); + ManifestDescriptorTemplate.Platform platform2 = + manifestList.getManifests().get(1).getPlatform(); + + Assert.assertEquals("arm64", platform1.getArchitecture()); + Assert.assertEquals("windows", platform1.getOs()); + Assert.assertEquals("amd32", platform2.getArchitecture()); + Assert.assertEquals("windows", platform2.getOs()); + } + + @Test + public void testBasic_jibImageToDockerDaemon() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + String toImage = dockerHost + ":5000/docker-to-docker"; + Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox")) + .setEntrypoint("echo", "Hello World") + .containerize(Containerizer.to(DockerDaemonImage.named(toImage))); + + String output = new Command("docker", "run", "--rm", toImage).run(); + Assert.assertEquals("Hello World\n", output); + imageToDelete = toImage; + } + + @Test + public void testDistroless_ociManifest() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Jib.from("gcr.io/distroless/base@" + KNOWN_OCI_INDEX_SHA) + .setPlatforms( + ImmutableSet.of(new Platform("arm64", "linux"), new Platform("amd64", "linux"))) + .containerize( + Containerizer.to( + RegistryImage.named(dockerHost + ":5000/jib-distroless:multi-platform")) + .setAllowInsecureRegistries(true)); + + V22ManifestListTemplate manifestList = + (V22ManifestListTemplate) + distrolessRegistryClient.pullManifest("multi-platform").getManifest(); + Assert.assertEquals(2, manifestList.getManifests().size()); + ManifestDescriptorTemplate.Platform platform1 = + manifestList.getManifests().get(0).getPlatform(); + ManifestDescriptorTemplate.Platform platform2 = + manifestList.getManifests().get(1).getPlatform(); + + Assert.assertEquals("arm64", platform1.getArchitecture()); + Assert.assertEquals("linux", platform1.getOs()); + Assert.assertEquals("amd64", platform2.getArchitecture()); + Assert.assertEquals("linux", platform2.getOs()); + } + + @Test + public void testOffline() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + Path cacheDirectory = temporaryFolder.getRoot().toPath(); + + JibContainerBuilder jibContainerBuilder = + Jib.from(dockerHost + ":5000/busybox").setEntrypoint("echo", "Hello World"); + + // Should fail since Jib can't build to registry offline + try { + jibContainerBuilder.containerize( + Containerizer.to(RegistryImage.named("ignored")).setOfflineMode(true)); + Assert.fail(); + } catch (IllegalStateException ex) { + Assert.assertEquals("Cannot build to a container registry in offline mode", ex.getMessage()); + } + + // Should fail since Jib hasn't cached the base image yet + try { + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named("ignored")) + .setBaseImageLayersCache(cacheDirectory) + .setOfflineMode(true)); + Assert.fail(); + } catch (ExecutionException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; " + + dockerHost + + ":5000/busybox not found in local Jib cache", + ex.getCause().getMessage()); + } + + // Run online to cache the base image + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named("ignored")) + .setBaseImageLayersCache(cacheDirectory) + .setAllowInsecureRegistries(true)); + + // Run again in offline mode, should succeed this time + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named(dockerHost + ":5000/offline")) + .setBaseImageLayersCache(cacheDirectory) + .setOfflineMode(true)); + + // Verify output + Assert.assertEquals( + "Hello World\n", new Command("docker", "run", "--rm", dockerHost + ":5000/offline").run()); + } + + /** Ensure that a provided executor is not disposed. */ + @Test + public void testProvidedExecutorNotDisposed() + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { + ExecutorService executorService = Executors.newCachedThreadPool(); + try { + Jib.fromScratch() + .containerize( + Containerizer.to(RegistryImage.named(dockerHost + ":5000/foo")) + .setExecutorService(executorService) + .setAllowInsecureRegistries(true)); + Assert.assertFalse(executorService.isShutdown()); + } finally { + executorService.shutdown(); + } + } + + @Test + public void testManifestListReferenceByShaDoesNotFail() + throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + Containerizer containerizer = + Containerizer.to(TarImage.at(temporaryFolder.newFile("goose").toPath()).named("whatever")); + + Jib.from("gcr.io/distroless/base@" + ManifestPullerIntegrationTest.KNOWN_MANIFEST_LIST_SHA) + .containerize(containerizer); + // pass, no exceptions thrown + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibMultiPlatformIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibMultiPlatformIntegrationTest.java new file mode 100644 index 0000000000..df00fe04d0 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibMultiPlatformIntegrationTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +public class JibMultiPlatformIntegrationTest { + + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + + private final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + private String imageToDelete; + + @After + public void tearDown() throws IOException, InterruptedException { + System.clearProperty("sendCredentialsOverHttp"); + if (imageToDelete != null) { + new Command("docker", "rmi", imageToDelete).run(); + } + } + + @Test + public void testBasic_jibImageToDockerDaemon_arm64() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + // Use arm64v8/busybox as base image. + String toImage = dockerHost + ":5000/docker-daemon-mismatched-arch"; + Jib.from( + RegistryImage.named( + "busybox@sha256:eb427d855f82782c110b48b9a398556c629ce4951ae252c6f6751a136e194668")) + .containerize(Containerizer.to(DockerDaemonImage.named(toImage))); + String os = + new Command("docker", "inspect", toImage, "--format", "{{.Os}}").run().replace("\n", ""); + String architecture = + new Command("docker", "inspect", toImage, "--format", "{{.Architecture}}") + .run() + .replace("\n", ""); + assertThat(os).isEqualTo("linux"); + assertThat(architecture).isEqualTo("arm64"); + imageToDelete = toImage; + } + + @Test + public void testBasicMultiPlatform_toDockerDaemon_pickFirstPlatformWhenNoMatchingImage() + throws IOException, InterruptedException, InvalidImageReferenceException, + CacheDirectoryCreationException, ExecutionException, RegistryException { + String toImage = dockerHost + ":5000/docker-daemon-multi-plat-mismatched-configs"; + Jib.from( + RegistryImage.named( + "busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977")) + .setPlatforms(ImmutableSet.of(new Platform("s390x", "linux"), new Platform("arm", "linux"))) + .containerize( + Containerizer.to(DockerDaemonImage.named(toImage)).setAllowInsecureRegistries(true)); + String os = + new Command("docker", "inspect", toImage, "--format", "{{.Os}}").run().replace("\n", ""); + String architecture = + new Command("docker", "inspect", toImage, "--format", "{{.Architecture}}") + .run() + .replace("\n", ""); + assertThat(os).isEqualTo("linux"); + assertThat(architecture).isEqualTo("s390x"); + imageToDelete = toImage; + } + + @Test + public void testBasicMultiPlatform_toDockerDaemon() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + String toImage = dockerHost + ":5000/docker-daemon-multi-platform"; + Jib.from( + RegistryImage.named( + "busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977")) + .setPlatforms( + ImmutableSet.of(new Platform("arm64", "linux"), new Platform("amd64", "linux"))) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(DockerDaemonImage.named(toImage)).setAllowInsecureRegistries(true)); + + String output = new Command("docker", "run", "--rm", toImage).run(); + Assert.assertEquals("Hello World\n", output); + imageToDelete = toImage; + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java new file mode 100644 index 0000000000..3eb44434e8 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.io.CharStreams; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; +import java.util.zip.GZIPInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Verify that created image has explicit directory structures, default timestamps, permissions, and + * file orderings. + */ +public class ReproducibleImageTest { + + @ClassRule public static final TemporaryFolder imageLocation = new TemporaryFolder(); + + private static File imageTar; + + @BeforeClass + public static void createImage() + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { + + Path root = imageLocation.getRoot().toPath(); + Path fileA = Files.createFile(root.resolve("fileA.txt")); + Path fileB = Files.createFile(root.resolve("fileB.txt")); + Path fileC = Files.createFile(root.resolve("fileC.txt")); + Path subdir = Files.createDirectory(root.resolve("dir")); + Path subsubdir = Files.createDirectory(subdir.resolve("subdir")); + Files.createFile(subdir.resolve("fileD.txt")); + Files.createFile(subsubdir.resolve("fileE.txt")); + + imageTar = new File(imageLocation.getRoot(), "image.tar"); + Containerizer containerizer = + Containerizer.to(TarImage.at(imageTar.toPath()).named("jib-core/reproducible")); + + Jib.fromScratch() + .setEntrypoint("echo", "Hello World") + .addLayer(ImmutableList.of(fileA), AbsoluteUnixPath.get("/app")) + // layer with out-of-order files + .addLayer(ImmutableList.of(fileC, fileB), "/app") + .addFileEntriesLayer( + FileEntriesLayer.builder() + .addEntryRecursive(subdir, AbsoluteUnixPath.get("/app")) + .build()) + .containerize(containerizer); + } + + @Test + public void testTarballStructure() throws IOException { + // known content should produce known results + List actual = new ArrayList<>(); + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(imageTar.toPath()))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextEntry()) != null) { + actual.add(imageEntry.getName()); + } + } + + assertThat(actual) + .containsExactly( + "98682a867906d9d07cf3c51a4fb9e08e9d5baddd1ca5dc7834f58f434c9cb15c.tar.gz", + "527db49d4e0c4159346119b4971d59016bfedceed874abab2b510ce433f6b15c.tar.gz", + "16d03883198935b4119896dcea0ea14e1bf105b6ac0a35a88820d08bc0263306.tar.gz", + "config.json", + "manifest.json") + .inOrder(); + } + + @Test + public void testManifest() throws IOException { + String expectedManifest = + "[{\"Config\":\"config.json\",\"RepoTags\":[\"jib-core/reproducible:latest\"]," + + "\"Layers\":[\"98682a867906d9d07cf3c51a4fb9e08e9d5baddd1ca5dc7834f58f434c9cb15c.tar.gz\",\"527db49d4e0c4159346119b4971d59016bfedceed874abab2b510ce433f6b15c.tar.gz\",\"16d03883198935b4119896dcea0ea14e1bf105b6ac0a35a88820d08bc0263306.tar.gz\"]}]"; + String generatedManifest = extractFromTarFileAsString(imageTar, "manifest.json"); + assertThat(generatedManifest).isEqualTo(expectedManifest); + } + + @Test + public void testConfiguration() throws IOException { + String expectedConfig = + "{\"created\":\"1970-01-01T00:00:00Z\",\"architecture\":\"amd64\",\"os\":\"linux\"," + + "\"config\":{\"Env\":[],\"Entrypoint\":[\"echo\",\"Hello World\"],\"ExposedPorts\":{},\"Labels\":{},\"Volumes\":{}}," + + "\"history\":[{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"}]," + + "\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:2fcc2157bf42c89195676ef6e973a96d7b018c9d30ba89db95e9e0722e1c8ef3\",\"sha256:21f521f3217067d277af37512a08c72281d90fdd02d7174db632c8c3a34403bd\",\"sha256:6beba018395265af5061864b7f4678e831eb2daebb1045487c641fc8b142e319\"]}}"; + String generatedConfig = extractFromTarFileAsString(imageTar, "config.json"); + assertThat(generatedConfig).isEqualTo(expectedConfig); + } + + @Test + public void testImageLayout() throws IOException { + Set paths = new HashSet<>(); + layerEntriesDo( + (layerName, layerEntry) -> { + if (layerEntry.isFile()) { + paths.add(layerEntry.getName()); + } + }); + assertThat(paths) + .containsExactly( + "app/fileA.txt", + "app/fileB.txt", + "app/fileC.txt", + "app/fileD.txt", + "app/subdir/fileE.txt"); + } + + @Test + public void testAllFileAndDirectories() throws IOException { + layerEntriesDo( + (layerName, layerEntry) -> + assertThat(layerEntry.isFile() || layerEntry.isDirectory()).isTrue()); + } + + @Test + public void testTimestampsEpochPlus1s() throws IOException { + layerEntriesDo( + (layerName, layerEntry) -> { + Instant modificationTime = layerEntry.getLastModifiedDate().toInstant(); + assertThat(modificationTime).isEqualTo(Instant.ofEpochSecond(1)); + }); + } + + @Test + public void testPermissions() throws IOException { + assertThat(FilePermissions.DEFAULT_FILE_PERMISSIONS.getPermissionBits()).isEqualTo(0644); + assertThat(FilePermissions.DEFAULT_FOLDER_PERMISSIONS.getPermissionBits()).isEqualTo(0755); + layerEntriesDo( + (layerName, layerEntry) -> { + if (layerEntry.isFile()) { + assertThat(layerEntry.getMode() & 0777).isEqualTo(0644); + } else if (layerEntry.isDirectory()) { + assertThat(layerEntry.getMode() & 0777).isEqualTo(0755); + } + }); + } + + @Test + public void testNoImplicitParentDirectories() throws IOException { + Set directories = new HashSet<>(); + layerEntriesDo( + (layerName, layerEntry) -> { + String entryPath = layerEntry.getName(); + if (layerEntry.isDirectory()) { + assertThat(entryPath.endsWith("/")).isTrue(); + entryPath = entryPath.substring(0, entryPath.length() - 1); + } + + int lastSlashPosition = entryPath.lastIndexOf('/'); + String parent = entryPath.substring(0, Math.max(0, lastSlashPosition)); + if (!parent.isEmpty()) { + assertThat(directories.contains(parent)).isTrue(); + } + if (layerEntry.isDirectory()) { + directories.add(entryPath); + } + }); + } + + @Test + public void testFileOrdering() throws IOException { + Multimap layerPaths = ArrayListMultimap.create(); + layerEntriesDo((layerName, layerEntry) -> layerPaths.put(layerName, layerEntry.getName())); + for (Collection paths : layerPaths.asMap().values()) { + List sorted = new ArrayList<>(paths); + // ReproducibleLayerBuilder sorts by TarArchiveEntry::getName() + Collections.sort(sorted); + assertThat(paths).containsExactlyElementsIn(sorted).inOrder(); + } + } + + private void layerEntriesDo(BiConsumer layerConsumer) + throws IOException { + + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(imageTar.toPath()))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextEntry()) != null) { + String imageEntryName = imageEntry.getName(); + // assume all .tar.gz files are layers + if (imageEntry.isFile() && imageEntryName.endsWith(".tar.gz")) { + @SuppressWarnings("resource") // must not close sub-streams + TarArchiveInputStream layer = new TarArchiveInputStream(new GZIPInputStream(input)); + TarArchiveEntry layerEntry; + while ((layerEntry = layer.getNextEntry()) != null) { + layerConsumer.accept(imageEntryName, layerEntry); + } + } + } + } + } + + private static String extractFromTarFileAsString(File tarFile, String filename) + throws IOException { + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(tarFile.toPath()))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextEntry()) != null) { + if (filename.equals(imageEntry.getName())) { + return CharStreams.toString(new InputStreamReader(input, StandardCharsets.UTF_8)); + } + } + } + throw new AssertionError("file not found: " + filename); + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java deleted file mode 100644 index a7449efa3d..0000000000 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.Caches; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.registry.LocalRegistry; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.ExecutionException; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Integration tests for {@link BuildSteps}. */ -public class BuildStepsIntegrationTest { - - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - - private static final TestBuildLogger logger = new TestBuildLogger(); - - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); - - private SourceFilesConfiguration sourceFilesConfiguration; - - @Before - public void setUp() throws IOException, URISyntaxException { - sourceFilesConfiguration = new TestSourceFilesConfiguration(); - } - - @Test - public void testSteps_forBuildToDockerRegistry() - throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - BuildSteps buildImageSteps = - getBuildSteps( - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) - .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setMainClass("HelloWorld") - .setJavaArguments(Collections.singletonList("An argument.")) - .setExposedPorts( - ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) - .setAllowHttp(true) - .build()); - - long lastTime = System.nanoTime(); - buildImageSteps.run(); - logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); - lastTime = System.nanoTime(); - buildImageSteps.run(); - logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); - - String imageReference = "localhost:5000/testimage:testtag"; - new Command("docker", "pull", imageReference).run(); - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/tcp\": {},\n" - + " \"2001/tcp\": {},\n" - + " \"2002/tcp\": {},\n" - + " \"3000/udp\": {}")); - Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); - } - - @Test - public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() - throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException, CacheMetadataCorruptedException, - CacheDirectoryNotOwnedException { - getBuildSteps( - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.parse("openjdk:8-jre-alpine")) - .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setMainClass("HelloWorld") - .setJavaArguments(Collections.singletonList("An argument.")) - .setAllowHttp(true) - .build()) - .run(); - - String imageReference = "localhost:5000/testimage:testtag"; - new Command("docker", "pull", imageReference).run(); - Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); - } - - @Test - public void testSteps_forBuildToDockerDaemon() - throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) - .setTargetImage(ImageReference.of(null, "testdocker", null)) - .setMainClass("HelloWorld") - .setJavaArguments(Collections.singletonList("An argument.")) - .setExposedPorts( - ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) - .build(); - - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - BuildSteps.forBuildToDockerDaemon( - buildConfiguration, - sourceFilesConfiguration, - new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) - .run(); - - Assert.assertThat( - new Command("docker", "inspect", "testdocker").run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/tcp\": {},\n" - + " \"2001/tcp\": {},\n" - + " \"2002/tcp\": {},\n" - + " \"3000/udp\": {}")); - Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", "testdocker").run()); - } - - private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws IOException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - return BuildSteps.forBuildToDockerRegistry( - buildConfiguration, - sourceFilesConfiguration, - new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)); - } -} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java deleted file mode 100644 index 30ba8796af..0000000000 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry; - -import com.google.cloud.tools.jib.http.Authorization; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; - -/** Integration tests for {@link AuthenticationMethodRetriever}. */ -public class AuthenticationMethodRetrieverIntegrationTest { - - @Test - public void testGetRegistryAuthenticator() - throws RegistryAuthenticationFailedException, IOException, RegistryException { - RegistryClient registryClient = - RegistryClient.factory("registry.hub.docker.com", "library/busybox").newRegistryClient(); - RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); - Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(); - - RegistryClient authorizedRegistryClient = - RegistryClient.factory("registry.hub.docker.com", "library/busybox") - .setAuthorization(authorization) - .newRegistryClient(); - authorizedRegistryClient.pullManifest("latest"); - } -} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java new file mode 100644 index 0000000000..fd62675406 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import java.io.IOException; +import org.junit.Test; + +/** Integration tests for bearer authentication. */ +public class BearerAuthenticationIntegrationTest { + + private final FailoverHttpClient httpClient = new FailoverHttpClient(false, false, ignored -> {}); + + @Test + public void testGetRegistryAuthenticator() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory( + EventHandlers.NONE, "registry.hub.docker.com", "library/busybox", httpClient) + .newRegistryClient(); + // For public images, Docker Hub still requires bearer authentication (without credentials) + registryClient.doPullBearerAuth(); + registryClient.pullManifest("latest"); + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 9f810a4342..508fbd51ee 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,38 +16,45 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; -import org.junit.ClassRule; import org.junit.Test; /** Integration tests for {@link BlobChecker}. */ public class BlobCheckerIntegrationTest { - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); @Test public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); V22ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V22ManifestTemplate.class); + registryClient + .pullManifest( + ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class) + .getManifest(); DescriptorDigest blobDigest = manifestTemplate.getLayers().get(0).getDigest(); - Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).getDigest()); + Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).get().getDigest()); } @Test public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); DescriptorDigest fakeBlobDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Assert.assertEquals(null, registryClient.checkBlob(fakeBlobDigest)); + Assert.assertFalse(registryClient.checkBlob(fakeBlobDigest).isPresent()); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 370244b7e5..1886e4c649 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,65 +16,79 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.io.ByteStreams; import java.io.IOException; -import java.io.OutputStream; import java.security.DigestException; +import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; /** Integration tests for {@link BlobPuller}. */ public class BlobPullerIntegrationTest { - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); @Test public void testPull() throws IOException, RegistryException { - // Pulls the busybox image. RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); - V21ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V21ManifestTemplate.class); - - DescriptorDigest realDigest = manifestTemplate.getLayerDigests().get(0); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + V22ManifestTemplate manifestTemplate = + registryClient + .pullManifest( + ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class) + .getManifest(); - // Pulls a layer BLOB of the busybox image. - CountingDigestOutputStream layerOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - registryClient.pullBlob(realDigest, layerOutputStream); + DescriptorDigest realDigest = manifestTemplate.getLayers().get(0).getDigest(); - Assert.assertEquals(realDigest, layerOutputStream.toBlobDescriptor().getDigest()); + // Pulls a layer BLOB of the distroless/base image. + LongAdder totalByteCount = new LongAdder(); + LongAdder expectedSize = new LongAdder(); + Blob pulledBlob = + registryClient.pullBlob( + realDigest, + size -> { + Assert.assertEquals(0, expectedSize.sum()); + expectedSize.add(size); + }, + totalByteCount::add); + Assert.assertEquals(realDigest, pulledBlob.writeTo(ByteStreams.nullOutputStream()).getDigest()); + Assert.assertTrue(expectedSize.sum() > 0); + Assert.assertEquals(expectedSize.sum(), totalByteCount.sum()); } @Test - public void testPull_unknownBlob() throws RegistryException, IOException, DigestException { + public void testPull_unknownBlob() throws IOException, DigestException { DescriptorDigest nonexistentDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + try { - RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") - .setAllowHttp(true) - .newRegistryClient(); - registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class)); + registryClient + .pullBlob(nonexistentDigest, ignored -> {}, ignored -> {}) + .writeTo(ByteStreams.nullOutputStream()); Assert.fail("Trying to pull nonexistent blob should have errored"); - } catch (RegistryErrorException ex) { - Assert.assertThat( + } catch (IOException ex) { + if (!(ex.getCause() instanceof RegistryErrorException)) { + throw ex; + } + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( - "pull BLOB for localhost:5000/busybox with digest " + nonexistentDigest)); + "pull BLOB for gcr.io/distroless/base with digest " + nonexistentDigest)); } } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 2f571eb68b..3d68e2ef63 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -28,7 +31,11 @@ /** Integration tests for {@link BlobPusher}. */ public class BlobPusherIntegrationTest { - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + private final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; @Test public void testPush() throws DigestException, IOException, RegistryException { @@ -39,9 +46,8 @@ public void testPush() throws DigestException, IOException, RegistryException { "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "testimage") - .setAllowHttp(true) + RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "testimage", httpClient) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob)); + Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 6b3759dffa..c6cb6a16c8 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,77 +17,167 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.Command; -import com.google.common.io.CharStreams; +import com.google.common.collect.Lists; import java.io.IOException; -import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Arrays; +import java.util.List; +import java.util.Set; import java.util.UUID; +import javax.annotation.Nullable; import org.junit.rules.ExternalResource; -import org.junit.rules.TestRule; +import org.mindrot.jbcrypt.BCrypt; -/** {@link TestRule} that runs a local registry. */ +/** Runs a local registry. */ public class LocalRegistry extends ExternalResource { - private final int port; - - /** The name for the container running the registry. */ private final String containerName = "registry-" + UUID.randomUUID(); + public final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + private final int port; + @Nullable private final String username; + @Nullable private final String password; public LocalRegistry(int port) { + this(port, null, null); + } + + public LocalRegistry(int port, String username, String password) { this.port = port; + this.username = username; + this.password = password; } /** Starts the local registry. */ @Override - protected void before() throws Throwable { - // Runs the Docker registry. - new Command( - "docker", - "run", - "-d", - "-p", - port + ":5000", - "--restart=always", - "--name", - containerName, - "registry:2") - .run(); - - // Pulls 'busybox'. - new Command("docker", "pull", "busybox").run(); - - // Tags 'busybox' to push to our local registry. - new Command("docker", "tag", "busybox", "localhost:" + port + "/busybox").run(); - - // Pushes 'busybox' to our local registry. - new Command("docker", "push", "localhost:" + port + "/busybox").run(); + protected void before() throws IOException, InterruptedException { + start(); } - /** Stops the local registry. */ @Override protected void after() { + stop(); + } + + /** Starts the registry. */ + public void start() throws IOException, InterruptedException { + // Runs the Docker registry. + List dockerTokens = + Lists.newArrayList( + "docker", "run", "--rm", "-d", "-p", port + ":5000", "--name", containerName); + if (username != null && password != null) { + // Equivalent of "$ htpasswd -nbB username password". + // https://httpd.apache.org/docs/2.4/misc/password_encryptions.html + // BCrypt generates hashes using $2a$ algorithm (instead of $2y$ from docs), but this seems + // to work okay + String credentialString = username + ":" + BCrypt.hashpw(password, BCrypt.gensalt()); + FileAttribute> attrs = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x")); + Path tempFolder = Files.createTempDirectory(Paths.get("/tmp"), "", attrs); + Files.write( + tempFolder.resolve("htpasswd"), credentialString.getBytes(StandardCharsets.UTF_8)); + String authenticationVolume = tempFolder + ":/auth"; + if (System.getenv("KOKORO_JOB_CLUSTER") != null + && System.getenv("KOKORO_JOB_CLUSTER").equals("MACOS_EXTERNAL")) { + authenticationVolume = "/home/docker/auth:/auth"; + } else if (System.getenv("KOKORO_JOB_CLUSTER") != null + && System.getenv("KOKORO_JOB_CLUSTER").equals("GCP_UBUNTU_DOCKER")) { + authenticationVolume = "/tmpfs/auth:/auth"; + } + // Run the Docker registry + dockerTokens.addAll( + Arrays.asList( + "-v", + // Volume mount used for storing credentials + authenticationVolume, + "-e", + "REGISTRY_AUTH=htpasswd", + "-e", + "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "-e", + "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd")); + } + dockerTokens.add("registry:2"); + new Command(dockerTokens).run(); + waitUntilReady(); + } + + /** Stops the registry. */ + public void stop() { try { - // Stops the registry. + logout(); new Command("docker", "stop", containerName).run(); - // Removes the container. - new Command("docker", "rm", "-v", containerName).run(); - } catch (InterruptedException | IOException ex) { throw new RuntimeException("Could not stop local registry fully: " + containerName, ex); } } - private void printLogs() throws IOException, InterruptedException { - Process process = Runtime.getRuntime().exec("docker logs " + containerName); - try (InputStreamReader inputStreamReader = - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { - System.out.println(CharStreams.toString(inputStreamReader)); + /** + * Pulls an image to a Docker daemon (not to this local registry). + * + * @param from the image reference to pull + * @throws IOException if the pull command fails + * @throws InterruptedException if the pull command is interrupted + */ + public void pull(String from) throws IOException, InterruptedException { + login(); + new Command("docker", "pull", from).run(); + logout(); + } + + /** + * Pulls an image and pushes it to the local registry under a new tag. + * + * @param from the image reference to pull + * @param to the new location of the image (i.e. {@code localhost:[port]/[to]} + * @throws IOException if the commands fail + * @throws InterruptedException if the commands are interrupted + */ + public void pullAndPushToLocal(String from, String to) throws IOException, InterruptedException { + login(); + new Command("docker", "pull", from).run(); + new Command("docker", "tag", from, dockerHost + ":" + port + "/" + to).run(); + new Command("docker", "push", dockerHost + ":" + port + "/" + to).run(); + logout(); + } + + private void login() throws IOException, InterruptedException { + if (username != null && password != null) { + new Command("docker", "login", dockerHost + ":" + port, "-u", username, "--password-stdin") + .run(password.getBytes(StandardCharsets.UTF_8)); } - try (InputStreamReader inputStreamReader = - new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { - System.err.println(CharStreams.toString(inputStreamReader)); + } + + private void logout() throws IOException, InterruptedException { + if (username != null && password != null) { + new Command("docker", "logout", dockerHost + ":" + port).run(); + } + } + + private void waitUntilReady() throws InterruptedException, MalformedURLException { + URL queryUrl = new URL("http://" + dockerHost + ":" + port + "/v2/_catalog"); + + for (int i = 0; i < 40; i++) { + try { + HttpURLConnection connection = (HttpURLConnection) queryUrl.openConnection(); + int code = connection.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK || code == HttpURLConnection.HTTP_UNAUTHORIZED) { + return; + } + } catch (IOException ex) { + // ignored + } + Thread.sleep(250); } - process.waitFor(); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestCheckerIntegrationTest.java new file mode 100644 index 0000000000..fa8d5c7a38 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestCheckerIntegrationTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import java.io.IOException; +import java.util.Optional; +import org.junit.Test; + +/** Integration tests for {@link ManifestChecker}. */ +public class ManifestCheckerIntegrationTest { + + /** A known manifest list sha for gcr.io/distroless/base. */ + private static final String KNOWN_MANIFEST = + "sha256:44cbdb9c24e123882d7894ba78fb6f572d2496889885a47eb4b32241a8c07a00"; + + /** A fictitious sha to test unknown images. */ + private static final String UNKNOWN_MANIFEST = + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + + @Test + public void testExistingManifest() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + Optional> manifestDescriptor = + registryClient.checkManifest(KNOWN_MANIFEST); + + assertTrue(manifestDescriptor.isPresent()); + assertEquals(KNOWN_MANIFEST, manifestDescriptor.get().getDigest().toString()); + } + + @Test + public void testNonExistingManifest() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + Optional> manifestDescriptor = + registryClient.checkManifest(UNKNOWN_MANIFEST); + + assertEquals(Optional.empty(), manifestDescriptor); + } +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 40a1e7dd15..da6f058ccb 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,57 +16,151 @@ package com.google.cloud.tools.jib.registry; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; -import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; /** Integration tests for {@link ManifestPuller}. */ public class ManifestPullerIntegrationTest { + /** A known manifest list sha for gcr.io/distroless/base. */ + public static final String KNOWN_MANIFEST_LIST_SHA = + "sha256:44cbdb9c24e123882d7894ba78fb6f572d2496889885a47eb4b32241a8c07a00"; + + /** A known OCI image index sha for gcr.io/distroless/base. */ + public static final String KNOWN_OCI_INDEX_SHA = + "sha256:2c50b819aa3bfaf6ae72e47682f6c5abc0f647cf3f4224a4a9be97dd30433909"; + + /** A known docker manifest schema 2 sha for gcr.io/distroless/base. */ + public static final String KNOWN_MANIFEST_V22_SHA = + "sha256:da5c568e59f3241b09e5699a525a37b3309ce2c182d8d20802b9eaee55711b19"; + + /** A known oci manifest sha for gcr.io/distroless/base. */ + public static final String KNOWN_OCI_MANIFEST_SHA = + "sha256:0477dc38b254096e350a9b605b7355d3cf0d5a844558e6986148ce2a1fe18ba8"; + @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + public final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + + @BeforeClass + public static void setUp() throws IOException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); @Test public void testPull_v21() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "busybox", httpClient) + .newRegistryClient(); + V21ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V21ManifestTemplate.class); + registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); - Assert.assertEquals(1, manifestTemplate.getSchemaVersion()); - Assert.assertTrue(manifestTemplate.getFsLayers().size() > 0); + assertThat(manifestTemplate.getSchemaVersion()).isEqualTo(1); + assertThat(manifestTemplate.getFsLayers()).isNotEmpty(); } @Test public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("gcr.io", "distroless/java").newRegistryClient(); - ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + V22ManifestTemplate manifestTemplate = + registryClient + .pullManifest(KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class) + .getManifest(); + + assertThat(manifestTemplate.getSchemaVersion()).isEqualTo(2); + assertThat(manifestTemplate.getLayers()).isNotEmpty(); + } + + @Test + public void testPull_ociManifest() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + OciManifestTemplate manifestTemplate = + registryClient + .pullManifest(KNOWN_OCI_MANIFEST_SHA, OciManifestTemplate.class) + .getManifest(); + + assertThat(manifestTemplate.getSchemaVersion()).isEqualTo(2); + assertThat(manifestTemplate.getLayers()).isNotEmpty(); + } + + @Test + public void testPull_v22ManifestList() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + // Ensures call to image at the specified SHA returns a manifest list + V22ManifestListTemplate manifestListTargeted = + registryClient + .pullManifest(KNOWN_MANIFEST_LIST_SHA, V22ManifestListTemplate.class) + .getManifest(); + assertThat(manifestListTargeted.getSchemaVersion()).isEqualTo(2); + assertThat(manifestListTargeted.getManifests()).isNotEmpty(); + + // Generic call to image at the specified SHA, should also return a manifest list + ManifestTemplate manifestListGeneric = + registryClient.pullManifest(KNOWN_MANIFEST_LIST_SHA).getManifest(); + assertThat(manifestListGeneric.getSchemaVersion()).isEqualTo(2); + assertThat(manifestListGeneric).isInstanceOf(V22ManifestListTemplate.class); + assertThat(((V22ManifestListTemplate) manifestListGeneric).getManifests()).isNotEmpty(); + } + + @Test + public void testPull_ociIndex() throws IOException, RegistryException { + RegistryClient registryClient = + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) + .newRegistryClient(); + + // Ensures call to image at the specified SHA returns an OCI index + OciIndexTemplate manifestListTargeted = + registryClient.pullManifest(KNOWN_OCI_INDEX_SHA, OciIndexTemplate.class).getManifest(); + assertThat(manifestListTargeted.getSchemaVersion()).isEqualTo(2); + assertThat((manifestListTargeted.getManifests().size() > 0)).isTrue(); - Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); - V22ManifestTemplate v22ManifestTemplate = (V22ManifestTemplate) manifestTemplate; - Assert.assertTrue(v22ManifestTemplate.getLayers().size() > 0); + // Generic call to image at the specified SHA, should also return an OCI index + ManifestTemplate manifestListGeneric = + registryClient.pullManifest(KNOWN_OCI_INDEX_SHA).getManifest(); + assertThat(manifestListGeneric.getSchemaVersion()).isEqualTo(2); + assertThat(manifestListGeneric).isInstanceOf(OciIndexTemplate.class); + assertThat(((OciIndexTemplate) manifestListGeneric).getManifests()).isNotEmpty(); } @Test public void testPull_unknownManifest() throws RegistryException, IOException { try { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") - .setAllowHttp(true) + RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "busybox", httpClient) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); Assert.fail("Trying to pull nonexistent image should have errored"); } catch (RegistryErrorException ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "pull image manifest for localhost:5000/busybox:nonexistent-tag")); + assertThat(ex) + .hasMessageThat() + .contains("pull image manifest for " + dockerHost + ":5000/busybox:nonexistent-tag"); } } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 2834e7cd0a..478762d893 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,11 +16,15 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -32,24 +36,30 @@ /** Integration tests for {@link ManifestPusher}. */ public class ManifestPusherIntegrationTest { - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + public final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; @Test public void testPush_missingBlobs() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("gcr.io", "distroless/java").newRegistryClient(); - ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java", httpClient) + .newRegistryClient(); + ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "ignored", httpClient) + .newRegistryClient(); try { - registryClient.pushManifest((V22ManifestTemplate) manifestTemplate, "latest"); + registryClient.pushManifest(manifestTemplate, "latest"); Assert.fail("Pushing manifest without its BLOBs should fail"); } catch (RegistryErrorException ex) { - HttpResponseException httpResponseException = (HttpResponseException) ex.getCause(); + ResponseException responseException = (ResponseException) ex.getCause(); Assert.assertEquals( - HttpStatusCodes.STATUS_CODE_BAD_REQUEST, httpResponseException.getStatusCode()); + HttpStatusCodes.STATUS_CODE_BAD_REQUEST, responseException.getStatusCode()); } } @@ -73,24 +83,37 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "testimage") - .setAllowHttp(true) + RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "testimage", httpClient) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob)); + Assert.assertFalse( + registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); Assert.assertFalse( registryClient.pushBlob( - testContainerConfigurationBlobDigest, testContainerConfigurationBlob)); + testContainerConfigurationBlobDigest, + testContainerConfigurationBlob, + null, + ignored -> {})); // Pushes the manifest. - registryClient.pushManifest(expectedManifestTemplate, "latest"); + DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); // Pulls the manifest. V22ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V22ManifestTemplate.class); + registryClient.pullManifest("latest", V22ManifestTemplate.class).getManifest(); Assert.assertEquals(1, manifestTemplate.getLayers().size()); Assert.assertEquals(testLayerBlobDigest, manifestTemplate.getLayers().get(0).getDigest()); + Assert.assertNotNull(manifestTemplate.getContainerConfiguration()); Assert.assertEquals( testContainerConfigurationBlobDigest, manifestTemplate.getContainerConfiguration().getDigest()); + + // Pulls the manifest by digest. + V22ManifestTemplate manifestTemplateByDigest = + registryClient + .pullManifest(imageDigest.toString(), V22ManifestTemplate.class) + .getManifest(); + Assert.assertEquals( + Digests.computeJsonDigest(manifestTemplate), + Digests.computeJsonDigest(manifestTemplateByDigest)); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java deleted file mode 100644 index dd482adc0f..0000000000 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry; - -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; - -/** Integration tests for {@link RegistryAuthenticator}. */ -public class RegistryAuthenticatorIntegrationTest { - - @Test - public void testAuthenticate() - throws RegistryAuthenticationFailedException, InvalidImageReferenceException, IOException, - RegistryException { - ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) - .initialize(); - Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(); - - // Checks that some token was received. - Assert.assertTrue(0 < authorization.getToken().length()); - } -} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index d2093a8269..bda754c9b2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,66 +17,68 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.api.Credential; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; /** Integration tests for {@link DockerCredentialHelper}. */ public class DockerCredentialHelperIntegrationTest { + // This binary must exist and be functioning properly for these tests to succeed. + private static final String GCR_CREDENTIAL_HELPER = "docker-credential-gcr"; + /** Tests retrieval via {@code docker-credential-gcr} CLI. */ @Test - public void testRetrieveGCR() - throws IOException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, URISyntaxException, InterruptedException { - new Command("docker-credential-gcr", "store") + public void testRetrieveGcr() + throws IOException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException, URISyntaxException, InterruptedException { + new Command(GCR_CREDENTIAL_HELPER, "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); DockerCredentialHelper dockerCredentialHelper = - new DockerCredentialHelperFactory("myregistry").withCredentialHelperSuffix("gcr"); - - Authorization authorization = dockerCredentialHelper.retrieve(); + new DockerCredentialHelper("myregistry", Paths.get(GCR_CREDENTIAL_HELPER)); - // Checks that token received was base64 encoding of "myusername:mysecret". - Assert.assertEquals("bXl1c2VybmFtZTpteXNlY3JldA==", authorization.getToken()); + Credential credentials = dockerCredentialHelper.retrieve(); + Assert.assertEquals("myusername", credentials.getUsername()); + Assert.assertEquals("mysecret", credentials.getPassword()); } @Test public void testRetrieve_nonexistentCredentialHelper() - throws IOException, NonexistentServerUrlDockerCredentialHelperException { + throws IOException, CredentialHelperUnhandledServerUrlException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory("").withCredentialHelperSuffix("fake-cloud-provider"); + new DockerCredentialHelper("", Paths.get("non-existing-helper")); fakeDockerCredentialHelper.retrieve(); Assert.fail("Retrieve should have failed for nonexistent credential helper"); - } catch (NonexistentDockerCredentialHelperException ex) { - Assert.assertEquals( - "The system does not have docker-credential-fake-cloud-provider CLI", ex.getMessage()); + } catch (CredentialHelperNotFoundException ex) { + Assert.assertEquals("The system does not have non-existing-helper CLI", ex.getMessage()); } } @Test public void testRetrieve_nonexistentServerUrl() - throws IOException, NonexistentDockerCredentialHelperException { + throws IOException, CredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory("fake.server.url").withCredentialHelperSuffix("gcr"); + new DockerCredentialHelper("fake.server.url", Paths.get(GCR_CREDENTIAL_HELPER)); fakeDockerCredentialHelper.retrieve(); Assert.fail("Retrieve should have failed for nonexistent server URL"); - } catch (NonexistentServerUrlDockerCredentialHelperException ex) { - Assert.assertThat( + } catch (CredentialHelperUnhandledServerUrlException ex) { + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( "The credential helper (docker-credential-gcr) has nothing for server URL: fake.server.url")); diff --git a/jib-core/src/integration-test/resources/core/busybox-docker.tar b/jib-core/src/integration-test/resources/core/busybox-docker.tar new file mode 100644 index 0000000000..f17c980af9 Binary files /dev/null and b/jib-core/src/integration-test/resources/core/busybox-docker.tar differ diff --git a/jib-core/src/integration-test/resources/core/busybox-jib.tar b/jib-core/src/integration-test/resources/core/busybox-jib.tar new file mode 100644 index 0000000000..9bef1e80a1 Binary files /dev/null and b/jib-core/src/integration-test/resources/core/busybox-jib.tar differ diff --git a/jib-core/src/integration-test/resources/core/hello b/jib-core/src/integration-test/resources/core/hello new file mode 100644 index 0000000000..557db03de9 --- /dev/null +++ b/jib-core/src/integration-test/resources/core/hello @@ -0,0 +1 @@ +Hello World diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java index c9ae060cb9..72d80c55be 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java deleted file mode 100644 index 64ed458ff2..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib; - -import com.google.cloud.tools.jib.builder.BuildLogger; -import java.io.Closeable; -import javax.annotation.Nullable; - -/** - * Times execution intervals. This is only for testing purposes and will be removed before the first - * release. - */ -public class Timer implements Closeable { - - private final BuildLogger buildLogger; - private final int depth; - - @Nullable private String label; - private long startTime = System.nanoTime(); - - public Timer(BuildLogger buildLogger, String label) { - this(buildLogger, label, 0); - } - - private Timer(BuildLogger buildLogger, String label, int depth) { - this.buildLogger = buildLogger; - this.label = label; - this.depth = depth; - - if (buildLogger != null) { - buildLogger.debug(getTabs().append("TIMING\t").append(label)); - if (depth == 0) { - buildLogger.info("RUNNING\t" + label); - } - } - } - - public Timer subTimer(String label) { - return new Timer(buildLogger, label, depth + 1); - } - - public void lap(@Nullable String label) { - if (this.label == null) { - throw new IllegalStateException("Tried to lap Timer after closing"); - } - if (buildLogger != null) { - double timeInMillis = (System.nanoTime() - startTime) / 1000 / 1000.0; - buildLogger.debug( - getTabs() - .append("TIMED\t") - .append(this.label) - .append(" : ") - .append(timeInMillis) - .append(" ms")); - if (depth == 0) { - buildLogger.info(this.label + " : " + timeInMillis + " ms"); - } - } - this.label = label; - startTime = System.nanoTime(); - } - - private StringBuilder getTabs() { - StringBuilder tabs = new StringBuilder(); - for (int i = 0; i < depth; i++) { - tabs.append("\t"); - } - return tabs; - } - - @Override - public void close() { - lap(null); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java similarity index 70% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java index b017b0df21..d049fe1a8a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,19 +14,13 @@ * the License. */ -package com.google.cloud.tools.jib.cache; +package com.google.cloud.tools.jib.api; -import java.nio.file.Path; - -/** Thrown when a directory to be used as the {@link Cache} could not be created. */ +/** Thrown when a directory to be used as the cache could not be created. */ public class CacheDirectoryCreationException extends Exception { private static final String MESSAGE = "Could not create cache directory"; - CacheDirectoryCreationException(Path cacheDirectory, Throwable cause) { - super(MESSAGE + ": " + cacheDirectory, cause); - } - public CacheDirectoryCreationException(Throwable cause) { super(MESSAGE, cause); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java new file mode 100644 index 0000000000..96442a5135 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -0,0 +1,408 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.builder.steps.StepsRunner; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.CliDockerClient; +import com.google.cloud.tools.jib.docker.DockerClientResolver; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.XdgDirectories; +import com.google.common.base.Preconditions; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** Configures how to containerize. */ +public class Containerizer { + + /** + * The default directory for caching the base image layers, in {@code [user cache + * home]/google-cloud-tools-java/jib}. + */ + public static final Path DEFAULT_BASE_CACHE_DIRECTORY = XdgDirectories.getCacheHome(); + + public static final String DEFAULT_APPLICATION_CACHE_DIRECTORY_NAME = + "jib-core-application-layers-cache"; + + private static final String DEFAULT_TOOL_NAME = "jib-core"; + private static final String DEFAULT_TOOL_VERSION = + Containerizer.class.getPackage().getImplementationVersion(); + + private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; + private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; + private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; + + /** + * Gets a new {@link Containerizer} that containerizes to a container registry. + * + * @param registryImage the {@link RegistryImage} that defines target container registry and + * credentials + * @return a new {@link Containerizer} + */ + public static Containerizer to(RegistryImage registryImage) { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(registryImage.getImageReference()) + .setCredentialRetrievers(registryImage.getCredentialRetrievers()) + .build(); + + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).registryPushSteps(); + + return new Containerizer( + DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory, true); + } + + /** + * Gets a new {@link Containerizer} that containerizes to a Docker daemon. + * + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines target Docker daemon + * @return a new {@link Containerizer} + */ + public static Containerizer to(DockerDaemonImage dockerDaemonImage) { + DockerClient dockerClient = + DockerClientResolver.resolve(dockerDaemonImage.getDockerEnvironment()) + .orElse( + new CliDockerClient( + dockerDaemonImage.getDockerExecutable(), + dockerDaemonImage.getDockerEnvironment())); + + return to(dockerClient, dockerDaemonImage); + } + + /** + * Gets a new {@link Containerizer} that containerizes to a tarball archive. + * + * @param tarImage the {@link TarImage} that defines target output file + * @return a new {@link Containerizer} + */ + public static Containerizer to(TarImage tarImage) { + Optional imageReference = tarImage.getImageReference(); + if (!imageReference.isPresent()) { + throw new IllegalArgumentException( + "Image name must be set when building a TarImage; use TarImage#named(...) to set the name" + + " of the target image"); + } + + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(imageReference.get()).build(); + + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).tarBuildSteps(tarImage.getPath()); + + return new Containerizer( + DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory, false); + } + + /** + * Gets a new {@link Containerizer} that containerizes to a Docker daemon. + * + * @param dockerClient the {@link DockerClient} to connect + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines target Docker daemon + * @return a new {@link Containerizer} + */ + public static Containerizer to(DockerClient dockerClient, DockerDaemonImage dockerDaemonImage) { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(dockerDaemonImage.getImageReference()).build(); + + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).dockerLoadSteps(dockerClient); + + return new Containerizer( + DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory, false); + } + + private final String description; + private final ImageConfiguration imageConfiguration; + private final Function stepsRunnerFactory; + private final boolean mustBeOnline; + private final Set additionalTags = new HashSet<>(); + private final EventHandlers.Builder eventHandlersBuilder = EventHandlers.builder(); + + @Nullable private ExecutorService executorService; + private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; + @Nullable private Path applicationLayersCacheDirectory; + private boolean allowInsecureRegistries = false; + private boolean offline = false; + private String toolName = DEFAULT_TOOL_NAME; + @Nullable private String toolVersion = DEFAULT_TOOL_VERSION; + private boolean alwaysCacheBaseImage = false; + private ListMultimap registryMirrors = ArrayListMultimap.create(); + + /** Instantiate with {@link #to}. */ + private Containerizer( + String description, + ImageConfiguration imageConfiguration, + Function stepsRunnerFactory, + boolean mustBeOnline) { + this.description = description; + this.imageConfiguration = imageConfiguration; + this.stepsRunnerFactory = stepsRunnerFactory; + this.mustBeOnline = mustBeOnline; + } + + /** + * Adds an additional tag to tag the target image with. For example, the following would + * containerize to both {@code gcr.io/my-project/my-image:tag} and {@code + * gcr.io/my-project/my-image:tag2}: + * + *
{@code
+   * Containerizer.to(RegistryImage.named("gcr.io/my-project/my-image:tag")).withAdditionalTag("tag2");
+   * }
+ * + * @param tag the additional tag to push to + * @return this + */ + public Containerizer withAdditionalTag(String tag) { + Preconditions.checkArgument(ImageReference.isValidTag(tag), "invalid tag '%s'", tag); + additionalTags.add(tag); + return this; + } + + /** + * Sets the {@link ExecutorService} Jib executes on. Jib, by default, uses {@link + * Executors#newCachedThreadPool}. + * + * @param executorService the {@link ExecutorService} + * @return this + */ + public Containerizer setExecutorService(@Nullable ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + /** + * Sets the directory to use for caching base image layers. This cache can (and should) be shared + * between multiple images. The default base image layers cache directory is {@code [user cache + * home]/google-cloud-tools-java/jib} ({@link #DEFAULT_BASE_CACHE_DIRECTORY}. This directory can + * be the same directory used for {@link #setApplicationLayersCache}. + * + * @param cacheDirectory the cache directory + * @return this + */ + public Containerizer setBaseImageLayersCache(Path cacheDirectory) { + baseImageLayersCacheDirectory = cacheDirectory; + return this; + } + + /** + * Sets the directory to use for caching application layers. This cache can be shared between + * multiple images. If not set, a temporary directory will be used as the application layers + * cache. This directory can be the same directory used for {@link #setBaseImageLayersCache}. + * + * @param cacheDirectory the cache directory + * @return this + */ + public Containerizer setApplicationLayersCache(Path cacheDirectory) { + applicationLayersCacheDirectory = cacheDirectory; + return this; + } + + /** + * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventType}. The + * order in which handlers are added is the order in which they are called when the event is + * dispatched. + * + *

Note: Implementations of {@code eventConsumer} must be thread-safe. + * + * @param eventType the event type that {@code eventConsumer} should handle + * @param eventConsumer the event handler + * @param the type of {@code eventType} + * @return this + */ + public Containerizer addEventHandler( + Class eventType, Consumer eventConsumer) { + eventHandlersBuilder.add(eventType, eventConsumer); + return this; + } + + /** + * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link + * #addEventHandler(Class, Consumer)} for more details. + * + * @param eventConsumer the event handler + * @return this + */ + public Containerizer addEventHandler(Consumer eventConsumer) { + eventHandlersBuilder.add(JibEvent.class, eventConsumer); + return this; + } + + /** + * Sets whether or not to allow communication over HTTP/insecure HTTPS. + * + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed + * @return this + */ + public Containerizer setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; + return this; + } + + /** + * Sets whether or not to run the build in offline mode. In offline mode, the base image is + * retrieved from the cache instead of pulled from a registry, and the build will fail if the base + * image is not in the cache or if the target is an image registry. + * + * @param offline if {@code true}, the build will run in offline mode + * @return this + */ + public Containerizer setOfflineMode(boolean offline) { + if (mustBeOnline && offline) { + throw new IllegalStateException("Cannot build to a container registry in offline mode"); + } + this.offline = offline; + return this; + } + + /** + * Sets the name of the tool that is using Jib Core. The tool name is sent as part of the {@code + * User-Agent} in registry requests and set as the {@code created_by} in the container layer + * history. Defaults to {@code jib-core}. + * + * @param toolName the name of the tool using this library + * @return this + */ + public Containerizer setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Sets the version of the tool that is using Jib Core. The tool version is sent as part of the + * {@code User-Agent} in registry requests and set as the {@code created_by} in the container + * layer history. Defaults to the current version of jib-core. + * + * @param toolVersion the name of the tool using this library + * @return this + */ + public Containerizer setToolVersion(@Nullable String toolVersion) { + this.toolVersion = toolVersion; + return this; + } + + /** + * Controls the optimization which skips downloading base image layers that exist in a target + * registry. If the user does not set this property, then read as false. + * + * @param alwaysCacheBaseImage if {@code true}, base image layers are always pulled and cached. If + * {@code false}, base image layers will not be pulled/cached if they already exist on the + * target registry. + * @return this + */ + public Containerizer setAlwaysCacheBaseImage(boolean alwaysCacheBaseImage) { + this.alwaysCacheBaseImage = alwaysCacheBaseImage; + return this; + } + + /** + * Adds mirrors for a base image registry. Jib will try its mirrors in the given order before + * finally trying the registry. + * + * @param registry base image registry for which mirrors are configured + * @param mirrors a list of mirrors, where each element is in the form of {@code host[:port]} + * @return this + */ + public Containerizer addRegistryMirrors(String registry, List mirrors) { + registryMirrors.putAll(registry, mirrors); + return this; + } + + Set getAdditionalTags() { + return ImmutableSet.copyOf(additionalTags); + } + + ListMultimap getRegistryMirrors() { + return ImmutableListMultimap.copyOf(registryMirrors); + } + + Optional getExecutorService() { + return Optional.ofNullable(executorService); + } + + Path getBaseImageLayersCacheDirectory() { + return baseImageLayersCacheDirectory; + } + + Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException { + if (applicationLayersCacheDirectory == null) { + // Create a directory in temp if application layers cache directory is not set. + try { + Path tmp = Paths.get(System.getProperty("java.io.tmpdir")); + applicationLayersCacheDirectory = tmp.resolve(DEFAULT_APPLICATION_CACHE_DIRECTORY_NAME); + Files.createDirectories(applicationLayersCacheDirectory); + } catch (IOException ex) { + throw new CacheDirectoryCreationException(ex); + } + } + return applicationLayersCacheDirectory; + } + + EventHandlers buildEventHandlers() { + return eventHandlersBuilder.build(); + } + + boolean getAllowInsecureRegistries() { + return allowInsecureRegistries; + } + + boolean isOfflineMode() { + return offline; + } + + String getToolName() { + return toolName; + } + + @Nullable + String getToolVersion() { + return toolVersion; + } + + boolean getAlwaysCacheBaseImage() { + return alwaysCacheBaseImage; + } + + String getDescription() { + return description; + } + + ImageConfiguration getImageConfiguration() { + return imageConfiguration; + } + + BuildResult run(BuildContext buildContext) throws ExecutionException, InterruptedException { + return stepsRunnerFactory.apply(buildContext).run(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java new file mode 100644 index 0000000000..7e263704e9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.util.Objects; + +/** Holds credentials (username and password). */ +public class Credential { + // If the username is set to , the secret would be a refresh token. + // https://github.com/docker/cli/blob/master/docs/reference/commandline/login.md#credential-helper-protocol + public static final String OAUTH2_TOKEN_USER_NAME = ""; + + /** + * Gets a {@link Credential} configured with a username and password. + * + * @param username the username + * @param password the password + * @return a new {@link Credential} + */ + public static Credential from(String username, String password) { + return new Credential(username, password); + } + + private final String username; + private final String password; + + private Credential(String username, String password) { + this.username = username; + this.password = password; + } + + /** + * Gets the username. + * + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * Gets the password. + * + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * Check whether this credential is an OAuth 2.0 refresh token. + * + * @return true if this credential is an OAuth 2.0 refresh token. + */ + public boolean isOAuth2RefreshToken() { + return OAUTH2_TOKEN_USER_NAME.equals(username); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Credential)) { + return false; + } + Credential otherCredential = (Credential) other; + return username.equals(otherCredential.username) && password.equals(otherCredential.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, password); + } + + @Override + public String toString() { + return username + ":" + password; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java new file mode 100644 index 0000000000..4e357ff8a5 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import java.util.Optional; + +/** Retrieves credentials for a registry. */ +@FunctionalInterface +public interface CredentialRetriever { + + /** + * Fetches the credentials. Implementations must be thread-safe. + * + *

Implementations should return {@link Optional#empty} if no credentials could be fetched with + * this {@link CredentialRetriever} (and so other credential retrieval methods may be tried), or + * throw an exception something went wrong when fetching the credentials. + * + * @return the fetched credentials or {@link Optional#empty} if no credentials could be fetched + * with this provider + * @throws CredentialRetrievalException if the credential retrieval encountered an exception + */ + Optional retrieve() throws CredentialRetrievalException; +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java similarity index 93% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java index aa786254e7..94dc79d08c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -34,8 +34,10 @@ @JsonDeserialize(using = DescriptorDigestDeserializer.class) public class DescriptorDigest { + public static final int HASH_LENGTH = 64; + /** Pattern matches a SHA-256 hash - 32 bytes in lowercase hexadecimal. */ - private static final String HASH_REGEX = "[a-f0-9]{64}"; + private static final String HASH_REGEX = String.format("[a-f0-9]{%d}", HASH_LENGTH); /** The algorithm prefix for the digest string. */ private static final String DIGEST_PREFIX = "sha256:"; @@ -87,7 +89,7 @@ public String getHash() { @Override public String toString() { - return "sha256:" + hash; + return DIGEST_PREFIX + hash; } /** Pass-through hash code of the digest string. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java new file mode 100644 index 0000000000..7e08a79495 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.image.ImageTarball; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +public interface DockerClient { + + /** + * Validate if the DockerClient is supported. + * + * @param parameters to be used by the docker client + * @return true if conditions are met + */ + boolean supported(Map parameters); + + /** + * Loads an image tarball into the Docker daemon. + * + * @see https://docs.docker.com/engine/reference/commandline/load + * @param imageTarball the built container tarball + * @param writtenByteCountListener callback to call when bytes are loaded + * @return stdout from {@code docker} + * @throws InterruptedException if the 'docker load' process is interrupted + * @throws IOException if streaming the blob to 'docker load' fails + */ + String load(ImageTarball imageTarball, Consumer writtenByteCountListener) + throws InterruptedException, IOException; + + /** + * Saves an image tarball from the Docker daemon. + * + * @see https://docs.docker.com/engine/reference/commandline/save + * @param imageReference the image to save + * @param outputPath the destination path to save the output tarball + * @param writtenByteCountListener callback to call when bytes are saved + * @throws InterruptedException if the 'docker save' process is interrupted + * @throws IOException if creating the tarball fails + */ + void save(ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) + throws InterruptedException, IOException; + + /** + * Gets the size, image ID, and diff IDs of an image in the Docker daemon. + * + * @param imageReference the image to inspect + * @return the size, image ID, and diff IDs of the image + * @throws IOException if an I/O exception occurs or {@code docker inspect} failed + * @throws InterruptedException if the {@code docker inspect} process was interrupted + */ + ImageDetails inspect(ImageReference imageReference) throws IOException, InterruptedException; + + /** + * Gets docker info details of local docker installation. + * + * @return docker info details. + * @throws IOException if an I/O exception occurs or {@code docker info} failed + * @throws InterruptedException if the {@code docker info} process was interrupted + */ + default DockerInfoDetails info() throws IOException, InterruptedException { + return new DockerInfoDetails(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java new file mode 100644 index 0000000000..d6fcde1864 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.docker.CliDockerClient; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +/** Builds to the Docker daemon. */ +public class DockerDaemonImage { + + /** + * Instantiate with the image reference to tag the built image with. This is the name that shows + * up on the Docker daemon. + * + * @param imageReference the image reference + * @return a new {@link DockerDaemonImage} + */ + public static DockerDaemonImage named(ImageReference imageReference) { + return new DockerDaemonImage(imageReference); + } + + /** + * Instantiate with the image reference to tag the built image with. This is the name that shows + * up on the Docker daemon. + * + * @param imageReference the image reference + * @return a new {@link DockerDaemonImage} + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public static DockerDaemonImage named(String imageReference) + throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + private final ImageReference imageReference; + private Path dockerExecutable = CliDockerClient.DEFAULT_DOCKER_CLIENT; + private Map dockerEnvironment = Collections.emptyMap(); + + /** Instantiate with {@link #named}. */ + private DockerDaemonImage(ImageReference imageReference) { + this.imageReference = imageReference; + } + + /** + * Sets the path to the {@code docker} CLI. This is {@code docker} by default. + * + * @param dockerExecutable the path to the {@code docker} CLI + * @return this + */ + public DockerDaemonImage setDockerExecutable(Path dockerExecutable) { + this.dockerExecutable = dockerExecutable; + return this; + } + + /** + * Sets the additional environment variables to use when running {@link #dockerExecutable docker}. + * + * @param dockerEnvironment additional environment variables + * @return this + */ + public DockerDaemonImage setDockerEnvironment(Map dockerEnvironment) { + this.dockerEnvironment = dockerEnvironment; + return this; + } + + ImageReference getImageReference() { + return imageReference; + } + + Path getDockerExecutable() { + return dockerExecutable; + } + + Map getDockerEnvironment() { + return dockerEnvironment; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java new file mode 100644 index 0000000000..fa486c40f9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.json.JsonTemplate; + +/** Contains docker info details outputted by {@code docker info}. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class DockerInfoDetails implements JsonTemplate { + + @JsonProperty("OSType") + private String osType = ""; + + @JsonProperty("Architecture") + private String architecture = ""; + + public String getOsType() { + return osType; + } + + public String getArchitecture() { + return architecture; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageDetails.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageDetails.java new file mode 100644 index 0000000000..c1c90766e1 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageDetails.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.security.DigestException; +import java.util.List; + +public interface ImageDetails { + + long getSize(); + + DescriptorDigest getImageId() throws DigestException; + + List getDiffIds() throws DigestException; +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java new file mode 100644 index 0000000000..3cefab246f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java @@ -0,0 +1,452 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.registry.RegistryAliasGroup; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * Represents an image reference. + * + * @see https://github.com/docker/distribution/blob/master/reference/reference.go + * @see https://docs.docker.com/engine/reference/commandline/tag/#extended-description + */ +public class ImageReference { + + private static final String DOCKER_HUB_REGISTRY = "registry-1.docker.io"; + private static final String DEFAULT_TAG = "latest"; + private static final String LIBRARY_REPOSITORY_PREFIX = "library/"; + private static final String SCRATCH = "scratch"; + + /** + * Matches all sequences of alphanumeric characters possibly separated by any number of dashes in + * the middle. + */ + private static final String REGISTRY_COMPONENT_REGEX = + "(?:[a-zA-Z\\d]|(?:[a-zA-Z\\d][a-zA-Z\\d-]*[a-zA-Z\\d]))"; + + /** + * Matches sequences of {@code REGISTRY_COMPONENT_REGEX} separated by a dot, with an optional + * {@code :port} at the end. + */ + private static final String REGISTRY_REGEX = + String.format("%s(?:\\.%s)*(?::\\d+)?", REGISTRY_COMPONENT_REGEX, REGISTRY_COMPONENT_REGEX); + + /** + * Matches all sequences of alphanumeric characters separated by a separator. + * + *

A separator is either an underscore, a dot, two underscores, or any number of dashes. + */ + private static final String REPOSITORY_COMPONENT_REGEX = "[a-z\\d]+(?:(?:[_.]|__|-+)[a-z\\d]+)*"; + + /** Matches all repetitions of {@code REPOSITORY_COMPONENT_REGEX} separated by a backslash. */ + private static final String REPOSITORY_REGEX = + String.format("(?:%s/)*%s", REPOSITORY_COMPONENT_REGEX, REPOSITORY_COMPONENT_REGEX); + + /** Matches a tag of max length 128. */ + private static final String TAG_REGEX = "[\\w][\\w.-]{0,127}"; + + /** + * Matches a full image reference, which is the registry, repository, and tag/digest separated by + * backslashes. The repository is required, but the registry and tag/digest are optional. + */ + private static final String REFERENCE_REGEX = + String.format( + "^(?:(%s)/)?(%s)(?::(%s))?(?:@(%s))?$", + REGISTRY_REGEX, REPOSITORY_REGEX, TAG_REGEX, DescriptorDigest.DIGEST_REGEX); + + private static final Pattern REFERENCE_PATTERN = Pattern.compile(REFERENCE_REGEX); + + /** + * Parses a string {@code reference} into an {@link ImageReference}. + * + *

Image references should generally be in the form: {@code /:} For + * example, an image reference could be {@code gcr.io/k8s-skaffold/skaffold:v1.20.0}. + * + *

See https://docs.docker.com/engine/reference/commandline/tag/#extended-description + * for a description of valid image reference format. Note, however, that the image reference is + * referred confusingly as {@code tag} on that page. + * + * @param reference the string to parse + * @return an {@link ImageReference} parsed from the string + * @throws InvalidImageReferenceException if {@code reference} is formatted incorrectly + */ + public static ImageReference parse(String reference) throws InvalidImageReferenceException { + if (reference.equals(SCRATCH)) { + return ImageReference.scratch(); + } + + Matcher matcher = REFERENCE_PATTERN.matcher(reference); + + if (!matcher.find() || matcher.groupCount() < 4) { + throw new InvalidImageReferenceException(reference); + } + + String registry = matcher.group(1); + String repository = matcher.group(2); + String tag = matcher.group(3); + String digest = matcher.group(4); + + // If no registry was matched, use Docker Hub by default. + if (Strings.isNullOrEmpty(registry)) { + registry = DOCKER_HUB_REGISTRY; + } + + if (Strings.isNullOrEmpty(repository)) { + throw new InvalidImageReferenceException(reference); + } + /* + * If a registry was matched but it does not contain any dots or colons, it should actually be + * part of the repository unless it is "localhost". + * + * See https://github.com/docker/distribution/blob/245ca4659e09e9745f3cc1217bf56e946509220c/reference/normalize.go#L62 + */ + if (!registry.contains(".") && !registry.contains(":") && !"localhost".equals(registry)) { + repository = registry + "/" + repository; + registry = DOCKER_HUB_REGISTRY; + } + + /* + * For Docker Hub, if the repository is only one component, then it should be prefixed with + * 'library/'. + * + * See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-from-docker-hub + */ + if (DOCKER_HUB_REGISTRY.equals(registry) && repository.indexOf('/') < 0) { + repository = LIBRARY_REPOSITORY_PREFIX + repository; + } + + if (Strings.isNullOrEmpty(tag) && Strings.isNullOrEmpty(digest)) { + tag = DEFAULT_TAG; + } + if (Strings.isNullOrEmpty(tag)) { + tag = null; + } + if (Strings.isNullOrEmpty(digest)) { + digest = null; + } + + return new ImageReference(registry, repository, tag, digest); + } + + /** + * Constructs an {@link ImageReference} from the image reference components, consisting of an + * optional registry, a repository, and an optional tag. + * + * @param registry the image registry, or {@code null} to use the default registry (Docker Hub) + * @param repository the image repository + * @param qualifier the image tag or digest, or {@code null} to use the default tag ({@code + * latest}). + * @return an {@link ImageReference} built from the given registry, repository, and tag + */ + public static ImageReference of( + @Nullable String registry, String repository, @Nullable String qualifier) { + if (!Strings.isNullOrEmpty(qualifier) && isValidDigest(qualifier)) { + return of(registry, repository, null, qualifier); + } + return of(registry, repository, qualifier, null); + } + + /** + * Constructs an {@link ImageReference} from the image reference components, consisting of an + * optional registry, a repository, an optional tag, and an optional digest. If neither the tag + * nor digest are specified, {@code tag} will take on the default value of {@code latest}. + * + * @param registry the image registry, or {@code null} to use the default registry (Docker Hub) + * @param repository the image repository + * @param tag the image tag, or {@code null} to use the default tag ({@code latest}) + * @param digest the image digest + * @return an {@link ImageReference} built from the given registry, repository, and tag + */ + public static ImageReference of( + @Nullable String registry, String repository, @Nullable String tag, @Nullable String digest) { + Preconditions.checkArgument(Strings.isNullOrEmpty(registry) || isValidRegistry(registry)); + Preconditions.checkArgument(isValidRepository(repository)); + Preconditions.checkArgument(Strings.isNullOrEmpty(tag) || isValidTag(tag)); + Preconditions.checkArgument(Strings.isNullOrEmpty(digest) || isValidDigest(digest)); + + if (Strings.isNullOrEmpty(registry)) { + registry = DOCKER_HUB_REGISTRY; + } + if (Strings.isNullOrEmpty(tag) && Strings.isNullOrEmpty(digest)) { + tag = DEFAULT_TAG; + } + return new ImageReference(registry, repository, tag, digest); + } + + /** + * Constructs an {@link ImageReference} with an empty registry and tag component, and repository + * set to "scratch". + * + * @return an {@link ImageReference} with an empty registry and tag component, and repository set + * to "scratch" + */ + public static ImageReference scratch() { + return new ImageReference("", SCRATCH, null, null); + } + + /** + * Returns {@code true} if {@code registry} is a valid registry string. For example, a valid + * registry could be {@code gcr.io} or {@code localhost:5000}. + * + * @param registry the registry to check + * @return {@code true} if is a valid registry; {@code false} otherwise + */ + public static boolean isValidRegistry(String registry) { + return registry.matches(REGISTRY_REGEX); + } + + /** + * Returns {@code true} if {@code repository} is a valid repository string. For example, a valid + * repository string could be {@code my-repository} or {@code k8s-skaffold/skaffold}. + * + * @param repository the repository to check + * @return {@code true} if is a valid repository; {@code false} otherwise + */ + public static boolean isValidRepository(String repository) { + return repository.matches(REPOSITORY_REGEX); + } + + /** + * Returns {@code true} if {@code tag} is a valid tag string. For example, a valid tag could be + * {@code v120.5-release}. + * + * @param tag the tag to check + * @return {@code true} if is a valid tag; {@code false} otherwise + */ + public static boolean isValidTag(String tag) { + return tag.matches(TAG_REGEX); + } + + /** + * Returns {@code true} if {@code digest} is a valid digest string. For example, a valid digest + * could be {@code sha256:868fd30a0e47b8d8ac485df174795b5e2fe8a6c8f056cc707b232d65b8a1ab68}. + * + * @param digest the digest to check + * @return {@code true} if is a valid digest; {@code false} otherwise + */ + public static boolean isValidDigest(String digest) { + return digest.matches(DescriptorDigest.DIGEST_REGEX); + } + + /** + * Returns {@code true} if {@code tag} is the default tag ({@code latest}); {@code false} if not. + * + * @param tag the tag to check + * @return {@code true} if {@code tag} is the default tag ({@code latest}); {@code false} if not + */ + public static boolean isDefaultTag(@Nullable String tag) { + return DEFAULT_TAG.equals(tag); + } + + private final String registry; + private final String repository; + @Nullable private final String tag; + @Nullable private final String digest; + + /** Construct with {@link #parse}. */ + private ImageReference( + String registry, String repository, @Nullable String tag, @Nullable String digest) { + Preconditions.checkArgument( + SCRATCH.equals(repository) || !Strings.isNullOrEmpty(tag) || !Strings.isNullOrEmpty(digest), + "Either tag or digest needs to be set."); + this.registry = RegistryAliasGroup.getHost(registry); + this.repository = repository; + this.tag = tag; + this.digest = digest; + } + + /** + * Gets the registry portion of the {@link ImageReference}. + * + * @return the registry host + */ + public String getRegistry() { + return registry; + } + + /** + * Gets the repository portion of the {@link ImageReference}. + * + * @return the repository + */ + public String getRepository() { + return repository; + } + + /** + * Gets the tag portion of the {@link ImageReference}. + * + * @return the optional tag + */ + public Optional getTag() { + return Optional.ofNullable(tag); + } + + /** + * Gets the digest portion of the {@link ImageReference}. + * + * @return the optional digest + */ + public Optional getDigest() { + return Optional.ofNullable(digest); + } + + /** + * Gets the digest portion of the {@link ImageReference} if set, else returns the tag. + * + * @return the digest if set, else the tag + */ + public String getQualifier() { + if (!Strings.isNullOrEmpty(digest)) { + return digest; + } + return Preconditions.checkNotNull(tag); + } + + /** + * Returns {@code true} if the {@link ImageReference} uses the default tag ({@code latest}); + * {@code false} if not. + * + * @return {@code true} if uses the default tag; {@code false} if not + */ + public boolean usesDefaultTag() { + return isDefaultTag(tag); + } + + /** + * Returns {@code true} if the {@link ImageReference} is a scratch image; {@code false} if not. + * + * @return {@code true} if the {@link ImageReference} is a scratch image; {@code false} if not + */ + public boolean isScratch() { + return "".equals(registry) + && SCRATCH.equals(repository) + && Strings.isNullOrEmpty(tag) + && Strings.isNullOrEmpty(digest); + } + + /** + * Gets an {@link ImageReference} with the same registry and repository, but a different tag or + * digest. + * + * @param newQualifier the new tag or digest + * @return an {@link ImageReference} with the same registry/repository and the new tag or digest + */ + public ImageReference withQualifier(String newQualifier) { + if (isValidDigest(newQualifier)) { + return ImageReference.of(registry, repository, tag, newQualifier); + } + return ImageReference.of(registry, repository, newQualifier, digest); + } + + /** + * Stringifies the {@link ImageReference}. + * + * @return the image reference in Docker-readable format (inverse of {@link #parse}) + */ + @Override + public String toString() { + return toString(false); + } + + /** + * Stringifies the {@link ImageReference}. If the digest is set, the result will include the + * digest and no tag. Otherwise, the result will include the tag, or {@code latest} if no tag is + * set. + * + * @return the image reference in Docker-readable format including a qualifier. + */ + public String toStringWithQualifier() { + return toString(true); + } + + /** + * Stringifies the {@link ImageReference}. + * + * @param singleQualifier when {@code true}, the result will include exactly one qualifier (i.e. + * the digest, or the tag if the digest is missing). When {@code false}, the result will + * include all specified qualifiers (omitting tag if the default {@code latest} is used). + * @return the image reference in a Docker-readable format. + */ + private String toString(boolean singleQualifier) { + if (isScratch()) { + return SCRATCH; + } + + StringBuilder referenceString = new StringBuilder(); + + if (!DOCKER_HUB_REGISTRY.equals(registry)) { + // Use registry and repository if not Docker Hub. + referenceString.append(registry).append('/').append(repository); + + } else if (repository.startsWith(LIBRARY_REPOSITORY_PREFIX)) { + // If Docker Hub and repository has 'library/' prefix, remove the 'library/' prefix. + referenceString.append(repository.substring(LIBRARY_REPOSITORY_PREFIX.length())); + + } else { + // Use just repository if Docker Hub. + referenceString.append(repository); + } + + if (singleQualifier) { + if (!Strings.isNullOrEmpty(digest)) { + referenceString.append('@').append(digest); + } else { + referenceString.append(':').append(tag); + } + } else { + if (!Strings.isNullOrEmpty(tag) && !usesDefaultTag()) { + referenceString.append(':').append(tag); + } + if (!Strings.isNullOrEmpty(digest)) { + referenceString.append('@').append(digest); + } + } + + return referenceString.toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ImageReference)) { + return false; + } + ImageReference otherImageReference = (ImageReference) other; + return registry.equals(otherImageReference.registry) + && repository.equals(otherImageReference.repository) + && Objects.equals(tag, otherImageReference.tag) + && Objects.equals(digest, otherImageReference.digest); + } + + @Override + public int hashCode() { + return Objects.hash(registry, repository, tag, digest); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java similarity index 59% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java index e76fa42f8c..c40a9759a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import java.net.URL; @@ -23,7 +23,17 @@ */ public class InsecureRegistryException extends RegistryException { - InsecureRegistryException(URL insecureUrl) { - super("Only secure connections are allowed, but tried to reach URL " + insecureUrl); + /** + * Creates a new exception with a human readable message. + * + * @param insecureUrl the insecure url that is attempted to be accessed + * @param cause the underlying cause that triggered this exception + */ + public InsecureRegistryException(URL insecureUrl, Throwable cause) { + super( + "Failed to verify the server at " + + insecureUrl + + " because only secure connections are allowed.", + cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java similarity index 80% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java index f3267334a1..e6ebc85c9e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,12 +14,19 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; /** Thrown when attempting to parse an invalid image reference. */ public class InvalidImageReferenceException extends Exception { + private final String reference; + public InvalidImageReferenceException(String reference) { super("Invalid image reference: " + reference); + this.reference = reference; + } + + public String getInvalidReference() { + return reference; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java new file mode 100644 index 0000000000..148131d91f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -0,0 +1,724 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ModificationTimeProvider; +import com.google.cloud.tools.jib.api.buildplan.RelativeUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Streams; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** Creates a {@link JibContainerBuilder} for containerizing Java applications. */ +public class JavaContainerBuilder { + + /** Holds a directory and a filter. */ + private static class PathPredicatePair { + + private final Path path; + private final Predicate predicate; + + private PathPredicatePair(Path path, Predicate predicate) { + this.path = path; + this.predicate = predicate; + } + } + + /** Represents the different types of layers for a Java application. */ + public enum LayerType { + DEPENDENCIES("dependencies"), + SNAPSHOT_DEPENDENCIES("snapshot dependencies"), + PROJECT_DEPENDENCIES("project dependencies"), + RESOURCES("resources"), + CLASSES("classes"), + EXTRA_FILES("extra files"), + JVM_ARG_FILES("jvm arg files"); + + private final String name; + + /** + * Initializes with a name for the layer. + * + * @param name name to set for the layer; does not affect the contents of the layer + */ + LayerType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + /** + * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For + * more information on {@code gcr.io/distroless/java}, see the distroless repository. + * + * @return a new {@link JavaContainerBuilder} + * @see The distroless repository + * @deprecated Use {@code from()} with the image reference {@code gcr.io/distroless/java}. + */ + @Deprecated + public static JavaContainerBuilder fromDistroless() { + try { + return from(RegistryImage.named("gcr.io/distroless/java")); + } catch (InvalidImageReferenceException ignored) { + throw new IllegalStateException("Unreachable"); + } + } + + /** + * The default app root in the image. For example, if this is set to {@code "/app"}, dependency + * JARs will be in {@code "/app/libs"}. + */ + public static final String DEFAULT_APP_ROOT = "/app"; + + /** + * The default webapp root in the image. For example, if this is set to {@code + * "/jetty/webapps/ROOT"}, dependency JARs will be in {@code "/jetty/webapps/ROOT/WEB-INF/lib"}. + * + * @deprecated Use the string {@code "/jetty/webapps/ROOT"}. + */ + @Deprecated public static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; + + /** + * Creates a new {@link JavaContainerBuilder} that uses distroless jetty as the base image. For + * more information on {@code gcr.io/distroless/java}, see the distroless repository. + * + * @return a new {@link JavaContainerBuilder} + * @see The distroless repository + * @deprecated Use {@code from()} with the image reference {@code gcr.io/distroless/java/jetty} + * and change the app root by calling {@code + * JavaContainerBuilder.setAppRoot("/jetty/webapps/ROOT")}. + */ + @Deprecated + public static JavaContainerBuilder fromDistrolessJetty() { + try { + return from(RegistryImage.named("gcr.io/distroless/java/jetty")) + .setAppRoot(AbsoluteUnixPath.get(DEFAULT_WEB_APP_ROOT)); + } catch (InvalidImageReferenceException ignored) { + throw new IllegalStateException("Unreachable"); + } + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image reference. The type of + * base image can be specified using a prefix; see {@link Jib#from(String)} for the accepted + * prefixes. + * + * @param baseImageReference the base image reference + * @return a new {@link JavaContainerBuilder} + * @throws InvalidImageReferenceException if {@code baseImageReference} is invalid + */ + public static JavaContainerBuilder from(String baseImageReference) + throws InvalidImageReferenceException { + return new JavaContainerBuilder(Jib.from(baseImageReference)); + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image reference. + * + * @param baseImageReference the base image reference + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(ImageReference baseImageReference) { + return from(RegistryImage.named(baseImageReference)); + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image. + * + * @param registryImage the {@link RegistryImage} that defines base container registry and + * credentials + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(RegistryImage registryImage) { + return new JavaContainerBuilder(Jib.from(registryImage)); + } + + /** + * Starts building the container from a base image stored in the Docker cache. Requires a running + * Docker daemon. + * + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines the base image and Docker + * client + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(DockerDaemonImage dockerDaemonImage) { + return new JavaContainerBuilder(Jib.from(dockerDaemonImage)); + } + + /** + * Starts building the container from a tarball. + * + * @param tarImage the {@link TarImage} that defines the path to the base image + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(TarImage tarImage) { + return new JavaContainerBuilder(Jib.from(tarImage)); + } + + private final JibContainerBuilder jibContainerBuilder; + private final List jvmFlags = new ArrayList<>(); + private final LinkedHashSet classpathOrder = new LinkedHashSet<>(4); + + // Keeps track of files to add to the image, by system path + private final List addedResources = new ArrayList<>(); + private final List addedClasses = new ArrayList<>(); + private final List addedDependencies = new ArrayList<>(); + private final List addedSnapshotDependencies = new ArrayList<>(); + private final List addedProjectDependencies = new ArrayList<>(); + private final List addedOthers = new ArrayList<>(); + + private AbsoluteUnixPath appRoot = AbsoluteUnixPath.get(DEFAULT_APP_ROOT); + private RelativeUnixPath classesDestination = RelativeUnixPath.get("classes"); + private RelativeUnixPath resourcesDestination = RelativeUnixPath.get("resources"); + private RelativeUnixPath dependenciesDestination = RelativeUnixPath.get("libs"); + private RelativeUnixPath othersDestination = RelativeUnixPath.get("classpath"); + @Nullable private String mainClass; + private ModificationTimeProvider modificationTimeProvider = + FileEntriesLayer.DEFAULT_MODIFICATION_TIME_PROVIDER; + + private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { + this.jibContainerBuilder = jibContainerBuilder; + } + + /** + * Sets the app root of the container image (useful for building WAR containers). + * + * @param appRoot the absolute path of the app on the container ({@code /app} by default) + * @return this + */ + public JavaContainerBuilder setAppRoot(String appRoot) { + return setAppRoot(AbsoluteUnixPath.get(appRoot)); + } + + /** + * Sets the app root of the container image (useful for building WAR containers). + * + * @param appRoot the absolute path of the app on the container ({@code /app} by default) + * @return this + */ + public JavaContainerBuilder setAppRoot(AbsoluteUnixPath appRoot) { + this.appRoot = appRoot; + return this; + } + + /** + * Sets the destination directory of the classes added to the container (relative to the app + * root). + * + * @param classesDestination the path to the classes directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setClassesDestination(RelativeUnixPath classesDestination) { + this.classesDestination = classesDestination; + return this; + } + + /** + * Sets the destination directory of the resources added to the container (relative to the app + * root). + * + * @param resourcesDestination the path to the resources directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setResourcesDestination(RelativeUnixPath resourcesDestination) { + this.resourcesDestination = resourcesDestination; + return this; + } + + /** + * Sets the destination directory of the dependencies added to the container (relative to the app + * root). + * + * @param dependenciesDestination the path to the dependencies directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setDependenciesDestination(RelativeUnixPath dependenciesDestination) { + this.dependenciesDestination = dependenciesDestination; + return this; + } + + /** + * Sets the destination directory of additional classpath files added to the container (relative + * to the app root). + * + * @param othersDestination the additional classpath directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setOthersDestination(RelativeUnixPath othersDestination) { + this.othersDestination = othersDestination; + return this; + } + + /** + * Adds dependency JARs to the image. Duplicate JAR filenames across all dependencies are renamed + * with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addDependencies(List dependencyFiles) throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + addedDependencies.addAll(dependencyFiles); + classpathOrder.add(LayerType.DEPENDENCIES); + return this; + } + + /** + * Adds dependency JARs to the image. Duplicate JAR filenames across all dependencies are renamed + * with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addDependencies(Path... dependencyFiles) throws IOException { + return addDependencies(Arrays.asList(dependencyFiles)); + } + + /** + * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are + * renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addSnapshotDependencies(List dependencyFiles) + throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + addedSnapshotDependencies.addAll(dependencyFiles); + classpathOrder.add(LayerType.DEPENDENCIES); // this is a single classpath entry with all deps + return this; + } + + /** + * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are + * renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addSnapshotDependencies(Path... dependencyFiles) throws IOException { + return addSnapshotDependencies(Arrays.asList(dependencyFiles)); + } + + /** + * Adds project dependency JARs to the image. Generally, project dependency are jars produced from + * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across + * all dependencies are renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addProjectDependencies(List dependencyFiles) + throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + addedProjectDependencies.addAll(dependencyFiles); + classpathOrder.add(LayerType.DEPENDENCIES); // this is a single classpath entry with all deps + return this; + } + + /** + * Adds project dependency JARs to the image. Generally, project dependency are jars produced from + * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across + * all dependencies are renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addProjectDependencies(Path... dependencyFiles) throws IOException { + return addProjectDependencies(Arrays.asList(dependencyFiles)); + } + + /** + * Adds the contents of a resources directory to the image. + * + * @param resourceFilesDirectory the directory containing the project's resources + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addResources(Path resourceFilesDirectory) throws IOException { + return addResources(resourceFilesDirectory, path -> true); + } + + /** + * Adds the contents of a resources directory to the image. + * + * @param resourceFilesDirectory the directory containing the project's resources + * @param pathFilter filter that determines which files (not directories) should be added + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addResources(Path resourceFilesDirectory, Predicate pathFilter) + throws IOException { + classpathOrder.add(LayerType.RESOURCES); + return addDirectory(addedResources, resourceFilesDirectory, pathFilter); + } + + /** + * Adds the contents of a classes directory to the image. + * + * @param classFilesDirectory the directory containing the class files + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addClasses(Path classFilesDirectory) throws IOException { + return addClasses(classFilesDirectory, path -> true); + } + + /** + * Adds the contents of a classes directory to the image. + * + * @param classFilesDirectory the directory containing the class files + * @param pathFilter filter that determines which files (not directories) should be added + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addClasses(Path classFilesDirectory, Predicate pathFilter) + throws IOException { + classpathOrder.add(LayerType.CLASSES); + return addDirectory(addedClasses, classFilesDirectory, pathFilter); + } + + /** + * Adds additional files to the classpath. If {@code otherFiles} contains a directory, the files + * within are added recursively, maintaining the directory structure. For files in {@code + * otherFiles}, files with duplicate filenames will be overwritten (e.g. if {@code otherFiles} + * contains '/loser/messages.txt' and '/winner/messages.txt', only the second 'messages.txt' is + * added. + * + * @param otherFiles the list of files to add + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addToClasspath(List otherFiles) throws IOException { + // Make sure all files exist before adding any + for (Path file : otherFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + classpathOrder.add(LayerType.EXTRA_FILES); + addedOthers.addAll(otherFiles); + return this; + } + + /** + * Adds additional files to the classpath. If {@code otherFiles} contains a directory, the files + * within are added recursively, maintaining the directory structure. For files in {@code + * otherFiles}, files with duplicate filenames will be overwritten (e.g. if {@code otherFiles} + * contains '/loser/messages.txt' and '/winner/messages.txt', only the second 'messages.txt' is + * added. + * + * @param otherFiles the list of files to add + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addToClasspath(Path... otherFiles) throws IOException { + return addToClasspath(Arrays.asList(otherFiles)); + } + + /** + * Adds a JVM flag to use when starting the application. + * + * @param jvmFlag the JVM flag to add + * @return this + */ + public JavaContainerBuilder addJvmFlag(String jvmFlag) { + jvmFlags.add(jvmFlag); + return this; + } + + /** + * Adds JVM flags to use when starting the application. + * + * @param jvmFlags the list of JVM flags to add + * @return this + */ + public JavaContainerBuilder addJvmFlags(List jvmFlags) { + this.jvmFlags.addAll(jvmFlags); + return this; + } + + /** + * Adds JVM flags to use when starting the application. + * + * @param jvmFlags the list of JVM flags to add + * @return this + */ + public JavaContainerBuilder addJvmFlags(String... jvmFlags) { + this.jvmFlags.addAll(Arrays.asList(jvmFlags)); + return this; + } + + /** + * Sets the container entrypoint with the specified main class. The entrypoint will be left + * unconfigured if this method is not called. To find the main class from {@code .class} files, + * use {@link MainClassFinder}. + * + * @param mainClass the main class used to start the application + * @return this + * @see MainClassFinder + */ + public JavaContainerBuilder setMainClass(String mainClass) { + this.mainClass = mainClass; + return this; + } + + /** + * Sets the modification time provider for container files. + * + * @param modificationTimeProvider a provider that takes a source path and destination path on the + * container and returns the file modification time that should be set for that path + * @return this + */ + public JavaContainerBuilder setModificationTimeProvider( + ModificationTimeProvider modificationTimeProvider) { + this.modificationTimeProvider = modificationTimeProvider; + return this; + } + + /** + * Returns a new {@link JibContainerBuilder} using the parameters specified on the {@link + * JavaContainerBuilder}. + * + * @return a new {@link JibContainerBuilder} using the parameters specified on the {@link + * JavaContainerBuilder} + * @throws IOException if building the {@link JibContainerBuilder} fails. + */ + public JibContainerBuilder toContainerBuilder() throws IOException { + if (mainClass == null && !jvmFlags.isEmpty()) { + throw new IllegalStateException( + "Failed to construct entrypoint on JavaContainerBuilder; " + + "jvmFlags were set, but mainClass is null. Specify the main class using " + + "JavaContainerBuilder#setMainClass(String), or consider using MainClassFinder to " + + "infer the main class."); + } + if (classpathOrder.isEmpty()) { + throw new IllegalStateException( + "Failed to construct entrypoint because no files were added to the JavaContainerBuilder"); + } + + Map layerBuilders = new EnumMap<>(LayerType.class); + + // Add classes to layer configuration + for (PathPredicatePair directory : addedClasses) { + addDirectoryContentsToLayer( + layerBuilders, + LayerType.CLASSES, + directory.path, + directory.predicate, + appRoot.resolve(classesDestination)); + } + + // Add resources to layer configuration + for (PathPredicatePair directory : addedResources) { + addDirectoryContentsToLayer( + layerBuilders, + LayerType.RESOURCES, + directory.path, + directory.predicate, + appRoot.resolve(resourcesDestination)); + } + + // Detect duplicate filenames across all dependency layer types + Map occurrences = + Streams.concat( + addedDependencies.stream(), + addedSnapshotDependencies.stream(), + addedProjectDependencies.stream()) + .map(path -> path.getFileName().toString()) + .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())); + List duplicates = + occurrences.entrySet().stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + ImmutableMap> layerMap = + ImmutableMap.of( + LayerType.DEPENDENCIES, addedDependencies, + LayerType.SNAPSHOT_DEPENDENCIES, addedSnapshotDependencies, + LayerType.PROJECT_DEPENDENCIES, addedProjectDependencies); + for (Map.Entry> entry : layerMap.entrySet()) { + for (Path file : Preconditions.checkNotNull(entry.getValue())) { + // Handle duplicates by appending filesize to the end of the file. This renaming logic + // must be in sync with the code that does the same in the other place. See + // https://github.com/GoogleContainerTools/jib/issues/3331 + String jarName = file.getFileName().toString(); + if (duplicates.contains(jarName)) { + jarName = jarName.replaceFirst("\\.jar$", "-" + Files.size(file)) + ".jar"; + } + // Add dependencies to layer configuration + addFileToLayer( + layerBuilders, + entry.getKey(), + file, + appRoot.resolve(dependenciesDestination).resolve(jarName)); + } + } + + // Add others to layer configuration + for (Path path : addedOthers) { + if (Files.isDirectory(path)) { + addDirectoryContentsToLayer( + layerBuilders, + LayerType.EXTRA_FILES, + path, + ignored -> true, + appRoot.resolve(othersDestination)); + } else { + addFileToLayer( + layerBuilders, + LayerType.EXTRA_FILES, + path, + appRoot.resolve(othersDestination).resolve(path.getFileName())); + } + } + + // Add layer configurations to container builder + List layers = new ArrayList<>(); + layerBuilders.forEach((type, builder) -> layers.add(builder.setName(type.getName()).build())); + jibContainerBuilder.setFileEntriesLayers(layers); + + if (mainClass != null) { + // Construct entrypoint. Ensure classpath elements are in the same order as the files were + // added to the JavaContainerBuilder. + List classpathElements = new ArrayList<>(); + for (LayerType path : classpathOrder) { + switch (path) { + case CLASSES: + classpathElements.add(appRoot.resolve(classesDestination).toString()); + break; + case RESOURCES: + classpathElements.add(appRoot.resolve(resourcesDestination).toString()); + break; + case DEPENDENCIES: + classpathElements.add(appRoot.resolve(dependenciesDestination).resolve("*").toString()); + break; + case EXTRA_FILES: + classpathElements.add(appRoot.resolve(othersDestination).toString()); + break; + default: + throw new RuntimeException( + "Bug in jib-core; please report the bug at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); + } + } + String classpathString = String.join(":", classpathElements); + List entrypoint = new ArrayList<>(4 + jvmFlags.size()); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(classpathString); + entrypoint.add(mainClass); + jibContainerBuilder.setEntrypoint(entrypoint); + } + + return jibContainerBuilder; + } + + private JavaContainerBuilder addDirectory( + List addedPaths, Path directory, Predicate filter) + throws NoSuchFileException, NotDirectoryException { + if (!Files.exists(directory)) { + throw new NoSuchFileException(directory.toString()); + } + if (!Files.isDirectory(directory)) { + throw new NotDirectoryException(directory.toString()); + } + addedPaths.add(new PathPredicatePair(directory, filter)); + return this; + } + + private void addFileToLayer( + Map layerBuilders, + LayerType layerType, + Path sourceFile, + AbsoluteUnixPath pathInContainer) { + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, FileEntriesLayer.builder()); + } + Instant modificationTime = modificationTimeProvider.get(sourceFile, pathInContainer); + layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer, modificationTime); + } + + private void addDirectoryContentsToLayer( + Map layerBuilders, + LayerType layerType, + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer) + throws IOException { + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, FileEntriesLayer.builder()); + } + FileEntriesLayer.Builder builder = layerBuilders.get(layerType); + + new DirectoryWalker(sourceRoot) + .filterRoot() + .filter(path -> Files.isDirectory(path) || pathFilter.test(path)) + .walk( + path -> { + AbsoluteUnixPath pathOnContainer = + basePathInContainer.resolve(sourceRoot.relativize(path)); + Instant modificationTime = modificationTimeProvider.get(path, pathOnContainer); + builder.addEntry(path, pathOnContainer, modificationTime); + }); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java new file mode 100644 index 0000000000..a8387df0e6 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.nio.file.Paths; + +/** Build containers with Jib. */ +public class Jib { + + public static final String REGISTRY_IMAGE_PREFIX = "registry://"; + public static final String DOCKER_DAEMON_IMAGE_PREFIX = "docker://"; + public static final String TAR_IMAGE_PREFIX = "tar://"; + + /** + * Starts building the container from a base image. The type of base image can be specified using + * a prefix, e.g. {@code docker://gcr.io/project/image}. The available prefixes are described + * below: + * + *

    + *
  • No prefix, or {@code registry://}: uses a registry base image + *
  • {@code docker://}: uses a base image found in the local Docker daemon + *
  • {@code tar://}: uses a tarball base image at the path following the prefix + *
+ * + * @param baseImageReference the base image reference + * @return a new {@link JibContainerBuilder} to continue building the container + * @throws InvalidImageReferenceException if the {@code baseImageReference} is not a valid image + * reference + */ + public static JibContainerBuilder from(String baseImageReference) + throws InvalidImageReferenceException { + if (baseImageReference.startsWith(DOCKER_DAEMON_IMAGE_PREFIX)) { + return from( + DockerDaemonImage.named(baseImageReference.replaceFirst(DOCKER_DAEMON_IMAGE_PREFIX, ""))); + } + if (baseImageReference.startsWith(TAR_IMAGE_PREFIX)) { + return from(TarImage.at(Paths.get(baseImageReference.replaceFirst(TAR_IMAGE_PREFIX, "")))); + } + return from(RegistryImage.named(baseImageReference.replaceFirst(REGISTRY_IMAGE_PREFIX, ""))); + } + + /** + * Starts building the container from a base image. The base image should be publicly-available. + * For a base image that requires credentials, use {@link #from(RegistryImage)}. + * + * @param baseImageReference the base image reference + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(ImageReference baseImageReference) { + return from(RegistryImage.named(baseImageReference)); + } + + /** + * Starts building the container from a registry base image. + * + * @param registryImage the {@link RegistryImage} that defines base container registry and + * credentials + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(RegistryImage registryImage) { + return new JibContainerBuilder(registryImage); + } + + /** + * Starts building the container from a base image stored in the Docker cache. Requires a running + * Docker daemon. + * + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines the base image and Docker + * client + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(DockerDaemonImage dockerDaemonImage) { + return new JibContainerBuilder(dockerDaemonImage); + } + + /** + * Starts building the container from a tarball. + * + * @param tarImage the {@link TarImage} that defines the path to the base image + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(TarImage tarImage) { + return new JibContainerBuilder(tarImage); + } + + /** + * Starts building the container from an empty base image. + * + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder fromScratch() { + return from(ImageReference.scratch()); + } + + /** + * Starts building the container from a base image stored in the Docker cache. Requires a running + * Docker daemon. + * + * @param dockerClient the {@link DockerClient} to connect + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines the base image and Docker + * client + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from( + DockerClient dockerClient, DockerDaemonImage dockerDaemonImage) { + return new JibContainerBuilder(dockerClient, dockerDaemonImage); + } + + private Jib() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java new file mode 100644 index 0000000000..aeb50913d8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.common.annotations.VisibleForTesting; +import java.util.Objects; +import java.util.Set; + +/** The container built by Jib. */ +public class JibContainer { + + private final ImageReference targetImage; + private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; + private final Set tags; + private final boolean imagePushed; + + @VisibleForTesting + JibContainer( + ImageReference targetImage, + DescriptorDigest imageDigest, + DescriptorDigest imageId, + Set tags, + boolean imagePushed) { + this.targetImage = targetImage; + this.imageDigest = imageDigest; + this.imageId = imageId; + this.tags = tags; + this.imagePushed = imagePushed; + } + + static JibContainer from(BuildContext buildContext, BuildResult buildResult) { + ImageReference targetImage = buildContext.getTargetImageConfiguration().getImage(); + DescriptorDigest imageDigest = buildResult.getImageDigest(); + DescriptorDigest imageId = buildResult.getImageId(); + Set tags = buildContext.getAllTargetImageTags(); + return new JibContainer(targetImage, imageDigest, imageId, tags, buildResult.isImagePushed()); + } + + /** + * Get the target image that was built. + * + * @return the target image reference. + */ + public ImageReference getTargetImage() { + return targetImage; + } + + /** + * Returns true if we pushed this image all the way to a registry. + * + * @return true if pushed. + */ + public boolean isImagePushed() { + return imagePushed; + } + + /** + * Gets the digest of the registry image manifest built by Jib. This digest can be used to fetch a + * specific image from the registry in the form {@code myregistry/myimage@digest}. + * + * @return the image digest + */ + public DescriptorDigest getDigest() { + return imageDigest; + } + + /** + * Gets the digest of the container configuration built by Jib. + * + * @return the image ID + */ + public DescriptorDigest getImageId() { + return imageId; + } + + /** + * Get the tags applied to the container. + * + * @return the set of all tags + */ + public Set getTags() { + return tags; + } + + @Override + public int hashCode() { + return Objects.hash(targetImage, imageDigest, imageId, tags, imagePushed); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JibContainer)) { + return false; + } + JibContainer otherContainer = (JibContainer) other; + return targetImage.equals(otherContainer.targetImage) + && imageDigest.equals(otherContainer.imageDigest) + && imageId.equals(otherContainer.imageId) + && tags.equals(otherContainer.tags) + && imagePushed == otherContainer.imagePushed; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java new file mode 100644 index 0000000000..9a809ef798 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -0,0 +1,758 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.LayerObject; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.CliDockerClient; +import com.google.cloud.tools.jib.docker.DockerClientResolver; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.http.conn.HttpHostConnectException; + +/** + * Builds a container with Jib. + * + *

Example usage: + * + *

{@code
+ * Jib.from(baseImage)
+ *    .addLayer(sourceFiles, extractionPath)
+ *    .setEntrypoint("myprogram", "--flag", "subcommand")
+ *    .setProgramArguments("hello", "world")
+ *    .addEnvironmentVariable("HOME", "/app")
+ *    .addExposedPort(Port.tcp(8080))
+ *    .addLabel("containerizer", "jib")
+ *    .containerize(...);
+ * }
+ */ +public class JibContainerBuilder { + + private static String capitalizeFirstLetter(String string) { + if (string.isEmpty()) { + return string; + } + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + + private final ContainerBuildPlan.Builder containerBuildPlanBuilder = ContainerBuildPlan.builder(); + // TODO(chanseok): remove and use containerBuildPlanBuilder instead. Note that + // ContainerConfiguation implements equals() and hashCode(), so need to verify + // if they are required. + private final ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder(); + private final BuildContext.Builder buildContextBuilder; + + private ImageConfiguration baseImageConfiguration; + // TODO(chanseok): remove and use containerBuildPlanBuilder instead. + private List layerConfigurations = new ArrayList<>(); + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(RegistryImage baseImage) { + this( + ImageConfiguration.builder(baseImage.getImageReference()) + .setCredentialRetrievers(baseImage.getCredentialRetrievers()) + .build(), + BuildContext.builder()); + } + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(DockerDaemonImage baseImage) { + this( + ImageConfiguration.builder(baseImage.getImageReference()) + .setDockerClient( + DockerClientResolver.resolve(baseImage.getDockerEnvironment()) + .orElse( + new CliDockerClient( + baseImage.getDockerExecutable(), baseImage.getDockerEnvironment()))) + .build(), + BuildContext.builder()); + } + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(TarImage baseImage) { + // TODO: Cleanup using scratch as placeholder + this( + ImageConfiguration.builder(baseImage.getImageReference().orElse(ImageReference.scratch())) + .setTarPath(baseImage.getPath()) + .build(), + BuildContext.builder()); + } + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(DockerClient dockerClient, DockerDaemonImage baseImage) { + this( + ImageConfiguration.builder(baseImage.getImageReference()) + .setDockerClient(dockerClient) + .build(), + BuildContext.builder()); + } + + @VisibleForTesting + JibContainerBuilder( + ImageConfiguration imageConfiguration, BuildContext.Builder buildContextBuilder) { + this.buildContextBuilder = buildContextBuilder.setBaseImageConfiguration(imageConfiguration); + baseImageConfiguration = imageConfiguration; + containerBuildPlanBuilder.setBaseImage(imageConfiguration.getImage().toString()); + } + + /** + * Adds a new layer to the container with {@code files} as the source files and {@code + * pathInContainer} as the path to copy the source files to in the container file system. + * + *

Source files that are directories will be recursively copied. For example, if the source + * files are: + * + *

    + *
  • {@code fileFoo} + *
  • {@code fileBar} + *
  • {@code directory/} + *
+ * + *

and the destination to copy to is {@code /path/in/container}, then the new layer will have + * the following entries for the container file system: + * + *

    + *
  • {@code /path/in/container/fileFoo} + *
  • {@code /path/in/container/fileBar} + *
  • {@code /path/in/container/directory/} + *
  • {@code /path/in/container/directory/...} (all contents of {@code directory/}) + *
+ * + * @param files the source files to copy to a new layer in the container + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + * @throws IOException if an exception occurred when recursively listing any directories + */ + public JibContainerBuilder addLayer(List files, AbsoluteUnixPath pathInContainer) + throws IOException { + FileEntriesLayer.Builder layerConfigurationBuilder = FileEntriesLayer.builder(); + + for (Path file : files) { + layerConfigurationBuilder.addEntryRecursive( + file, pathInContainer.resolve(file.getFileName())); + } + + return addFileEntriesLayer(layerConfigurationBuilder.build()); + } + + /** + * Adds a new layer to the container with {@code files} as the source files and {@code + * pathInContainer} as the path to copy the source files to in the container file system. + * + * @param files the source files to copy to a new layer in the container + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + * @throws IOException if an exception occurred when recursively listing any directories + * @throws IllegalArgumentException if {@code pathInContainer} is not an absolute Unix-style path + * @see #addLayer(List, AbsoluteUnixPath) + */ + public JibContainerBuilder addLayer(List files, String pathInContainer) throws IOException { + return addLayer(files, AbsoluteUnixPath.get(pathInContainer)); + } + + /** + * Adds a layer (defined by a {@link LayerConfiguration}). + * + * @deprecated use {@link #addFileEntriesLayer(FileEntriesLayer)}. + * @param layerConfiguration the {@link LayerConfiguration} + * @return this + */ + @Deprecated + public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { + return addFileEntriesLayer(layerConfiguration.toFileEntriesLayer()); + } + + /** + * Adds a layer (defined by a {@link FileEntriesLayer}). + * + * @param layer the {@link FileEntriesLayer} + * @return this + */ + public JibContainerBuilder addFileEntriesLayer(FileEntriesLayer layer) { + containerBuildPlanBuilder.addLayer(layer); + layerConfigurations.add(layer); + return this; + } + + /** + * Sets the layers (defined by a list of {@link LayerConfiguration}s). This replaces any + * previously-added layers. + * + * @deprecated use {@link #setFileEntriesLayers(List)}. + * @param layerConfigurations the list of {@link LayerConfiguration}s + * @return this + */ + @Deprecated + public JibContainerBuilder setLayers(List layerConfigurations) { + return setFileEntriesLayers( + layerConfigurations.stream() + .map(LayerConfiguration::toFileEntriesLayer) + .collect(Collectors.toList())); + } + + /** + * Sets the layers (defined by a list of {@link FileEntriesLayer}s). This replaces any + * previously-added layers. + * + * @param layers the list of {@link FileEntriesLayer}s + * @return this + */ + public JibContainerBuilder setFileEntriesLayers(List layers) { + containerBuildPlanBuilder.setLayers(layers); + layerConfigurations = new ArrayList<>(layers); + return this; + } + + /** + * Sets the layers. This replaces any previously-added layers. + * + * @deprecated use {@link #setFileEntriesLayers(FileEntriesLayer...)}. + * @param layerConfigurations the {@link LayerConfiguration}s + * @return this + */ + @Deprecated + public JibContainerBuilder setLayers(LayerConfiguration... layerConfigurations) { + return setLayers(Arrays.asList(layerConfigurations)); + } + + /** + * Sets the layers. This replaces any previously-added layers. + * + * @param layers the {@link FileEntriesLayer}s + * @return this + */ + public JibContainerBuilder setFileEntriesLayers(FileEntriesLayer... layers) { + return setFileEntriesLayers(Arrays.asList(layers)); + } + + /** + * Sets the container entrypoint. This is the beginning of the command that is run when the + * container starts. {@link #setProgramArguments} sets additional tokens. + * + *

This is similar to {@code + * ENTRYPOINT} in Dockerfiles or {@code command} in the Kubernetes + * Container spec. + * + * @param entrypoint a list of the entrypoint command + * @return this + */ + public JibContainerBuilder setEntrypoint(@Nullable List entrypoint) { + containerBuildPlanBuilder.setEntrypoint(entrypoint); + containerConfigurationBuilder.setEntrypoint(entrypoint); + return this; + } + + /** + * Sets the container entrypoint. + * + * @param entrypoint the entrypoint command + * @return this + * @see #setEntrypoint(List) + */ + public JibContainerBuilder setEntrypoint(String... entrypoint) { + return setEntrypoint(Arrays.asList(entrypoint)); + } + + /** + * Sets the container entrypoint program arguments. These are additional tokens added to the end + * of the entrypoint command. + * + *

This is similar to {@code + * CMD} in Dockerfiles or {@code args} in the Kubernetes + * Container spec. + * + *

For example, if the entrypoint was {@code myprogram --flag subcommand} and program arguments + * were {@code hello world}, then the command that run when the container starts is {@code + * myprogram --flag subcommand hello world}. + * + * @param programArguments a list of program argument tokens + * @return this + */ + public JibContainerBuilder setProgramArguments(@Nullable List programArguments) { + containerBuildPlanBuilder.setCmd(programArguments); + containerConfigurationBuilder.setProgramArguments(programArguments); + return this; + } + + /** + * Sets the container entrypoint program arguments. + * + * @param programArguments program arguments tokens + * @return this + * @see #setProgramArguments(List) + */ + public JibContainerBuilder setProgramArguments(String... programArguments) { + return setProgramArguments(Arrays.asList(programArguments)); + } + + /** + * Sets the container environment. These environment variables are available to the program + * launched by the container entrypoint command. This replaces any previously-set environment + * variables. + * + *

This is similar to {@code + * ENV} in Dockerfiles or {@code env} in the Kubernetes + * Container spec. + * + * @param environmentMap a map of environment variable names to values + * @return this + */ + public JibContainerBuilder setEnvironment(Map environmentMap) { + containerBuildPlanBuilder.setEnvironment(environmentMap); + containerConfigurationBuilder.setEnvironment(environmentMap); + return this; + } + + /** + * Adds a variable in the container environment. + * + * @param name the environment variable name + * @param value the environment variable value + * @return this + * @see #setEnvironment + */ + public JibContainerBuilder addEnvironmentVariable(String name, String value) { + containerBuildPlanBuilder.addEnvironmentVariable(name, value); + containerConfigurationBuilder.addEnvironment(name, value); + return this; + } + + /** + * Sets the directories that may hold externally mounted volumes. + * + *

This is similar to {@code + * VOLUME} in Dockerfiles. + * + * @param volumes the directory paths on the container filesystem to set as volumes + * @return this + */ + public JibContainerBuilder setVolumes(Set volumes) { + containerBuildPlanBuilder.setVolumes(volumes); + containerConfigurationBuilder.setVolumes(volumes); + return this; + } + + /** + * Sets the directories that may hold externally mounted volumes. + * + * @param volumes the directory paths on the container filesystem to set as volumes + * @return this + * @see #setVolumes(Set) + */ + public JibContainerBuilder setVolumes(AbsoluteUnixPath... volumes) { + return setVolumes(new HashSet<>(Arrays.asList(volumes))); + } + + /** + * Adds a directory that may hold an externally mounted volume. + * + * @param volume a directory path on the container filesystem to represent a volume + * @return this + * @see #setVolumes(Set) + */ + public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { + containerBuildPlanBuilder.addVolume(volume); + containerConfigurationBuilder.addVolume(volume); + return this; + } + + /** + * Sets the ports to expose from the container. Ports exposed will allow ingress traffic. This + * replaces any previously-set exposed ports. + * + *

Use {@link Port#tcp} to expose a port for TCP traffic and {@link Port#udp} to expose a port + * for UDP traffic. + * + *

This is similar to {@code + * EXPOSE} in Dockerfiles or {@code ports} in the Kubernetes + * Container spec. + * + * @param ports the ports to expose + * @return this + */ + public JibContainerBuilder setExposedPorts(Set ports) { + containerBuildPlanBuilder.setExposedPorts(ports); + containerConfigurationBuilder.setExposedPorts(ports); + return this; + } + + /** + * Sets the ports to expose from the container. This replaces any previously-set exposed ports. + * + * @param ports the ports to expose + * @return this + * @see #setExposedPorts(Set) + */ + public JibContainerBuilder setExposedPorts(Port... ports) { + return setExposedPorts(new HashSet<>(Arrays.asList(ports))); + } + + /** + * Adds a port to expose from the container. + * + * @param port the port to expose + * @return this + * @see #setExposedPorts(Set) + */ + public JibContainerBuilder addExposedPort(Port port) { + containerBuildPlanBuilder.addExposedPort(port); + containerConfigurationBuilder.addExposedPort(port); + return this; + } + + /** + * Sets the labels for the container. This replaces any previously-set labels. + * + *

This is similar to {@code + * LABEL} in Dockerfiles. + * + * @param labelMap a map of label keys to values + * @return this + */ + public JibContainerBuilder setLabels(Map labelMap) { + containerBuildPlanBuilder.setLabels(labelMap); + containerConfigurationBuilder.setLabels(labelMap); + return this; + } + + /** + * Sets a label for the container. + * + * @param key the label key + * @param value the label value + * @return this + */ + public JibContainerBuilder addLabel(String key, String value) { + containerBuildPlanBuilder.addLabel(key, value); + containerConfigurationBuilder.addLabel(key, value); + return this; + } + + /** + * Sets the format to build the container image as. Use {@link ImageFormat#Docker} for Docker V2.2 + * or {@link ImageFormat#OCI} for OCI. + * + * @param imageFormat the {@link ImageFormat} + * @return this + */ + public JibContainerBuilder setFormat(ImageFormat imageFormat) { + containerBuildPlanBuilder.setFormat(imageFormat); + buildContextBuilder.setTargetFormat(imageFormat); + return this; + } + + /** + * Sets the container image creation time. The default is {@link Instant#EPOCH}. + * + * @param creationTime the container image creation time + * @return this + */ + public JibContainerBuilder setCreationTime(Instant creationTime) { + containerBuildPlanBuilder.setCreationTime(creationTime); + containerConfigurationBuilder.setCreationTime(creationTime); + return this; + } + + /** + * Sets a desired platform (properties including OS and architecture) list. If the base image + * reference is a Docker manifest list or an OCI image index, an image builder may select the base + * images matching the given platforms. If the base image reference is an image manifest, an image + * builder may ignore the given platforms and use the platform of the base image or may decide to + * raise on error. + * + *

Note that a new container builder starts with "amd64/linux" as the default platform. + * + * @param platforms list of platforms to select base images in case of a manifest list + * @return this + */ + public JibContainerBuilder setPlatforms(Set platforms) { + containerBuildPlanBuilder.setPlatforms(platforms); + containerConfigurationBuilder.setPlatforms(platforms); + return this; + } + + /** + * Adds a desired image platform (OS and architecture pair). If the base image reference is a + * Docker manifest list or an OCI image index, an image builder may select the base image matching + * the given platform. If the base image reference is an image manifest, an image builder may + * ignore the given platform and use the platform of the base image or may decide to raise on + * error. + * + *

Note that a new new container builder starts with "amd64/linux" as the default platform. If + * you want to reset the default platform instead of adding a new one, use {@link + * #setPlatforms(Set)}. + * + * @param architecture architecture (for example, {@code amd64}) to select a base image in case of + * a manifest list + * @param os OS (for example, {@code linux}) to select a base image in case of a manifest list + * @return this + */ + public JibContainerBuilder addPlatform(String architecture, String os) { + containerBuildPlanBuilder.addPlatform(architecture, os); + containerConfigurationBuilder.addPlatform(architecture, os); + return this; + } + + /** + * Sets the user and group to run the container as. {@code user} can be a username or UID along + * with an optional groupname or GID. + * + *

The following are valid formats for {@code user} + * + *

    + *
  • {@code user} + *
  • {@code uid} + *
  • {@code :group} + *
  • {@code :gid} + *
  • {@code user:group} + *
  • {@code uid:gid} + *
  • {@code uid:group} + *
  • {@code user:gid} + *
+ * + * @param user the user to run the container as + * @return this + */ + public JibContainerBuilder setUser(@Nullable String user) { + containerBuildPlanBuilder.setUser(user); + containerConfigurationBuilder.setUser(user); + return this; + } + + /** + * Sets the working directory in the container. + * + * @param workingDirectory the working directory + * @return this + */ + public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workingDirectory) { + containerBuildPlanBuilder.setWorkingDirectory(workingDirectory); + containerConfigurationBuilder.setWorkingDirectory(workingDirectory); + return this; + } + + /** + * Builds the container. + * + * @param containerizer the {@link Containerizer} that configures how to containerize + * @return the built container + * @throws IOException if an I/O exception occurs + * @throws CacheDirectoryCreationException if a directory to be used for the cache could not be + * created + * @throws HttpHostConnectException if jib failed to connect to a registry + * @throws RegistryUnauthorizedException if a registry request is unauthorized and needs + * authentication + * @throws RegistryAuthenticationFailedException if registry authentication failed + * @throws UnknownHostException if the registry does not exist + * @throws InsecureRegistryException if a server could not be verified due to an insecure + * connection + * @throws RegistryException if some other error occurred while interacting with a registry + * @throws ExecutionException if some other exception occurred during execution + * @throws InterruptedException if the execution was interrupted + */ + public JibContainer containerize(Containerizer containerizer) + throws InterruptedException, RegistryException, IOException, CacheDirectoryCreationException, + ExecutionException { + try (BuildContext buildContext = toBuildContext(containerizer); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildContext.getEventHandlers(), containerizer.getDescription())) { + logSources(buildContext.getEventHandlers()); + + BuildResult buildResult = containerizer.run(buildContext); + return JibContainer.from(buildContext, buildResult); + + } catch (ExecutionException ex) { + // If an ExecutionException occurs, re-throw the cause to be more easily handled by the user + if (ex.getCause() instanceof RegistryException) { + throw (RegistryException) ex.getCause(); + } + throw ex; + } + } + + /** + * Describes the container contents and configuration without actually physically building a + * container. + * + * @deprecated use {@link #toContainerBuildPlan}. + * @return a description of the container being built + */ + @Deprecated + public JibContainerDescription describeContainer() { + return new JibContainerDescription(layerConfigurations); + } + + /** + * Internal method. API end users should not use it. + * + *

Converts to {@link ContainerBuildPlan}. Note that not all values that this class holds can + * be described by a build plan, such as {@link CredentialRetriever}s for {@link RegistryImage}, + * {@link DockerClient} for {@link DockerDaemonImage}, and output path for {@link TarImage}. + * + * @return {@link ContainerBuildPlan} + */ + public ContainerBuildPlan toContainerBuildPlan() { + return containerBuildPlanBuilder.build(); + } + + /** + * Internal method. API end users should not use it. + * + *

Reconfigures {@link JibContainerBuilder} from the given {@code buildPlan}. Every value + * configurable using "setters" in this class is overwritten by the value in {@code buildPlan}; + * only retained are some base image properties inherent in {@link JibContainerBuilder} but absent + * in {@link ContainerBuildPlan}, such as {@link CredentialRetriever}s for {@link RegistryImage}, + * {@link DockerClient} for {@link DockerDaemonImage}, and output path for {@link TarImage}. + * + * @param buildPlan build plan to apply + * @return {@link JibContainerBuilder} reconfigured from {@code buildPlan} + * @throws InvalidImageReferenceException if the base image value in {@code buildPlan} is an + * invalid reference + */ + public JibContainerBuilder applyContainerBuildPlan(ContainerBuildPlan buildPlan) + throws InvalidImageReferenceException { + containerBuildPlanBuilder + .setBaseImage(buildPlan.getBaseImage()) + .setPlatforms(buildPlan.getPlatforms()) + .setCreationTime(buildPlan.getCreationTime()) + .setFormat(buildPlan.getFormat()) + .setEnvironment(buildPlan.getEnvironment()) + .setLabels(buildPlan.getLabels()) + .setVolumes(buildPlan.getVolumes()) + .setExposedPorts(buildPlan.getExposedPorts()) + .setUser(buildPlan.getUser()) + .setWorkingDirectory(buildPlan.getWorkingDirectory()) + .setEntrypoint(buildPlan.getEntrypoint()) + .setCmd(buildPlan.getCmd()) + .setLayers(buildPlan.getLayers()); + + containerConfigurationBuilder + .setPlatforms(buildPlan.getPlatforms()) + .setCreationTime(buildPlan.getCreationTime()) + .setEnvironment(buildPlan.getEnvironment()) + .setLabels(buildPlan.getLabels()) + .setVolumes(buildPlan.getVolumes()) + .setExposedPorts(buildPlan.getExposedPorts()) + .setUser(buildPlan.getUser()) + .setWorkingDirectory(buildPlan.getWorkingDirectory()) + .setEntrypoint(buildPlan.getEntrypoint()) + .setProgramArguments(buildPlan.getCmd()); + + ImageConfiguration.Builder builder = + ImageConfiguration.builder(ImageReference.parse(buildPlan.getBaseImage())) + .setCredentialRetrievers(baseImageConfiguration.getCredentialRetrievers()); + baseImageConfiguration.getDockerClient().ifPresent(builder::setDockerClient); + baseImageConfiguration.getTarPath().ifPresent(builder::setTarPath); + baseImageConfiguration = builder.build(); + + // For now, only FileEntriesLayer is supported in jib-core. + Function castToFileEntriesLayer = + layer -> { + Verify.verify( + layer instanceof FileEntriesLayer, + "layer types other than FileEntriesLayer not yet supported in build plan layers"); + return (FileEntriesLayer) layer; + }; + layerConfigurations = + buildPlan.getLayers().stream().map(castToFileEntriesLayer).collect(Collectors.toList()); + + buildContextBuilder + .setTargetFormat(buildPlan.getFormat()) + .setBaseImageConfiguration(baseImageConfiguration) + .setLayerConfigurations(layerConfigurations); + return this; + } + + /** + * Builds a {@link BuildContext} using this and a {@link Containerizer}. + * + * @param containerizer the {@link Containerizer} + * @return the {@link BuildContext} + * @throws CacheDirectoryCreationException if a cache directory could not be created + */ + @VisibleForTesting + BuildContext toBuildContext(Containerizer containerizer) throws CacheDirectoryCreationException { + return buildContextBuilder + .setTargetImageConfiguration(containerizer.getImageConfiguration()) + .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) + .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) + .setApplicationLayersCacheDirectory(containerizer.getApplicationLayersCacheDirectory()) + .setContainerConfiguration(containerConfigurationBuilder.build()) + .setLayerConfigurations(layerConfigurations) + .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) + .setOffline(containerizer.isOfflineMode()) + .setToolName(containerizer.getToolName()) + .setToolVersion(containerizer.getToolVersion()) + .setExecutorService(containerizer.getExecutorService().orElse(null)) + .setEventHandlers(containerizer.buildEventHandlers()) + .setAlwaysCacheBaseImage(containerizer.getAlwaysCacheBaseImage()) + .setRegistryMirrors(containerizer.getRegistryMirrors()) + .build(); + } + + private void logSources(EventHandlers eventHandlers) { + // Logs the different source files used. + eventHandlers.dispatch(LogEvent.info("Containerizing application with the following files:")); + + for (FileEntriesLayer layer : layerConfigurations) { + if (layer.getEntries().isEmpty()) { + continue; + } + + eventHandlers.dispatch(LogEvent.info("\t" + capitalizeFirstLetter(layer.getName()) + ":")); + + for (FileEntry entry : layer.getEntries()) { + eventHandlers.dispatch(LogEvent.info("\t\t" + entry.getSourceFile())); + } + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java new file mode 100644 index 0000000000..dcd72761fa --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** + * A class containing the representation of the contents of a container. Currently only exposes + * "layers", but can be extended to expose {@link ContainerConfiguration}, {@link ImageReference} of + * the base image, or other informational classes. + * + *

This class is immutable and thread-safe. + */ +public class JibContainerDescription { + + private final ImmutableList layers; + + JibContainerDescription(List layers) { + this.layers = ImmutableList.copyOf(layers); + } + + /** + * Returns a list of "user configured" layers, does not include base layer information. + * + * @return a {@link List} of {@link FileEntriesLayer}s + */ + public List getFileEntriesLayers() { + return layers; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java new file mode 100644 index 0000000000..ea9a035546 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +/** + * Type for events dispatched by Jib Core. Implementation classes should not inherit from + * each other. + */ +public interface JibEvent {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java new file mode 100644 index 0000000000..fb410268c5 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java @@ -0,0 +1,295 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.api.buildplan.FilePermissionsProvider; +import com.google.cloud.tools.jib.api.buildplan.ModificationTimeProvider; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.concurrent.Immutable; + +/** + * Configures how to build a layer in the container image. Instantiate with {@link #builder}. + * + * @deprecated Use {@link FileEntriesLayer}. + */ +@Deprecated +@Immutable +public class LayerConfiguration { + + /** Builds a {@link LayerConfiguration}. */ + public static class Builder { + + private FileEntriesLayer.Builder layerBuilder = FileEntriesLayer.builder(); + + private Builder() {} + + /** + * Sets a name for this layer. This name does not affect the contents of the layer. + * + * @param name the name + * @return this + */ + public Builder setName(String name) { + layerBuilder.setName(name); + return this; + } + + /** + * Sets entries for the layer. + * + * @param entries file entries in the layer + * @return this + */ + public Builder setEntries(List entries) { + layerBuilder.setEntries( + entries.stream().map(LayerEntry::toFileEntry).collect(Collectors.toList())); + return this; + } + + /** + * Adds an entry to the layer. + * + * @param entry the layer entry to add + * @return this + */ + public Builder addEntry(LayerEntry entry) { + layerBuilder.addEntry(entry.toFileEntry()); + return this; + } + + /** + * Adds an entry to the layer. Only adds the single source file to the exact path in the + * container file system. + * + *

For example, {@code addEntry(Paths.get("myfile"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a file {@code myfile} to the container file + * system at {@code /path/in/container}. + * + *

For example, {@code addEntry(Paths.get("mydirectory"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a directory {@code mydirectory/} to the + * container file system at {@code /path/in/container/}. This does not add the contents + * of {@code mydirectory}. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + */ + public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { + layerBuilder.addEntry(sourceFile, pathInContainer); + return this; + } + + /** + * Adds an entry to the layer with the given permissions. Only adds the single source file to + * the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, FilePermissions permissions) { + layerBuilder.addEntry(sourceFile, pathInContainer, permissions); + return this; + } + + /** + * Adds an entry to the layer with the given file modification time. Only adds the single source + * file to the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param modificationTime the file modification time + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, Instant modificationTime) { + layerBuilder.addEntry(sourceFile, pathInContainer, modificationTime); + return this; + } + + /** + * Adds an entry to the layer with the given permissions and file modification time. Only adds + * the single source file to the exact path in the container file system. See {@link + * Builder#addEntry(Path, AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS + */ + public Builder addEntry( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissions permissions, + Instant modificationTime) { + layerBuilder.addEntry( + new FileEntry(sourceFile, pathInContainer, permissions, modificationTime)); + return this; + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + *

For example, {@code addEntryRecursive(Paths.get("mydirectory", + * AbsoluteUnixPath.get("/path/in/container"))} adds {@code mydirectory} to the container file + * system at {@code /path/in/container} such that {@code mydirectory/subfile} is found at {@code + * /path/in/container/subfile}. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive(Path sourceFile, AbsoluteUnixPath pathInContainer) + throws IOException { + layerBuilder.addEntryRecursive(sourceFile, pathInContainer); + return this; + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissionsProvider filePermissionProvider) + throws IOException { + layerBuilder.addEntryRecursive(sourceFile, pathInContainer, filePermissionProvider); + return this; + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @param modificationTimeProvider a provider that takes a source path and destination path on + * the container and returns the file modification time that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissionsProvider filePermissionProvider, + ModificationTimeProvider modificationTimeProvider) + throws IOException { + layerBuilder.addEntryRecursive( + sourceFile, pathInContainer, filePermissionProvider, modificationTimeProvider); + return this; + } + + /** + * Returns the built {@link LayerConfiguration}. + * + * @return the built {@link LayerConfiguration} + */ + public LayerConfiguration build() { + return new LayerConfiguration(layerBuilder.build()); + } + } + + /** Provider that returns default file permissions (644 for files, 755 for directories). */ + public static final FilePermissionsProvider DEFAULT_FILE_PERMISSIONS_PROVIDER = + FileEntriesLayer.DEFAULT_FILE_PERMISSIONS_PROVIDER; + + /** Default file modification time (EPOCH + 1 second). */ + public static final Instant DEFAULT_MODIFICATION_TIME = + FileEntriesLayer.DEFAULT_MODIFICATION_TIME; + + /** Provider that returns default file modification time (EPOCH + 1 second). */ + public static final ModificationTimeProvider DEFAULT_MODIFICATION_TIME_PROVIDER = + FileEntriesLayer.DEFAULT_MODIFICATION_TIME_PROVIDER; + + /** + * Gets a new {@link Builder} for {@link LayerConfiguration}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + private final FileEntriesLayer fileEntriesLayer; + + private LayerConfiguration(FileEntriesLayer fileEntriesLayer) { + this.fileEntriesLayer = fileEntriesLayer; + } + + /** + * Gets the name. + * + * @return the name + */ + public String getName() { + return fileEntriesLayer.getName(); + } + + /** + * Gets the list of entries. + * + * @return the list of entries + */ + public ImmutableList getLayerEntries() { + List entries = fileEntriesLayer.getEntries(); + return entries.stream().map(LayerEntry::new).collect(ImmutableList.toImmutableList()); + } + + FileEntriesLayer toFileEntriesLayer() { + return fileEntriesLayer; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java new file mode 100644 index 0000000000..dd12b966b9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java @@ -0,0 +1,136 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import java.nio.file.Path; +import java.time.Instant; +import javax.annotation.concurrent.Immutable; + +/** + * Represents an entry in the layer. A layer consists of many entries that can be converted into tar + * archive entries. + * + *

This class is immutable and thread-safe. + * + * @deprecated Use {@link FileEntry}. + */ +@Deprecated +@Immutable +public class LayerEntry { + + private final FileEntry fileEntry; + + /** + * Instantiates with a source file and the path to place the source file in the container file + * system. + * + *

For example, {@code new LayerEntry(Paths.get("HelloWorld.class"), + * AbsoluteUnixPath.get("/app/classes/HelloWorld.class"))} adds a file {@code HelloWorld.class} to + * the container file system at {@code /app/classes/HelloWorld.class}. + * + *

For example, {@code new LayerEntry(Paths.get("com"), + * AbsoluteUnixPath.get("/app/classes/com"))} adds a directory to the container file system at + * {@code /app/classes/com}. This does not add the contents of {@code com/}. + * + *

Note that: + * + *

    + *
  • Entry source files can be either files or directories. + *
  • Adding a directory does not include the contents of the directory. Each file under a + * directory must be added as a separate {@link LayerEntry}. + *
+ * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param modificationTime the file modification time + */ + public LayerEntry( + Path sourceFile, + AbsoluteUnixPath extractionPath, + FilePermissions permissions, + Instant modificationTime) { + this(new FileEntry(sourceFile, extractionPath, permissions, modificationTime)); + } + + LayerEntry(FileEntry entry) { + fileEntry = entry; + } + + /** + * Returns the modification time of the file in the entry. + * + * @return the modification time + */ + public Instant getModificationTime() { + return fileEntry.getModificationTime(); + } + + /** + * Gets the source file. The source file may be relative or absolute, so the caller should use + * {@code getSourceFile().toAbsolutePath().toString()} for the serialized form since the + * serialization could change independently of the path representation. + * + * @return the source file + */ + public Path getSourceFile() { + return fileEntry.getSourceFile(); + } + + /** + * Gets the extraction path. + * + * @return the extraction path + */ + public AbsoluteUnixPath getExtractionPath() { + return fileEntry.getExtractionPath(); + } + + /** + * Gets the file permissions on the container. + * + * @return the file permissions on the container + */ + public FilePermissions getPermissions() { + return fileEntry.getPermissions(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LayerEntry)) { + return false; + } + LayerEntry otherLayerEntry = (LayerEntry) other; + return toFileEntry().equals(otherLayerEntry.toFileEntry()); + } + + @Override + public int hashCode() { + return fileEntry.hashCode(); + } + + FileEntry toFileEntry() { + return fileEntry; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java new file mode 100644 index 0000000000..ddcaef22f7 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java @@ -0,0 +1,125 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Objects; + +/** Log message event. */ +public class LogEvent implements JibEvent { + + /** Log levels, in order of verbosity. */ + public enum Level { + + /** Something went wrong. */ + ERROR, + + /** Something might not work as intended. */ + WARN, + + /** Default. */ + LIFECYCLE, + + /** Same as {@link #LIFECYCLE}, except represents progress updates. */ + PROGRESS, + + /** + * Details that can be ignored. + * + *

Use {@link #LIFECYCLE} for progress-indicating messages. + */ + INFO, + + /** Useful for debugging. */ + DEBUG + } + + public static LogEvent error(String message) { + return new LogEvent(Level.ERROR, message); + } + + public static LogEvent lifecycle(String message) { + return new LogEvent(Level.LIFECYCLE, message); + } + + public static LogEvent progress(String message) { + return new LogEvent(Level.PROGRESS, message); + } + + public static LogEvent warn(String message) { + return new LogEvent(Level.WARN, message); + } + + public static LogEvent info(String message) { + return new LogEvent(Level.INFO, message); + } + + public static LogEvent debug(String message) { + return new LogEvent(Level.DEBUG, message); + } + + private final Level level; + private final String message; + + private LogEvent(Level level, String message) { + this.level = level; + this.message = message; + } + + /** + * Gets the log level to log at. + * + * @return the log level + */ + public Level getLevel() { + return level; + } + + /** + * Gets the log message. + * + * @return the log message + */ + public String getMessage() { + return message; + } + + @VisibleForTesting + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof LogEvent)) { + return false; + } + + LogEvent otherLogEvent = (LogEvent) other; + return level == otherLogEvent.level && message.equals(otherLogEvent.message); + } + + @VisibleForTesting + @Override + public int hashCode() { + return Objects.hash(level, message); + } + + @Override + public String toString() { + return "LogEvent [level=" + level + ", message=" + message + "]"; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java new file mode 100644 index 0000000000..a3708b6307 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java @@ -0,0 +1,261 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Finds main classes in a list of class files. Main classes are classes that define a valid main + * method. + * + *

For class files compiled with Java 25 or later (JEP 512), valid main methods include: + * + *

    + *
  • {@code static void main(String[] args)} - with public, protected, or package-private access + *
  • {@code static void main()} - static main without parameters + *
  • {@code void main(String[] args)} - instance main with parameters + *
  • {@code void main()} - instance main without parameters + *
+ * + *

For class files compiled with earlier Java versions, only the traditional {@code public static + * void main(String[] args)} is recognized. + */ +public class MainClassFinder { + + /** The result of a call to {@link #find}. */ + public static class Result { + + /** The type of result. */ + public enum Type { + + // Found a single main class. + MAIN_CLASS_FOUND, + + // Did not find any main class. + MAIN_CLASS_NOT_FOUND, + + // Found multiple main classes. + MULTIPLE_MAIN_CLASSES + } + + private static Result success(String foundMainClass) { + return new Result(Type.MAIN_CLASS_FOUND, Collections.singletonList(foundMainClass)); + } + + private static Result mainClassNotFound() { + return new Result(Type.MAIN_CLASS_NOT_FOUND, Collections.emptyList()); + } + + private static Result multipleMainClasses(List foundMainClasses) { + return new Result(Type.MULTIPLE_MAIN_CLASSES, foundMainClasses); + } + + private final Type type; + private final List foundMainClasses; + + private Result(Type type, List foundMainClasses) { + this.foundMainClasses = foundMainClasses; + this.type = type; + } + + /** + * Gets the found main class. Only call if {@link #getType} is {@link Type#MAIN_CLASS_FOUND}. + * + * @return the found main class + */ + public String getFoundMainClass() { + Preconditions.checkState(Type.MAIN_CLASS_FOUND == type); + Preconditions.checkState(foundMainClasses.size() == 1); + return foundMainClasses.get(0); + } + + /** + * Gets the type of the result. + * + * @return the type of the result + */ + public Type getType() { + return type; + } + + /** + * Gets the found main classes. + * + * @return the found main classes + */ + public List getFoundMainClasses() { + return foundMainClasses; + } + } + + /** {@link ClassVisitor} that keeps track of whether or not it has visited a main class. */ + private static class MainClassVisitor extends ClassVisitor { + + /** Java 25 class file major version (flexible main methods finalized). */ + private static final int JAVA_25_CLASS_VERSION = 69; + + /** The return/argument types for main with String[] parameter. */ + private static final String MAIN_WITH_ARGS_DESCRIPTOR = + org.objectweb.asm.Type.getMethodDescriptor( + org.objectweb.asm.Type.VOID_TYPE, org.objectweb.asm.Type.getType(String[].class)); + + /** The return/argument types for main without parameters. */ + private static final String MAIN_NO_ARGS_DESCRIPTOR = + org.objectweb.asm.Type.getMethodDescriptor(org.objectweb.asm.Type.VOID_TYPE); + + /** Optional modifiers that main may or may not have. */ + private static final int OPTIONAL_MODIFIERS = + Opcodes.ACC_FINAL | Opcodes.ACC_DEPRECATED | Opcodes.ACC_VARARGS | Opcodes.ACC_SYNTHETIC; + + private boolean visitedMainClass; + private int classVersion; + + private MainClassVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + this.classVersion = version; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + @Nullable + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if (!name.equals("main")) { + return null; + } + + if ((access & Opcodes.ACC_PRIVATE) != 0) { + return null; + } + + // For class files before Java 25, only traditional main is valid + if (classVersion < JAVA_25_CLASS_VERSION) { + // Traditional main: public static void main(String[] args) + int requiredAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC; + if ((access & ~OPTIONAL_MODIFIERS) == requiredAccess + && descriptor.equals(MAIN_WITH_ARGS_DESCRIPTOR)) { + visitedMainClass = true; + } + return null; + } + + // For Java 25+, check flexible main method signatures (JEP 512) + boolean isValidDescriptor = + descriptor.equals(MAIN_WITH_ARGS_DESCRIPTOR) + || descriptor.equals(MAIN_NO_ARGS_DESCRIPTOR); + + if (!isValidDescriptor) { + return null; + } + + int relevantAccess = + access & ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | OPTIONAL_MODIFIERS); + if (relevantAccess == Opcodes.ACC_STATIC || relevantAccess == 0) { + visitedMainClass = true; + } + + return null; + } + } + + /** + * Tries to find classes with valid main methods (see class javadoc) in {@code files}. + * + * @param files the files to search + * @param logger a {@link Consumer} used to handle log events + * @return the {@link Result} of the main class finding attempt + */ + public static Result find(List files, Consumer logger) { + List mainClasses = new ArrayList<>(); + for (Path file : files) { + // Makes sure classFile is valid. + if (!Files.exists(file)) { + logger.accept(LogEvent.debug("MainClassFinder: " + file + " does not exist; ignoring")); + continue; + } + if (!Files.isRegularFile(file)) { + logger.accept( + LogEvent.debug("MainClassFinder: " + file + " is not a regular file; skipping")); + continue; + } + if (!file.toString().endsWith(".class")) { + logger.accept( + LogEvent.debug("MainClassFinder: " + file + " is not a class file; skipping")); + continue; + } + + MainClassVisitor mainClassVisitor = new MainClassVisitor(); + try (InputStream classFileInputStream = Files.newInputStream(file)) { + ClassReader reader = new ClassReader(classFileInputStream); + reader.accept(mainClassVisitor, 0); + if (mainClassVisitor.visitedMainClass) { + mainClasses.add(reader.getClassName().replace('/', '.')); + } + + } catch (IllegalArgumentException ex) { + throw new UnsupportedOperationException( + "Check the full stace trace, and if the root cause is from ASM ClassReader about " + + "unsupported class file version, see " + + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md" + + "#i-am-seeing-unsupported-class-file-major-version-when-building", + ex); + + } catch (ArrayIndexOutOfBoundsException ignored) { + // Not a valid class file (thrown by ClassReader if it reads an invalid format) + logger.accept(LogEvent.warn("Invalid class file found: " + file)); + + } catch (IOException ignored) { + // Could not read class file. + logger.accept(LogEvent.warn("Could not read file: " + file)); + } + } + + if (mainClasses.size() == 1) { + // Valid class found. + return Result.success(mainClasses.get(0)); + } + if (mainClasses.isEmpty()) { + // No main class found anywhere. + return Result.mainClassNotFound(); + } + // More than one main class found. + return Result.multipleMainClasses(mainClasses); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java similarity index 77% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java index e2eb8c0613..8e3cd1c906 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,18 +14,18 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Utility for parsing exposed ports from plugin configuration */ -public class ExposedPortsParser { +/** Utility for parsing Docker/OCI ports from text representations. */ +public class Ports { /** * Pattern used for parsing information out of exposed port configurations. @@ -41,12 +41,13 @@ public class ExposedPortsParser { *

For example: ["1000", "2000-2002"] will expand to a list of {@link Port}s with the port * numbers [1000, 2000, 2001, 2002] * - * @param ports the list of port numbers/ranges + * @param ports the list of port numbers/ranges, with an optional protocol separated by a '/' + * (defaults to TCP if missing). * @return the ports as a list of {@link Port} * @throws NumberFormatException if any of the ports are in an invalid format or out of range */ - public static ImmutableList parse(List ports) throws NumberFormatException { - ImmutableList.Builder result = new ImmutableList.Builder<>(); + public static Set parse(List ports) throws NumberFormatException { + Set result = new HashSet<>(); for (String port : ports) { Matcher matcher = portPattern.matcher(port); @@ -66,7 +67,7 @@ public static ImmutableList parse(List ports) throws NumberFormatE if (!Strings.isNullOrEmpty(matcher.group(2))) { max = Integer.parseInt(matcher.group(2)); } - Protocol protocol = Protocol.getFromString(matcher.group(3)); + String protocol = matcher.group(3); // Error if configured as 'max-min' instead of 'min-max' if (min > max) { @@ -81,12 +82,12 @@ public static ImmutableList parse(List ports) throws NumberFormatE } for (int portNumber = min; portNumber <= max; portNumber++) { - result.add(new Port(portNumber, protocol)); + result.add(Port.parseProtocol(portNumber, protocol)); } } - return result.build(); + return result; } - private ExposedPortsParser() {} + private Ports() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java new file mode 100644 index 0000000000..aa97bf971f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.text.MessageFormat; + +/** Thrown because registry authentication failed. */ +public class RegistryAuthenticationFailedException extends RegistryException { + + private static final String REASON = "Failed to authenticate with registry {0}/{1} because: {2}"; + private final String serverUrl; + private final String imageName; + + /** + * Creates a new exception with a human readable message. + * + * @param serverUrl the registry server url + * @param imageName the image name that requires authentication + * @param cause the underlying cause that triggered this exception + */ + public RegistryAuthenticationFailedException( + String serverUrl, String imageName, Throwable cause) { + super(MessageFormat.format(REASON, serverUrl, imageName, cause.getMessage()), cause); + this.serverUrl = serverUrl; + this.imageName = imageName; + } + + /** + * Creates a new exception with a human readable message. + * + * @param serverUrl the registry server url + * @param imageName the image name that requires authentication + * @param reason the underlying reason that triggered this exception + */ + public RegistryAuthenticationFailedException(String serverUrl, String imageName, String reason) { + super(MessageFormat.format(REASON, serverUrl, imageName, reason)); + this.serverUrl = serverUrl; + this.imageName = imageName; + } + + /** + * The server being authenticated. + * + * @return the server being authenticated + */ + public String getServerUrl() { + return serverUrl; + } + + /** + * The image being authenticated. + * + * @return the image being authenticated + */ + public String getImageName() { + return imageName; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java similarity index 90% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java index c14cda8208..9dcfd0900b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import javax.annotation.Nullable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java new file mode 100644 index 0000000000..a7fd5854bb --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Defines an image on a container registry that can be used as either a source or target image. + * + *

The registry portion of the image reference determines which registry to the image lives (or + * should live) on. The repository portion is the namespace within the registry. The tag is a label + * to easily identify an image among all the images in the repository. See {@link ImageReference} + * for more details. + * + *

When configuring credentials (via {@link #addCredential} for example), make sure the + * credentials are valid push (for using this as a target image) or pull (for using this as a source + * image) credentials for the repository specified via the image reference. + */ +public class RegistryImage { + + /** + * Instantiate with the image reference to use. + * + * @param imageReference the image reference + * @return a new {@link RegistryImage} + */ + public static RegistryImage named(ImageReference imageReference) { + return new RegistryImage(imageReference); + } + + /** + * Instantiate with the image reference to use. + * + * @param imageReference the image reference + * @return a new {@link RegistryImage} + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public static RegistryImage named(String imageReference) throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + private final ImageReference imageReference; + private final List credentialRetrievers = new ArrayList<>(); + + /** Instantiate with {@link #named}. */ + private RegistryImage(ImageReference imageReference) { + this.imageReference = imageReference; + } + + /** + * Adds a username-password credential to use to push/pull the image. This is a shorthand for + * {@code addCredentialRetriever(() -> Optional.of(Credential.basic(username, password)))}. + * + * @param username the username + * @param password the password + * @return this + */ + public RegistryImage addCredential(String username, String password) { + addCredentialRetriever(() -> Optional.of(Credential.from(username, password))); + return this; + } + + /** + * Adds {@link CredentialRetriever} to fetch push/pull credentials for the image. Credential + * retrievers are attempted in the order in which they are specified until credentials are + * successfully retrieved. + * + *

Example usage: + * + *

{@code
+   * .addCredentialRetriever(() -> {
+   *   if (!Files.exists("secret.txt") {
+   *     return Optional.empty();
+   *   }
+   *   try {
+   *     String password = fetchPasswordFromFile("secret.txt");
+   *     return Credential.basic("myaccount", password);
+   *
+   *   } catch (IOException ex) {
+   *     throw new CredentialRetrievalException("Failed to load password", ex);
+   *   }
+   * })
+   * }
+ * + * @param credentialRetriever the {@link CredentialRetriever} to add + * @return this + */ + public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetriever) { + credentialRetrievers.add(credentialRetriever); + return this; + } + + ImageReference getImageReference() { + return imageReference; + } + + List getCredentialRetrievers() { + return credentialRetrievers; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java similarity index 80% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java index d65dd32071..fa0e92daf2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,9 +14,10 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.http.ResponseException; /** Thrown when a registry request was unauthorized and therefore authentication is needed. */ public class RegistryUnauthorizedException extends RegistryException { @@ -32,25 +33,17 @@ public class RegistryUnauthorizedException extends RegistryException { * @param cause the cause */ public RegistryUnauthorizedException( - String registry, String repository, HttpResponseException cause) { + String registry, String repository, ResponseException cause) { super("Unauthorized for " + registry + "/" + repository, cause); this.registry = registry; this.repository = repository; } - public String getRegistry() { - return registry; - } - - public String getRepository() { - return repository; - } - public String getImageReference() { return registry + "/" + repository; } public HttpResponseException getHttpResponseException() { - return (HttpResponseException) getCause(); + return (HttpResponseException) getCause().getCause(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java new file mode 100644 index 0000000000..34da7112e3 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.nio.file.Path; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * Builds to a tarball archive. + * + *

Usage example: + * + *

{@code
+ * TarImage tarImage = TarImage.at(Paths.get("image.tar"))
+ *                             .named("myimage");
+ * }
+ */ +public class TarImage { + + /** + * Constructs a {@link TarImage} with the specified path. + * + * @param path the path to the tarball archive + * @return a new {@link TarImage} + */ + public static TarImage at(Path path) { + return new TarImage(path); + } + + private final Path path; + @Nullable private ImageReference imageReference; + + /** Instantiate with {@link #at}. */ + private TarImage(Path path) { + this.path = path; + } + + /** + * Sets the name of the image. This is the name that shows up when the tar is loaded by the Docker + * daemon. + * + * @param imageReference the image reference + * @return this + */ + public TarImage named(ImageReference imageReference) { + this.imageReference = imageReference; + return this; + } + + /** + * Sets the name of the image. This is the name that shows up when the tar is loaded by the Docker + * daemon. + * + * @param imageReference the image reference + * @return this + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public TarImage named(String imageReference) throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + Path getPath() { + return path; + } + + Optional getImageReference() { + return Optional.ofNullable(imageReference); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java deleted file mode 100644 index f4d04248d6..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.async; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; - -/** - * Holds the future for an asynchronously-running step. Implementations should: - * - *
    - *
  1. Be immutable - *
  2. Construct with the dependent {@link AsyncStep}s and submit a {@link Callable} to the {@link - * ListeningExecutorService} to run after all its dependent {@link AsyncStep}s (for example, - * by using {@link Futures#whenAllSucceed}) - *
  3. Have {@link #getFuture} return the submitted future - *
- * - * @param the object type passed on by this step - */ -public interface AsyncStep { - - /** @return the submitted future */ - // TODO: Consider changing this to be orchestrated by an AsyncStepsBuilder. - ListenableFuture getFuture(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java index 38bf268460..40a4092ad0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -30,4 +30,12 @@ public interface Blob { * @throws IOException if writing the BLOB fails */ BlobDescriptor writeTo(OutputStream outputStream) throws IOException; + + /** + * Whether {@link #writeTo(OutputStream)} is retryable. + * + * @return {@code true} if {@link #writeTo(OutputStream)} can be called multiple times. {@code + * false} otherwise. + */ + boolean isRetryable(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java index e41f0286e2..0481a656a0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,7 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.ByteStreams; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import com.google.cloud.tools.jib.api.DescriptorDigest; /** Contains properties describing a BLOB, including its digest and possibly its size (in bytes). */ public class BlobDescriptor { @@ -31,24 +26,6 @@ public class BlobDescriptor { /** The size of the BLOB (in bytes). Negative if unknown. */ private final long size; - /** - * Creates a new {@link BlobDescriptor} from the contents of an {@link InputStream} while piping - * to an {@link OutputStream}. Does not close either streams. - * - * @param inputStream the stream to read the contents from - * @param outputStream the {@link OutputStream} to pipe to - * @return a {@link BlobDescriptor} of the piped contents - * @throws IOException if reading from or writing to the streams fails - */ - static BlobDescriptor fromPipe(InputStream inputStream, OutputStream outputStream) - throws IOException { - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(outputStream); - ByteStreams.copy(inputStream, countingDigestOutputStream); - countingDigestOutputStream.flush(); - return countingDigestOutputStream.toBlobDescriptor(); - } - public BlobDescriptor(long size, DescriptorDigest digest) { this.size = size; this.digest = digest; @@ -76,7 +53,9 @@ public long getSize() { } /** - * Two {@link BlobDescriptor} objects are equal if their + * Checks if two {@link BlobDescriptor}s are equal. + * + *

Two blobs are equal if their: * *

    *
  1. {@code digest}s are not null and equal, and diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 81edbd3459..1627308c34 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.WritableContents; +import com.google.cloud.tools.jib.json.JsonTemplate; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -33,16 +35,26 @@ public static Blob from(Path file) { return new FileBlob(file); } + public static Blob from(JsonTemplate template) { + return new JsonBlob(template); + } + + /** + * Creates a {@link StringBlob} with UTF-8 encoding. + * + * @param content the string to create the blob from + * @return the {@link StringBlob} + */ public static Blob from(String content) { return new StringBlob(content); } - public static Blob from(BlobWriter writer) { - return new WriterBlob(writer); + public static Blob from(WritableContents writable, boolean retryable) { + return new WritableContentsBlob(writable, retryable); } /** - * Writes the BLOB to a string. + * Writes the BLOB to a string with UTF-8 decoding. * * @param blob the BLOB to write * @return the BLOB contents as a string diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java index 745774eb7c..e686b3ed42 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -34,8 +35,13 @@ class FileBlob implements Blob { @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - try (InputStream fileStream = new BufferedInputStream(Files.newInputStream(file))) { - return BlobDescriptor.fromPipe(fileStream, outputStream); + try (InputStream fileIn = new BufferedInputStream(Files.newInputStream(file))) { + return Digests.computeDigest(fileIn, outputStream); } } + + @Override + public boolean isRetryable() { + return true; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java index d616d32230..282f39fa11 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,16 +34,21 @@ class InputStreamBlob implements Blob { } @Override - public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { + public BlobDescriptor writeTo(OutputStream outStream) throws IOException { // Cannot rewrite. if (isWritten) { throw new IllegalStateException("Cannot rewrite Blob backed by an InputStream"); } - try (InputStream inputStream = this.inputStream) { - return BlobDescriptor.fromPipe(inputStream, outputStream); + try (InputStream inStream = this.inputStream) { + return Digests.computeDigest(inStream, outStream); } finally { isWritten = true; } } + + @Override + public boolean isRetryable() { + return false; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java similarity index 56% rename from jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java index d7b16cfac1..c7de5a9b41 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2019 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,25 +16,27 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.json.JsonTemplate; import java.io.IOException; import java.io.OutputStream; -/** A {@link Blob} that writes with a {@link BlobWriter} function and hashes the bytes. */ -class WriterBlob implements Blob { +/** A {@link Blob} that holds {@link JsonTemplate}. */ +class JsonBlob implements Blob { - private final BlobWriter writer; + private final JsonTemplate template; - WriterBlob(BlobWriter writer) { - this.writer = writer; + JsonBlob(JsonTemplate template) { + this.template = template; } @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(outputStream); - writer.writeTo(countingDigestOutputStream); - countingDigestOutputStream.flush(); - return countingDigestOutputStream.toBlobDescriptor(); + return Digests.computeDigest(template, outputStream); + } + + @Override + public boolean isRetryable() { + return true; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java index b883b6a6ab..cf0285db46 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,13 +16,14 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -/** A {@link Blob} that holds a {@link String}. */ +/** A {@link Blob} that holds a {@link String}. Encodes in UTF-8 when writing in bytes. */ class StringBlob implements Blob { private final String content; @@ -33,9 +34,14 @@ class StringBlob implements Blob { @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - try (InputStream stringInputStream = + try (InputStream stringIn = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { - return BlobDescriptor.fromPipe(stringInputStream, outputStream); + return Digests.computeDigest(stringIn, outputStream); } } + + @Override + public boolean isRetryable() { + return true; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java new file mode 100644 index 0000000000..8384c7e5db --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.blob; + +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.hash.WritableContents; +import java.io.IOException; +import java.io.OutputStream; + +/** A {@link Blob} that holds {@link WritableContents}. */ +class WritableContentsBlob implements Blob { + + private final WritableContents writableContents; + private final boolean retryable; + + WritableContentsBlob(WritableContents writableContents, boolean retryable) { + this.writableContents = writableContents; + this.retryable = retryable; + } + + @Override + public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { + return Digests.computeDigest(writableContents, outputStream); + } + + @Override + public boolean isRetryable() { + return retryable; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java deleted file mode 100644 index 4b63826137..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; -import javax.lang.model.SourceVersion; - -/** Immutable configuration options for the builder process. */ -public class BuildConfiguration { - - public static class Builder { - - // All the parameters below are set to their default values. - @Nullable private ImageReference baseImageReference; - @Nullable private String baseImageCredentialHelperName; - @Nullable private RegistryCredentials knownBaseRegistryCredentials; - @Nullable private ImageReference targetImageReference; - @Nullable private String targetImageCredentialHelperName; - @Nullable private RegistryCredentials knownTargetRegistryCredentials; - @Nullable private String mainClass; - private ImmutableList javaArguments = ImmutableList.of(); - private ImmutableList jvmFlags = ImmutableList.of(); - private ImmutableMap environmentMap = ImmutableMap.of(); - private ImmutableList exposedPorts = ImmutableList.of(); - private Class targetFormat = V22ManifestTemplate.class; - @Nullable private CacheConfiguration applicationLayersCacheConfiguration; - @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; - private boolean allowHttp = false; - @Nullable private LayerConfiguration extraFilesLayerConfiguration; - - private BuildLogger buildLogger; - - private Builder(BuildLogger buildLogger) { - this.buildLogger = buildLogger; - } - - public Builder setBaseImage(@Nullable ImageReference imageReference) { - baseImageReference = imageReference; - return this; - } - - public Builder setTargetImage(@Nullable ImageReference imageReference) { - targetImageReference = imageReference; - return this; - } - - public Builder setBaseImageCredentialHelperName(@Nullable String credentialHelperName) { - baseImageCredentialHelperName = credentialHelperName; - return this; - } - - public Builder setTargetImageCredentialHelperName(@Nullable String credentialHelperName) { - targetImageCredentialHelperName = credentialHelperName; - return this; - } - - public Builder setKnownBaseRegistryCredentials( - @Nullable RegistryCredentials knownRegistryCrendentials) { - knownBaseRegistryCredentials = knownRegistryCrendentials; - return this; - } - - public Builder setKnownTargetRegistryCredentials( - @Nullable RegistryCredentials knownRegistryCrendentials) { - knownTargetRegistryCredentials = knownRegistryCrendentials; - return this; - } - - public Builder setMainClass(@Nullable String mainClass) { - this.mainClass = mainClass; - return this; - } - - public Builder setJavaArguments(@Nullable List javaArguments) { - if (javaArguments != null) { - Preconditions.checkArgument(!javaArguments.contains(null)); - this.javaArguments = ImmutableList.copyOf(javaArguments); - } - return this; - } - - public Builder setJvmFlags(@Nullable List jvmFlags) { - if (jvmFlags != null) { - Preconditions.checkArgument(!jvmFlags.contains(null)); - this.jvmFlags = ImmutableList.copyOf(jvmFlags); - } - return this; - } - - public Builder setEnvironment(@Nullable Map environmentMap) { - if (environmentMap != null) { - Preconditions.checkArgument( - !environmentMap.containsKey(null) && !environmentMap.containsValue(null)); - this.environmentMap = ImmutableMap.copyOf(environmentMap); - } - return this; - } - - public Builder setExposedPorts(@Nullable List exposedPorts) { - if (exposedPorts != null) { - Preconditions.checkArgument(!exposedPorts.contains(null)); - this.exposedPorts = ImmutableList.copyOf(exposedPorts); - } - return this; - } - - public Builder setTargetFormat(Class targetFormat) { - this.targetFormat = targetFormat; - return this; - } - - /** - * Sets the location of the cache for storing application layers. - * - * @param applicationLayersCacheConfiguration the application layers {@link CacheConfiguration} - * @return this - */ - public Builder setApplicationLayersCacheConfiguration( - @Nullable CacheConfiguration applicationLayersCacheConfiguration) { - this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; - return this; - } - - /** - * Sets the location of the cache for storing base image layers. - * - * @param baseImageLayersCacheConfiguration the base image layers {@link CacheConfiguration} - * @return this - */ - public Builder setBaseImageLayersCacheConfiguration( - @Nullable CacheConfiguration baseImageLayersCacheConfiguration) { - this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; - return this; - } - - /** - * Sets whether or not to allow communication over HTTP (as opposed to HTTPS). - * - * @param allowHttp if {@code true}, insecure connections will be allowed - * @return this - */ - public Builder setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; - return this; - } - - /** - * Sets the {@link LayerConfiguration} for an extra layer. - * - * @param extraFilesLayerConfiguration the layer configuration for the extra layer - * @return this - */ - public Builder setExtraFilesLayerConfiguration( - @Nullable LayerConfiguration extraFilesLayerConfiguration) { - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; - return this; - } - - /** @return the corresponding build configuration */ - public BuildConfiguration build() { - // Validates the parameters. - List errorMessages = new ArrayList<>(); - if (baseImageReference == null) { - errorMessages.add("base image is required but not set"); - } - if (targetImageReference == null) { - errorMessages.add("target image is required but not set"); - } - if (mainClass == null) { - errorMessages.add("main class is required but not set"); - } - - switch (errorMessages.size()) { - case 0: // No errors - if (baseImageReference == null || targetImageReference == null || mainClass == null) { - throw new IllegalStateException("Required fields should not be null"); - } - if (baseImageReference.usesDefaultTag()) { - buildLogger.warn( - "Base image '" - + baseImageReference - + "' does not use a specific image digest - build may not be reproducible"); - } - return new BuildConfiguration( - buildLogger, - baseImageReference, - baseImageCredentialHelperName, - knownBaseRegistryCredentials, - targetImageReference, - targetImageCredentialHelperName, - knownTargetRegistryCredentials, - mainClass, - javaArguments, - jvmFlags, - environmentMap, - exposedPorts, - targetFormat, - applicationLayersCacheConfiguration, - baseImageLayersCacheConfiguration, - allowHttp, - extraFilesLayerConfiguration); - - case 1: - throw new IllegalStateException(errorMessages.get(0)); - - case 2: - throw new IllegalStateException(errorMessages.get(0) + " and " + errorMessages.get(1)); - - default: - // Appends the descriptions in correct grammar. - StringBuilder errorMessage = new StringBuilder(errorMessages.get(0)); - for (int errorMessageIndex = 1; - errorMessageIndex < errorMessages.size(); - errorMessageIndex++) { - if (errorMessageIndex == errorMessages.size() - 1) { - errorMessage.append(", and "); - } else { - errorMessage.append(", "); - } - errorMessage.append(errorMessages.get(errorMessageIndex)); - } - throw new IllegalStateException(errorMessage.toString()); - } - } - } - - /** - * @param className the class name to check - * @return {@code true} if {@code className} is a valid Java class name; {@code false} otherwise - */ - public static boolean isValidJavaClass(String className) { - for (String part : Splitter.on('.').split(className)) { - if (!SourceVersion.isIdentifier(part)) { - return false; - } - } - return true; - } - - public static Builder builder(BuildLogger buildLogger) { - return new Builder(buildLogger); - } - - private final BuildLogger buildLogger; - private final ImageReference baseImageReference; - @Nullable private final String baseImageCredentialHelperName; - @Nullable private final RegistryCredentials knownBaseRegistryCredentials; - private final ImageReference targetImageReference; - @Nullable private final String targetImageCredentialHelperName; - @Nullable private final RegistryCredentials knownTargetRegistryCredentials; - private final String mainClass; - private final ImmutableList javaArguments; - private final ImmutableList jvmFlags; - private final ImmutableMap environmentMap; - private final ImmutableList exposedPorts; - private final Class targetFormat; - @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; - @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; - private final boolean allowHttp; - @Nullable private final LayerConfiguration extraFilesLayerConfiguration; - - /** Instantiate with {@link Builder#build}. */ - private BuildConfiguration( - BuildLogger buildLogger, - ImageReference baseImageReference, - @Nullable String baseImageCredentialHelperName, - @Nullable RegistryCredentials knownBaseRegistryCredentials, - ImageReference targetImageReference, - @Nullable String targetImageCredentialHelperName, - @Nullable RegistryCredentials knownTargetRegistryCredentials, - String mainClass, - ImmutableList javaArguments, - ImmutableList jvmFlags, - ImmutableMap environmentMap, - ImmutableList exposedPorts, - Class targetFormat, - @Nullable CacheConfiguration applicationLayersCacheConfiguration, - @Nullable CacheConfiguration baseImageLayersCacheConfiguration, - boolean allowHttp, - @Nullable LayerConfiguration extraFilesLayerConfiguration) { - this.buildLogger = buildLogger; - this.baseImageReference = baseImageReference; - this.baseImageCredentialHelperName = baseImageCredentialHelperName; - this.knownBaseRegistryCredentials = knownBaseRegistryCredentials; - this.targetImageReference = targetImageReference; - this.targetImageCredentialHelperName = targetImageCredentialHelperName; - this.knownTargetRegistryCredentials = knownTargetRegistryCredentials; - this.mainClass = mainClass; - this.javaArguments = javaArguments; - this.jvmFlags = jvmFlags; - this.environmentMap = environmentMap; - this.exposedPorts = exposedPorts; - this.targetFormat = targetFormat; - this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; - this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; - this.allowHttp = allowHttp; - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; - } - - public BuildLogger getBuildLogger() { - return buildLogger; - } - - public ImageReference getBaseImageReference() { - return baseImageReference; - } - - public String getBaseImageRegistry() { - return baseImageReference.getRegistry(); - } - - public String getBaseImageRepository() { - return baseImageReference.getRepository(); - } - - public String getBaseImageTag() { - return baseImageReference.getTag(); - } - - @Nullable - public String getBaseImageCredentialHelperName() { - return baseImageCredentialHelperName; - } - - @Nullable - public RegistryCredentials getKnownBaseRegistryCredentials() { - return knownBaseRegistryCredentials; - } - - public ImageReference getTargetImageReference() { - return targetImageReference; - } - - public String getTargetImageRegistry() { - return targetImageReference.getRegistry(); - } - - public String getTargetImageRepository() { - return targetImageReference.getRepository(); - } - - public String getTargetImageTag() { - return targetImageReference.getTag(); - } - - @Nullable - public String getTargetImageCredentialHelperName() { - return targetImageCredentialHelperName; - } - - @Nullable - public RegistryCredentials getKnownTargetRegistryCredentials() { - return knownTargetRegistryCredentials; - } - - public String getMainClass() { - return mainClass; - } - - public ImmutableList getJavaArguments() { - return javaArguments; - } - - public ImmutableList getJvmFlags() { - return jvmFlags; - } - - public ImmutableMap getEnvironment() { - return environmentMap; - } - - public ImmutableList getExposedPorts() { - return exposedPorts; - } - - public Class getTargetFormat() { - return targetFormat; - } - - /** - * Gets the location of the cache for storing application layers. - * - * @return the application layers {@link CacheConfiguration}, or {@code null} if not set - */ - @Nullable - public CacheConfiguration getApplicationLayersCacheConfiguration() { - return applicationLayersCacheConfiguration; - } - - /** - * Gets the location of the cache for storing base image layers. - * - * @return the base image layers {@link CacheConfiguration}, or {@code null} if not set - */ - @Nullable - public CacheConfiguration getBaseImageLayersCacheConfiguration() { - return baseImageLayersCacheConfiguration; - } - - /** - * Gets whether or not to allow communication over HTTP (as opposed to HTTPS). - * - * @return {@code true} if insecure connections will be allowed; {@code false} otherwise - */ - public boolean getAllowHttp() { - return allowHttp; - } - - /** - * Gets the {@link LayerConfiguration} for an extra layer. - * - * @return the layer configuration - */ - @Nullable - public LayerConfiguration getExtraFilesLayerConfiguration() { - return extraFilesLayerConfiguration; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java deleted file mode 100644 index 55303dd848..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.builder.steps.StepsRunner; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.Caches; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -/** Steps for building an image. */ -public class BuildSteps { - - /** Accepts {@link StepsRunner} by running the appropriate steps. */ - @FunctionalInterface - private interface StepsRunnerConsumer { - - void accept(StepsRunner stepsRunner) throws ExecutionException, InterruptedException; - } - - private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; - private static final String STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY = - "Containerizing application to %s..."; - // String parameter (target image reference) in cyan. - private static final String SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY = - "Built and pushed image as \u001B[36m%s\u001B[0m"; - - private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; - private static final String STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON = - "Containerizing application to Docker daemon as %s..."; - // String parameter (target image reference) in cyan. - private static final String SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON = - "Built image to Docker daemon as \u001B[36m%s\u001B[0m"; - - /** - * All the steps to build an image to a Docker registry. - * - * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image - * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache - * @return a new {@link BuildSteps} for building to a registry - */ - public static BuildSteps forBuildToDockerRegistry( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Caches.Initializer cachesInitializer) { - return new BuildSteps( - DESCRIPTION_FOR_DOCKER_REGISTRY, - buildConfiguration, - sourceFilesConfiguration, - cachesInitializer, - String.format( - STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageReference()), - String.format( - SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageReference()), - stepsRunner -> - stepsRunner - .runRetrieveTargetRegistryCredentialsStep() - .runAuthenticatePushStep() - .runPullBaseImageStep() - .runPullAndCacheBaseImageLayersStep() - .runPushBaseImageLayersStep() - .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) - .runPushContainerConfigurationStep() - .runPushApplicationLayersStep() - .runFinalizingPushStep() - .runPushImageStep() - .waitOnPushImageStep()); - } - - /** - * All the steps to build to Docker daemon - * - * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image - * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache - * @return a new {@link BuildSteps} for building to a Docker daemon - */ - public static BuildSteps forBuildToDockerDaemon( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Caches.Initializer cachesInitializer) { - return new BuildSteps( - DESCRIPTION_FOR_DOCKER_DAEMON, - buildConfiguration, - sourceFilesConfiguration, - cachesInitializer, - String.format( - STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, buildConfiguration.getTargetImageReference()), - String.format( - SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, buildConfiguration.getTargetImageReference()), - stepsRunner -> - stepsRunner - .runPullBaseImageStep() - .runPullAndCacheBaseImageLayersStep() - .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) - .runFinalizingBuildStep() - .runLoadDockerStep() - .waitOnLoadDockerStep()); - } - - /** Creates the container entrypoint for a given configuration. */ - private static ImmutableList getEntrypoint( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) { - return EntrypointBuilder.makeEntrypoint( - sourceFilesConfiguration, - buildConfiguration.getJvmFlags(), - buildConfiguration.getMainClass()); - } - - private final String description; - private final BuildConfiguration buildConfiguration; - private final SourceFilesConfiguration sourceFilesConfiguration; - private final Caches.Initializer cachesInitializer; - private final String startupMessage; - private final String successMessage; - private final StepsRunnerConsumer stepsRunnerConsumer; - - /** - * @param description a description of what the steps do - * @param startupMessage shown when the steps start running - * @param successMessage shown when the steps finish successfully - * @param stepsRunnerConsumer accepts a {@link StepsRunner} by running the necessary steps - */ - private BuildSteps( - String description, - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Caches.Initializer cachesInitializer, - String startupMessage, - String successMessage, - StepsRunnerConsumer stepsRunnerConsumer) { - this.description = description; - this.buildConfiguration = buildConfiguration; - this.sourceFilesConfiguration = sourceFilesConfiguration; - this.cachesInitializer = cachesInitializer; - this.startupMessage = startupMessage; - this.successMessage = successMessage; - this.stepsRunnerConsumer = stepsRunnerConsumer; - } - - public BuildConfiguration getBuildConfiguration() { - return buildConfiguration; - } - - public SourceFilesConfiguration getSourceFilesConfiguration() { - return sourceFilesConfiguration; - } - - public String getStartupMessage() { - return startupMessage; - } - - public String getSuccessMessage() { - return successMessage; - } - - public void run() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - buildConfiguration.getBuildLogger().lifecycle(""); - - try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), description)) { - try (Caches caches = cachesInitializer.init()) { - Cache baseImageLayersCache = caches.getBaseCache(); - Cache applicationLayersCache = caches.getApplicationCache(); - - StepsRunner stepsRunner = - new StepsRunner( - buildConfiguration, - sourceFilesConfiguration, - baseImageLayersCache, - applicationLayersCache); - stepsRunnerConsumer.accept(stepsRunner); - - // Writes the cached layers to the cache metadata. - baseImageLayersCache.addCachedLayersToMetadata(stepsRunner.getCachedBaseImageLayers()); - applicationLayersCache.addCachedLayersWithMetadataToMetadata( - stepsRunner.getCachedApplicationLayers()); - } - } - - buildConfiguration.getBuildLogger().lifecycle(""); - buildConfiguration - .getBuildLogger() - .lifecycle( - "Container entrypoint set to " - + getEntrypoint(buildConfiguration, sourceFilesConfiguration)); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java deleted file mode 100644 index 2c1c06ffea..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import java.util.List; - -/** Builds an image entrypoint for the Java application. */ -public class EntrypointBuilder { - - /** - * Builds the container entrypoint. - * - *

    The entrypoint is {@code java [jvm flags] -cp [classpaths] [main class]}. - * - * @param sourceFilesConfiguration configuration defining where files are copied onto the image - * @param jvmFlags the JVM flags to start the container with - * @param mainClass the name of the main class to run on startup - * @return a list of the entrypoint tokens - */ - public static ImmutableList makeEntrypoint( - SourceFilesConfiguration sourceFilesConfiguration, List jvmFlags, String mainClass) { - ImmutableList classPaths = - ImmutableList.of( - sourceFilesConfiguration.getDependenciesPathOnImage() + "*", - sourceFilesConfiguration.getResourcesPathOnImage(), - sourceFilesConfiguration.getClassesPathOnImage()); - - String classPathsString = String.join(":", classPaths); - - ImmutableList.Builder entrypointBuilder = - ImmutableList.builderWithExpectedSize(4 + jvmFlags.size()); - entrypointBuilder.add("java"); - entrypointBuilder.addAll(jvmFlags); - entrypointBuilder.add("-cp"); - entrypointBuilder.add(classPathsString); - entrypointBuilder.add(mainClass); - return entrypointBuilder.build(); - } - - private EntrypointBuilder() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java new file mode 100644 index 0000000000..f1aa4837f9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; +import com.google.common.base.Preconditions; +import java.io.Closeable; + +/** + * Dispatches {@link ProgressEvent}s associated with a managed {@link Allocation}. Keeps track of + * the allocation units that are remaining so that it can emit the remaining progress units upon + * {@link #close}. + * + *

    This class is not thread-safe. Only use a single instance per thread and create child + * instances with {@link #newChildProducer}. + */ +public class ProgressEventDispatcher implements Closeable { + + /** + * Creates a new {@link ProgressEventDispatcher} based off an existing {@link + * ProgressEventDispatcher}. {@link #create} should only be called once. + */ + @FunctionalInterface + public interface Factory { + + /** + * Creates the {@link ProgressEventDispatcher} with an associated {@link Allocation}. + * + * @param description user-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return the new {@link ProgressEventDispatcher} + */ + ProgressEventDispatcher create(String description, long allocationUnits); + } + + /** + * Creates a new {@link ProgressEventDispatcher} with a root {@link Allocation}. + * + * @param eventHandlers the {@link EventHandlers} + * @param description user-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return a new {@link ProgressEventDispatcher} + */ + public static ProgressEventDispatcher newRoot( + EventHandlers eventHandlers, String description, long allocationUnits) { + return newProgressEventDispatcher( + eventHandlers, Allocation.newRoot(description, allocationUnits)); + } + + /** + * Creates a new {@link ProgressEventDispatcher} and dispatches a new {@link ProgressEvent} with + * progress 0 for {@code allocation}. + * + * @param eventHandlers the {@link EventHandlers} + * @param allocation the {@link Allocation} to manage + * @return a new {@link ProgressEventDispatcher} + */ + private static ProgressEventDispatcher newProgressEventDispatcher( + EventHandlers eventHandlers, Allocation allocation) { + ProgressEventDispatcher progressEventDispatcher = + new ProgressEventDispatcher(eventHandlers, allocation); + progressEventDispatcher.dispatchProgress(0); + return progressEventDispatcher; + } + + private final EventHandlers eventHandlers; + private final Allocation allocation; + + private long remainingAllocationUnits; + private boolean closed = false; + + private ProgressEventDispatcher(EventHandlers eventHandlers, Allocation allocation) { + this.eventHandlers = eventHandlers; + this.allocation = allocation; + + remainingAllocationUnits = allocation.getAllocationUnits(); + } + + /** + * Creates a new {@link Factory} for a {@link ProgressEventDispatcher} that manages a child {@link + * Allocation}. Since each child {@link Allocation} accounts for 1 allocation unit of its parent, + * this method decrements the {@link #remainingAllocationUnits} by {@code 1}. + * + * @return a new {@link Factory} + */ + public Factory newChildProducer() { + decrementRemainingAllocationUnits(1); + + return new Factory() { + + private boolean used = false; + + @Override + public ProgressEventDispatcher create(String description, long allocationUnits) { + Preconditions.checkState(!used); + used = true; + return newProgressEventDispatcher( + eventHandlers, allocation.newChild(description, allocationUnits)); + } + }; + } + + /** Emits the remaining allocation units as progress units in a {@link ProgressEvent}. */ + @Override + public void close() { + if (remainingAllocationUnits > 0) { + dispatchProgress(remainingAllocationUnits); + } + closed = true; + } + + /** + * Dispatches a {@link ProgressEvent} representing {@code progressUnits} of progress on the + * managed {@link #allocation}. + * + * @param progressUnits units of progress + */ + public void dispatchProgress(long progressUnits) { + long unitsDecremented = decrementRemainingAllocationUnits(progressUnits); + eventHandlers.dispatch(new ProgressEvent(allocation, unitsDecremented)); + } + + /** + * Decrements remaining allocation units by {@code units} but no more than the remaining + * allocation units (which may be 0). Returns the actual units decremented, which never exceeds + * {@code units}. + * + * @param units units to decrement + * @return units actually decremented + */ + private long decrementRemainingAllocationUnits(long units) { + Preconditions.checkState(!closed); + + if (remainingAllocationUnits >= units) { + remainingAllocationUnits -= units; + return units; + } + + long actualDecrement = remainingAllocationUnits; + remainingAllocationUnits = 0; + return actualDecrement; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java deleted file mode 100644 index 15de722be6..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; - -/** - * Immutable configuration that defines where the source files for each of the application layers - * are. - */ -public interface SourceFilesConfiguration { - - String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; - String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; - String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; - - /** - * @return the source files for the dependencies layer. These files should be in a deterministic - * order. - */ - ImmutableList getDependenciesFiles(); - - /** - * @return the source files for the resources layer. These files should be in a deterministic - * order. - */ - ImmutableList getResourcesFiles(); - - /** - * @return the source files for the classes layer. These files should be in a deterministic order. - */ - ImmutableList getClassesFiles(); - - /** - * @return the Unix-style path where the dependencies source files are placed in the container - * filesystem. Must end with slash. - */ - String getDependenciesPathOnImage(); - - /** - * @return the Unix-style path where the resources source files are placed in the container - * filesystem. Must end with slash. - */ - String getResourcesPathOnImage(); - - /** - * @return the Unix-style path where the classes source files are placed in the container - * filesystem. Must end with slash. - */ - String getClassesPathOnImage(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java new file mode 100644 index 0000000000..d3443fdd43 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.events.TimerEvent; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Times code execution intervals. Call {@link #lap} at the end of each interval. */ +class Timer implements TimerEvent.Timer { + + private final Clock clock; + @Nullable private final Timer parentTimer; + + private final Instant startTime; + private Instant lapStartTime; + + Timer(Clock clock, @Nullable Timer parentTimer) { + this.clock = clock; + this.parentTimer = parentTimer; + + startTime = clock.instant(); + lapStartTime = startTime; + } + + @Override + public Optional getParent() { + return Optional.ofNullable(parentTimer); + } + + /** + * Captures the time since last lap or creation, and resets the start time. + * + * @return the duration of the last lap, or since creation + */ + Duration lap() { + Instant now = clock.instant(); + Duration duration = Duration.between(lapStartTime, now); + lapStartTime = now; + return duration; + } + + /** + * Gets the total elapsed time since creation. + * + * @return the total elapsed time + */ + Duration getElapsedTime() { + return Duration.between(startTime, clock.instant()); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java new file mode 100644 index 0000000000..3c136327a8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import com.google.common.annotations.VisibleForTesting; +import java.io.Closeable; +import java.time.Clock; +import java.time.Duration; +import javax.annotation.Nullable; + +/** Handles {@link Timer}s to dispatch {@link TimerEvent}s. */ +public class TimerEventDispatcher implements Closeable { + + private static final Clock DEFAULT_CLOCK = Clock.systemUTC(); + + private final EventHandlers eventHandlers; + private final String description; + + private final Clock clock; + private final Timer timer; + + /** + * Creates a new {@link TimerEventDispatcher}. + * + * @param eventHandlers the {@link EventHandlers} used to dispatch the {@link TimerEvent}s + * @param description the default description for the {@link TimerEvent}s + */ + public TimerEventDispatcher(EventHandlers eventHandlers, String description) { + this(eventHandlers, description, DEFAULT_CLOCK, null); + } + + @VisibleForTesting + TimerEventDispatcher( + EventHandlers eventHandlers, String description, Clock clock, @Nullable Timer parentTimer) { + this.eventHandlers = eventHandlers; + this.description = description; + this.clock = clock; + this.timer = new Timer(clock, parentTimer); + + dispatchTimerEvent(State.START, Duration.ZERO, description); + } + + /** + * Creates a new {@link TimerEventDispatcher} with its parent timer as this. + * + * @param description a new description + * @return the new {@link TimerEventDispatcher} + */ + public TimerEventDispatcher subTimer(String description) { + return new TimerEventDispatcher(eventHandlers, description, clock, timer); + } + + /** + * Captures the time since last lap or creation and dispatches an {@link State#LAP} {@link + * TimerEvent}. + * + * @see #lap(String) + */ + public void lap() { + dispatchTimerEvent(State.LAP, timer.lap(), description); + } + + /** + * Captures the time since last lap or creation and dispatches an {@link State#LAP} {@link + * TimerEvent}. + * + * @param newDescription the description to use instead of the {@link TimerEventDispatcher}'s + * description + */ + public void lap(String newDescription) { + dispatchTimerEvent(State.LAP, timer.lap(), newDescription); + } + + /** Laps and dispatches a {@link State#FINISHED} {@link TimerEvent} upon close. */ + @Override + public void close() { + dispatchTimerEvent(State.FINISHED, timer.lap(), description); + } + + private void dispatchTimerEvent(State state, Duration duration, String eventDescription) { + eventHandlers.dispatch( + new TimerEvent(state, timer, duration, timer.getElapsedTime(), eventDescription)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 56d8cb8ec1..85ad84f336 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,77 +16,60 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticator; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nullable; -// TODO: This is probably not necessary anymore either. /** * Authenticates push to a target registry using Docker Token Authentication. * * @see https://docs.docker.com/registry/spec/auth/token/ */ -class AuthenticatePushStep implements AsyncStep, Callable { +class AuthenticatePushStep implements Callable { - private static final String DESCRIPTION = "Authenticating with push to %s"; + @SuppressWarnings("InlineFormatString") + private static final String DESCRIPTION = "Authenticating push to %s"; - private final BuildConfiguration buildConfiguration; - private final RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; - - private final ListenableFuture listenableFuture; + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; AuthenticatePushStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep) { - this.buildConfiguration = buildConfiguration; - this.retrieveTargetRegistryCredentialsStep = retrieveTargetRegistryCredentialsStep; - - listenableFuture = - Futures.whenAllSucceed(retrieveTargetRegistryCredentialsStep.getFuture()) - .call(this, listeningExecutorService); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; } @Override - @Nullable - public Authorization call() - throws ExecutionException, RegistryAuthenticationFailedException, IOException, - RegistryException { - try (Timer ignored = - new Timer( - buildConfiguration.getBuildLogger(), - String.format(DESCRIPTION, buildConfiguration.getTargetImageRegistry()))) { - Authorization registryCredentials = - NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); + public RegistryClient call() throws CredentialRetrievalException, IOException, RegistryException { + String registry = buildContext.getTargetImageConfiguration().getImageRegistry(); + try (ProgressEventDispatcher progressDispatcher = + progressEventDispatcherFactory.create("authenticating push to " + registry, 2); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher( + buildContext.getEventHandlers(), String.format(DESCRIPTION, registry))) { + Credential credential = + RegistryCredentialRetriever.getTargetImageCredential(buildContext).orElse(null); + progressDispatcher.dispatchProgress(1); - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .initialize(); - if (registryAuthenticator == null) { - return registryCredentials; + RegistryClient registryClient = + buildContext + .newTargetImageRegistryClientFactory() + .setCredential(credential) + .newRegistryClient(); + if (!registryClient.doPushBearerAuth()) { + // server returned "WWW-Authenticate: Basic ..." (e.g., local Docker registry) + if (credential != null && !credential.isOAuth2RefreshToken()) { + registryClient.configureBasicAuth(); + } } - return registryAuthenticator.setAuthorization(registryCredentials).authenticatePush(); + return registryClient; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index bff71a426b..358592477e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,145 +16,100 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.CacheReader; -import com.google.cloud.tools.jib.cache.CacheWriter; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; +import java.util.List; +import java.util.Optional; import java.util.concurrent.Callable; /** Builds and caches application layers. */ -class BuildAndCacheApplicationLayerStep - implements AsyncStep, Callable { +class BuildAndCacheApplicationLayerStep implements Callable { - private static final String DESCRIPTION = "Building application layers"; + @SuppressWarnings("InlineFormatString") + private static final String DESCRIPTION = "Building %s layer"; /** * Makes a list of {@link BuildAndCacheApplicationLayerStep} for dependencies, resources, and * classes layers. Optionally adds an extra layer if configured to do so. */ static ImmutableList makeList( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Cache cache) { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - ImmutableList.Builder buildLayerStepsBuilder = - ImmutableList.builder() - .add( + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + List layerConfigurations = buildContext.getLayerConfigurations(); + + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "launching application layer builders", layerConfigurations.size()); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildContext.getEventHandlers(), "Preparing application layer builders")) { + return layerConfigurations.stream() + // Skips the layer if empty. + .filter(layerConfiguration -> !layerConfiguration.getEntries().isEmpty()) + .map( + layerConfiguration -> new BuildAndCacheApplicationLayerStep( - "dependencies", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getDependenciesFiles(), - sourceFilesConfiguration.getDependenciesPathOnImage()) - .build(), - cache)) - .add( - new BuildAndCacheApplicationLayerStep( - "resources", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getResourcesFiles(), - sourceFilesConfiguration.getResourcesPathOnImage()) - .build(), - cache)) - .add( - new BuildAndCacheApplicationLayerStep( - "classes", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getClassesFiles(), - sourceFilesConfiguration.getClassesPathOnImage()) - .build(), - cache)); - - // Adds the extra layer to be built, if configured. - if (buildConfiguration.getExtraFilesLayerConfiguration() != null) { - buildLayerStepsBuilder.add( - new BuildAndCacheApplicationLayerStep( - "extra files", - listeningExecutorService, - buildConfiguration, - buildConfiguration.getExtraFilesLayerConfiguration(), - cache)); - } - - return buildLayerStepsBuilder.build(); + buildContext, + progressEventDispatcher.newChildProducer(), + layerConfiguration.getName(), + layerConfiguration)) + .collect(ImmutableList.toImmutableList()); } } - private final String layerType; - private final BuildConfiguration buildConfiguration; - private final LayerConfiguration layerConfiguration; - private final Cache cache; + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final ListenableFuture listenableFuture; + private final String layerName; + private final FileEntriesLayer layerConfiguration; private BuildAndCacheApplicationLayerStep( - String layerType, - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - LayerConfiguration layerConfiguration, - Cache cache) { - this.layerType = layerType; - this.buildConfiguration = buildConfiguration; + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + String layerName, + FileEntriesLayer layerConfiguration) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.layerName = layerName; this.layerConfiguration = layerConfiguration; - this.cache = cache; - - listenableFuture = listeningExecutorService.submit(this); } @Override - public ListenableFuture getFuture() { - return listenableFuture; - } + public PreparedLayer call() throws IOException, CacheCorruptedException { + String description = String.format(DESCRIPTION, layerName); - @Override - public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorruptedException { - String description = "Building " + layerType + " layer"; + EventHandlers eventHandlers = buildContext.getEventHandlers(); + eventHandlers.dispatch(LogEvent.progress(description + "...")); - buildConfiguration.getBuildLogger().lifecycle(description + "..."); + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("building " + layerName + " layer", 1); + TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { + Cache cache = buildContext.getApplicationLayersCache(); - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { + ImmutableList layerEntries = ImmutableList.copyOf(layerConfiguration.getEntries()); // Don't build the layer if it exists already. - CachedLayerWithMetadata cachedLayer = - new CacheReader(cache) - .getUpToDateLayerByLayerEntries(layerConfiguration.getLayerEntries()); - if (cachedLayer != null) { - return cachedLayer; - } - - ReproducibleLayerBuilder reproducibleLayerBuilder = new ReproducibleLayerBuilder(); - for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - reproducibleLayerBuilder.addFiles( - layerEntry.getSourceFiles(), layerEntry.getExtractionPath()); + Optional optionalCachedLayer = cache.retrieve(layerEntries); + if (optionalCachedLayer.isPresent()) { + return new PreparedLayer.Builder(optionalCachedLayer.get()).setName(layerName).build(); } - cachedLayer = new CacheWriter(cache).writeLayer(reproducibleLayerBuilder); + Blob layerBlob = new ReproducibleLayerBuilder(layerEntries).build(); + CachedLayer cachedLayer = cache.writeUncompressedLayer(layerBlob, layerEntries); - buildConfiguration - .getBuildLogger() - .debug(description + " built " + cachedLayer.getBlobDescriptor().getDigest()); + eventHandlers.dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); - return cachedLayer; + return new PreparedLayer.Builder(cachedLayer).setName(layerName).build(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index ddddd6181a..00f21c5a8d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,96 +16,205 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.collect.UnmodifiableIterator; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; /** Builds a model {@link Image}. */ -class BuildImageStep - implements AsyncStep>>, Callable>> { +class BuildImageStep implements Callable { private static final String DESCRIPTION = "Building container configuration"; - private final BuildConfiguration buildConfiguration; - private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - private final ImmutableList buildAndCacheApplicationLayerSteps; - private final ImmutableList entrypoint; + @VisibleForTesting + static String truncateLongClasspath(ImmutableList imageEntrypoint) { + List truncated = new ArrayList<>(); + UnmodifiableIterator iterator = imageEntrypoint.iterator(); + while (iterator.hasNext()) { + String element = iterator.next(); + truncated.add(element); - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture>> listenableFuture; + if (element.equals("-cp") || element.equals("-classpath")) { + String classpath = iterator.next(); + if (classpath.length() > 200) { + truncated.add(classpath.substring(0, 200) + "<... classpath truncated ...>"); + } else { + truncated.add(classpath); + } + } + } + return truncated.toString(); + } + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final Image baseImage; + private final List baseImageLayers; + private final List applicationLayers; BuildImageStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps, - ImmutableList entrypoint) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; - this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - this.entrypoint = entrypoint; - - listenableFuture = - Futures.whenAllSucceed(pullAndCacheBaseImageLayersStep.getFuture()) - .call(this, listeningExecutorService); + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Image baseImage, + List baseImageLayers, + List applicationLayers) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.baseImage = baseImage; + this.baseImageLayers = baseImageLayers; + this.applicationLayers = applicationLayers; } @Override - public ListenableFuture>> getFuture() { - return listenableFuture; - } + public Image call() throws LayerPropertyNotFoundException { + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("building image format", 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher(buildContext.getEventHandlers(), DESCRIPTION)) { + // Constructs the image. + Image.Builder imageBuilder = Image.builder(buildContext.getTargetFormat()); - @Override - public AsyncStep> call() throws ExecutionException { - List> dependencies = new ArrayList<>(); + // Base image layers + baseImageLayers.forEach(imageBuilder::addLayer); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - dependencies.add(pullAndCacheBaseImageLayerStep.getFuture()); - } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - dependencies.add(buildAndCacheApplicationLayerStep.getFuture()); - } - ListenableFuture> future = - Futures.whenAllSucceed(dependencies) - .call(this::afterCachedLayersSteps, listeningExecutorService); - return () -> future; - } + // Passthrough config and count non-empty history entries + int nonEmptyLayerCount = 0; + for (HistoryEntry historyObject : baseImage.getHistory()) { + imageBuilder.addHistory(historyObject); + if (!historyObject.hasCorrespondingLayer()) { + nonEmptyLayerCount++; + } + } + imageBuilder + .setArchitecture(baseImage.getArchitecture()) + .setOs(baseImage.getOs()) + .addEnvironment(baseImage.getEnvironment()) + .addLabels(baseImage.getLabels()) + .setHealthCheck(baseImage.getHealthCheck()) + .addExposedPorts(baseImage.getExposedPorts()) + .addVolumes(baseImage.getVolumes()) + .setUser(baseImage.getUser()) + .setWorkingDirectory(baseImage.getWorkingDirectory()); - private Image afterCachedLayersSteps() - throws ExecutionException, LayerPropertyNotFoundException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - // Constructs the image. - Image.Builder imageBuilder = Image.builder(); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - imageBuilder.addLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); + // Add history elements for non-empty layers that don't have one yet + Instant layerCreationTime = containerConfiguration.getCreationTime(); + for (int count = 0; count < baseImageLayers.size() - nonEmptyLayerCount; count++) { + imageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(layerCreationTime) + .setComment("auto-generated by Jib") + .build()); } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); + + // Add built layers/configuration + for (PreparedLayer applicationLayer : applicationLayers) { + imageBuilder + .addLayer(applicationLayer) + .addHistory( + HistoryEntry.builder() + .setCreationTimestamp(layerCreationTime) + .setAuthor("Jib") + .setCreatedBy(buildContext.getToolName() + ":" + buildContext.getToolVersion()) + .setComment(applicationLayer.getName()) + .build()); + } + + imageBuilder + .addEnvironment(containerConfiguration.getEnvironmentMap()) + .setCreated(containerConfiguration.getCreationTime()) + .setEntrypoint(computeEntrypoint(baseImage, containerConfiguration)) + .setProgramArguments(computeProgramArguments(baseImage, containerConfiguration)) + .addExposedPorts(containerConfiguration.getExposedPorts()) + .addVolumes(containerConfiguration.getVolumes()) + .addLabels(containerConfiguration.getLabels()); + if (containerConfiguration.getUser() != null) { + imageBuilder.setUser(containerConfiguration.getUser()); + } + if (containerConfiguration.getWorkingDirectory() != null) { + imageBuilder.setWorkingDirectory(containerConfiguration.getWorkingDirectory().toString()); } - imageBuilder.setEnvironment(buildConfiguration.getEnvironment()); - imageBuilder.setEntrypoint(entrypoint); - imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments()); - imageBuilder.setExposedPorts(buildConfiguration.getExposedPorts()); // Gets the container configuration content descriptor. return imageBuilder.build(); } } + + /** + * Computes the image entrypoint. If {@link ContainerConfiguration#getEntrypoint()} is null, the + * entrypoint is inherited from the base image. Otherwise {@link + * ContainerConfiguration#getEntrypoint()} is returned. + * + * @param baseImage the base image + * @param containerConfiguration the container configuration + * @return the container entrypoint + */ + @Nullable + private ImmutableList computeEntrypoint( + Image baseImage, ContainerConfiguration containerConfiguration) { + boolean shouldInherit = + baseImage.getEntrypoint() != null && containerConfiguration.getEntrypoint() == null; + + ImmutableList entrypointToUse = + shouldInherit ? baseImage.getEntrypoint() : containerConfiguration.getEntrypoint(); + + if (entrypointToUse != null) { + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle("")); + if (shouldInherit) { + String message = + "Container entrypoint set to " + entrypointToUse + " (inherited from base image)"; + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle(message)); + } else { + String message = "Container entrypoint set to " + truncateLongClasspath(entrypointToUse); + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle(message)); + } + } + + return entrypointToUse; + } + + /** + * Computes the image program arguments. If {@link ContainerConfiguration#getEntrypoint()} and + * {@link ContainerConfiguration#getProgramArguments()} are null, the program arguments are + * inherited from the base image. Otherwise {@link ContainerConfiguration#getProgramArguments()} + * is returned. + * + * @param baseImage the base image + * @param containerConfiguration the container configuration + * @return the container program arguments + */ + @Nullable + private ImmutableList computeProgramArguments( + Image baseImage, ContainerConfiguration containerConfiguration) { + boolean shouldInherit = + baseImage.getProgramArguments() != null + // Inherit CMD only when inheriting ENTRYPOINT. + && containerConfiguration.getEntrypoint() == null + && containerConfiguration.getProgramArguments() == null; + + ImmutableList programArgumentsToUse = + shouldInherit + ? baseImage.getProgramArguments() + : containerConfiguration.getProgramArguments(); + + if (programArgumentsToUse != null) { + String logSuffix = shouldInherit ? " (inherited from base image)" : ""; + String message = "Container program arguments set to " + programArgumentsToUse + logSuffix; + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle(message)); + } + + return programArgumentsToUse; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStep.java new file mode 100644 index 0000000000..3ce08c74fc --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStep.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.api.client.util.Preconditions; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.image.json.ManifestListGenerator; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; + +/** Builds a manifest list or a single manifest. */ +class BuildManifestListOrSingleManifestStep implements Callable { + + private static final String DESCRIPTION = "Building a manifest list or a single manifest"; + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final List builtImages; + + BuildManifestListOrSingleManifestStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + List builtImages) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.builtImages = builtImages; + } + + @Override + public ManifestTemplate call() throws IOException { + Preconditions.checkState(!builtImages.isEmpty(), "no images given"); + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION); + ProgressEventDispatcher ignored2 = + progressEventDispatcherFactory.create( + "building a manifest list or a single manifest", 1)) { + + if (builtImages.size() == 1) { + eventHandlers.dispatch(LogEvent.info("Building a single manifest")); + ImageToJsonTranslator imageTranslator = new ImageToJsonTranslator(builtImages.get(0)); + BlobDescriptor configDescriptor = + Digests.computeDigest(imageTranslator.getContainerConfiguration()); + return imageTranslator.getManifestTemplate( + buildContext.getTargetFormat(), configDescriptor); + } + + eventHandlers.dispatch(LogEvent.info("Building a manifest list")); + return new ManifestListGenerator(builtImages) + .getManifestListTemplate(buildContext.getTargetFormat()); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java new file mode 100644 index 0000000000..570152f4e6 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import java.io.IOException; +import java.util.Objects; + +/** Used to record the results of a build. */ +public class BuildResult { + + /** + * Gets a {@link BuildResult} from an {@link Image}. + * + * @param image the image + * @param targetFormat the target format of the image + * @return a new {@link BuildResult} with the image's digest and id + * @throws IOException if writing the digest or container configuration fails + */ + static BuildResult fromImage(Image image, Class targetFormat) + throws IOException { + ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); + BlobDescriptor containerConfigurationBlobDescriptor = + Digests.computeDigest(imageToJsonTranslator.getContainerConfiguration()); + BuildableManifestTemplate manifestTemplate = + imageToJsonTranslator.getManifestTemplate( + targetFormat, containerConfigurationBlobDescriptor); + DescriptorDigest imageDigest = Digests.computeJsonDigest(manifestTemplate); + DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + return new BuildResult(imageDigest, imageId, false); + } + + private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; + private final Boolean imagePushed; + + BuildResult(DescriptorDigest imageDigest, DescriptorDigest imageId, boolean imagePushed) { + this.imageDigest = imageDigest; + this.imageId = imageId; + this.imagePushed = imagePushed; + } + + public DescriptorDigest getImageDigest() { + return imageDigest; + } + + public DescriptorDigest getImageId() { + return imageId; + } + + public boolean isImagePushed() { + return imagePushed; + } + + @Override + public int hashCode() { + return Objects.hash(imageDigest, imageId); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BuildResult)) { + return false; + } + BuildResult otherBuildResult = (BuildResult) other; + return imageDigest.equals(otherBuildResult.imageDigest) + && imageId.equals(otherBuildResult.imageId) + && imagePushed.equals(otherBuildResult.imagePushed); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CheckManifestStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CheckManifestStep.java new file mode 100644 index 0000000000..3348bb279d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CheckManifestStep.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; +import com.google.cloud.tools.jib.registry.RegistryClient; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.Callable; + +/** Checks the existence of a manifest. */ +class CheckManifestStep implements Callable>> { + + private static final String DESCRIPTION = "Checking existence of manifest"; + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final RegistryClient registryClient; + private final ManifestTemplate manifestTemplate; + + CheckManifestStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + ManifestTemplate manifestTemplate) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.registryClient = registryClient; + this.manifestTemplate = manifestTemplate; + } + + @Override + public Optional> call() + throws IOException, RegistryException { + DescriptorDigest manifestDigest = Digests.computeJsonDigest(manifestTemplate); + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION); + ProgressEventDispatcher ignored2 = + progressEventDispatcherFactory.create( + "checking existence of manifest for " + manifestDigest, 1)) { + eventHandlers.dispatch( + LogEvent.info("Checking existence of manifest for " + manifestDigest + "...")); + + if (!JibSystemProperties.skipExistingImages()) { + eventHandlers.dispatch( + LogEvent.info("Skipping manifest existence check; system property set to false")); + return Optional.empty(); + } + + return registryClient.checkManifest(manifestDigest.toString()); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java deleted file mode 100644 index e5022f676c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -/** Logs the message before finalizing an image build. */ -class FinalizingStep implements AsyncStep, Callable { - - private final BuildConfiguration buildConfiguration; - private final List>>> - futureDependencyLists; - - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; - - /** - * @param wrappedDependencyLists {@link AsyncStep}s that must be unwrapped for additional {@link - * AsyncStep}s to depend on - * @param dependencyList list of additional {@link AsyncStep}s to depend on directly - */ - FinalizingStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - List>>> wrappedDependencyLists, - List> dependencyList) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.futureDependencyLists = wrappedDependencyLists; - - List> dependenciesFutures = - new ArrayList<>(wrappedDependencyLists.size() + dependencyList.size()); - for (AsyncStep dependency : wrappedDependencyLists) { - dependenciesFutures.add(dependency.getFuture()); - } - for (AsyncStep dependency : dependencyList) { - dependenciesFutures.add(dependency.getFuture()); - } - listenableFuture = - Futures.whenAllSucceed(dependenciesFutures).call(this, listeningExecutorService); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; - } - - @Override - public Void call() throws ExecutionException { - // Unwrap the wrapped dependencies. - List> unwrappedDependencies = new ArrayList<>(); - for (AsyncStep>> wrappedDependency : - futureDependencyLists) { - for (AsyncStep unwrappedDependency : NonBlockingSteps.get(wrappedDependency)) { - unwrappedDependencies.add(unwrappedDependency.getFuture()); - } - } - - Futures.whenAllSucceed(unwrappedDependencies) - .call( - () -> { - buildConfiguration.getBuildLogger().lifecycle("Finalizing..."); - return null; - }, - listeningExecutorService); - - return null; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 8d4f724a9c..5e2d11cb54 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,83 +16,65 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.image.Image; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.cloud.tools.jib.image.ImageTarball; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Adds image layers to a tarball and loads into Docker daemon. */ -class LoadDockerStep implements AsyncStep, Callable { +class LoadDockerStep implements Callable { - private final BuildConfiguration buildConfiguration; - private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - private final ImmutableList buildAndCacheApplicationLayerSteps; - private final BuildImageStep buildImageStep; + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; + private final DockerClient dockerClient; + private final Image builtImage; LoadDockerStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; - this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - this.buildImageStep = buildImageStep; - - listenableFuture = - Futures.whenAllSucceed( - pullAndCacheBaseImageLayersStep.getFuture(), buildImageStep.getFuture()) - .call(this, listeningExecutorService); + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + DockerClient dockerClient, + Image builtImage) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.dockerClient = dockerClient; + this.builtImage = builtImage; } @Override - public ListenableFuture getFuture() { - return listenableFuture; - } + public BuildResult call() throws InterruptedException, IOException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, "Loading to Docker daemon")) { + eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); + ImageTarball imageTarball = + new ImageTarball( + builtImage, + buildContext.getTargetImageConfiguration().getImage(), + buildContext.getAllTargetImageTags()); - @Override - public Void call() throws ExecutionException, InterruptedException { - ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - dependenciesBuilder.add(pullAndCacheBaseImageLayerStep.getFuture()); - } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - dependenciesBuilder.add(buildAndCacheApplicationLayerStep.getFuture()); - } - dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); - return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::afterPushBaseImageLayerFuturesFuture, listeningExecutorService) - .get(); - } - - private Void afterPushBaseImageLayerFuturesFuture() - throws ExecutionException, InterruptedException, IOException { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + // Note: The progress reported here is not entirely accurate. The total allocation units is + // the size of the layers, but the progress being reported includes the config and manifest + // as well, so we will always go over the total progress allocation here. + // See https://github.com/GoogleContainerTools/jib/pull/1960#discussion_r321898390 + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "loading to Docker daemon", imageTarball.getTotalLayerSize()); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { + // Load the image to docker daemon. + eventHandlers.dispatch( + LogEvent.debug(dockerClient.load(imageTarball, throttledProgressReporter))); - // Load the image to docker daemon. - buildConfiguration.getBuildLogger().lifecycle("Loading to Docker daemon..."); - new DockerClient() - .load( - new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageReference())); - - return null; + return BuildResult.fromImage(builtImage, buildContext.getTargetFormat()); + } + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java new file mode 100644 index 0000000000..1f9744774a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -0,0 +1,317 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.ImageDetails; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.tar.TarExtractor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.ByteStreams; +import com.google.common.util.concurrent.Futures; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** Extracts a tar file base image. */ +public class LocalBaseImageSteps { + + /** Contains an {@link Image} and its layers. * */ + static class LocalImage { + final List> layers; + final ContainerConfigurationTemplate configurationTemplate; + + LocalImage( + List> layers, ContainerConfigurationTemplate configurationTemplate) { + this.layers = layers; + this.configurationTemplate = configurationTemplate; + } + } + + private LocalBaseImageSteps() {} + + /** + * Checks the first two bytes of a file to see if it has been gzipped. + * + * @param path the file to check + * @return {@code true} if the file is gzipped, {@code false} if not + * @throws IOException if reading the file fails + * @see GZIP file format + */ + @VisibleForTesting + static boolean isGzipped(Path path) throws IOException { + try (InputStream inputStream = Files.newInputStream(path)) { + inputStream.mark(2); + int magic = (inputStream.read() & 0xff) | ((inputStream.read() << 8) & 0xff00); + return magic == GZIPInputStream.GZIP_MAGIC; + } + } + + static Callable retrieveDockerDaemonLayersStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + DockerClient dockerClient, + TempDirectoryProvider tempDirectoryProvider) { + return () -> { + ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("processing base image " + imageReference, 2); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildContext.getEventHandlers(), + "Saving " + imageReference + " from Docker daemon")) { + ImageDetails dockerImageDetails = dockerClient.inspect(imageReference); + Optional cachedImage = + getCachedDockerImage(buildContext.getBaseImageLayersCache(), dockerImageDetails); + if (cachedImage.isPresent()) { + PlatformChecker.checkManifestPlatform( + buildContext, cachedImage.get().configurationTemplate); + return cachedImage.get(); + } + + Path tarPath = tempDirectoryProvider.newDirectory().resolve("out.tar"); + long size = dockerImageDetails.getSize(); + try (ProgressEventDispatcher dockerProgress = + progressEventDispatcher + .newChildProducer() + .create("saving base image " + imageReference, size); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(dockerProgress::dispatchProgress)) { + dockerClient.save(imageReference, tarPath, throttledProgressReporter); + } + + LocalImage localImage = + cacheDockerImageTar( + buildContext, + tarPath, + progressEventDispatcher.newChildProducer(), + tempDirectoryProvider); + PlatformChecker.checkManifestPlatform(buildContext, localImage.configurationTemplate); + return localImage; + } + }; + } + + static Callable retrieveTarLayersStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Path tarPath, + TempDirectoryProvider tempDirectoryProvider) { + return () -> { + LocalImage localImage = + cacheDockerImageTar( + buildContext, tarPath, progressEventDispatcherFactory, tempDirectoryProvider); + PlatformChecker.checkManifestPlatform(buildContext, localImage.configurationTemplate); + return localImage; + }; + } + + static Callable returnImageAndRegistryClientStep( + List layers, ContainerConfigurationTemplate configurationTemplate) { + return () -> { + // Collect compressed layers and add to manifest + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + for (PreparedLayer layer : layers) { + BlobDescriptor descriptor = layer.getBlobDescriptor(); + v22Manifest.addLayer(descriptor.getSize(), descriptor.getDigest()); + } + + BlobDescriptor configDescriptor = Digests.computeDigest(configurationTemplate); + v22Manifest.setContainerConfiguration( + configDescriptor.getSize(), configDescriptor.getDigest()); + return new ImagesAndRegistryClient( + Collections.singletonList( + JsonToImageTranslator.toImage(v22Manifest, configurationTemplate)), + null); + }; + } + + @VisibleForTesting + static Optional getCachedDockerImage(Cache cache, ImageDetails dockerImageDetails) + throws DigestException, IOException, CacheCorruptedException { + // Get config + Optional cachedConfig = + cache.retrieveLocalConfig(dockerImageDetails.getImageId()); + if (!cachedConfig.isPresent()) { + return Optional.empty(); + } + + // Get layers + List> cachedLayers = new ArrayList<>(); + for (DescriptorDigest diffId : dockerImageDetails.getDiffIds()) { + Optional cachedLayer = cache.retrieveTarLayer(diffId); + if (!cachedLayer.isPresent()) { + return Optional.empty(); + } + CachedLayer layer = cachedLayer.get(); + cachedLayers.add(Futures.immediateFuture(new PreparedLayer.Builder(layer).build())); + } + + return Optional.of(new LocalImage(cachedLayers, cachedConfig.get())); + } + + @VisibleForTesting + static LocalImage cacheDockerImageTar( + BuildContext buildContext, + Path tarPath, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + TempDirectoryProvider tempDirectoryProvider) + throws IOException, LayerCountMismatchException { + ExecutorService executorService = buildContext.getExecutorService(); + Path destination = tempDirectoryProvider.newDirectory(); + + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildContext.getEventHandlers(), + "Extracting tar " + tarPath + " into " + destination)) { + TarExtractor.extract(tarPath, destination); + + DockerManifestEntryTemplate loadManifest; + try (InputStream manifestStream = + Files.newInputStream(destination.resolve("manifest.json"))) { + loadManifest = + JsonMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .build() + .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; + } + + Path configPath = destination.resolve(loadManifest.getConfig()); + ContainerConfigurationTemplate configurationTemplate = + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + // Don't compute the digest of the loaded Java JSON instance. + BlobDescriptor originalConfigDescriptor = + Blobs.from(configPath).writeTo(ByteStreams.nullOutputStream()); + + List layerFiles = loadManifest.getLayerFiles(); + if (configurationTemplate.getLayerCount() != layerFiles.size()) { + throw new LayerCountMismatchException( + "Invalid base image format: manifest contains " + + layerFiles.size() + + " layers, but container configuration contains " + + configurationTemplate.getLayerCount() + + " layers"); + } + buildContext + .getBaseImageLayersCache() + .writeLocalConfig(originalConfigDescriptor.getDigest(), configurationTemplate); + + // Check the first layer to see if the layers are compressed already. 'docker save' output + // is uncompressed, but a jib-built tar has compressed layers. + boolean layersAreCompressed = + !layerFiles.isEmpty() && isGzipped(destination.resolve(layerFiles.get(0))); + + // Process layer blobs + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "processing base image layers", layerFiles.size())) { + // Start compressing layers in parallel + List> preparedLayers = new ArrayList<>(); + for (int index = 0; index < layerFiles.size(); index++) { + Path layerFile = destination.resolve(layerFiles.get(index)); + DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); + ProgressEventDispatcher.Factory layerProgressDispatcherFactory = + progressEventDispatcher.newChildProducer(); + preparedLayers.add( + executorService.submit( + () -> + compressAndCacheTarLayer( + buildContext.getBaseImageLayersCache(), + diffId, + layerFile, + layersAreCompressed, + layerProgressDispatcherFactory))); + } + return new LocalImage(preparedLayers, configurationTemplate); + } + } + } + + private static PreparedLayer compressAndCacheTarLayer( + Cache cache, + DescriptorDigest diffId, + Path layerFile, + boolean layersAreCompressed, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) + throws IOException, CacheCorruptedException { + try (ProgressEventDispatcher childDispatcher = + progressEventDispatcherFactory.create( + "compressing layer " + diffId, Files.size(layerFile)); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { + // Retrieve pre-compressed layer from cache + Optional optionalLayer = cache.retrieveTarLayer(diffId); + if (optionalLayer.isPresent()) { + return new PreparedLayer.Builder(optionalLayer.get()).build(); + } + + // Just write layers that are already compressed + if (layersAreCompressed) { + return new PreparedLayer.Builder(cache.writeTarLayer(diffId, Blobs.from(layerFile))) + .build(); + } + + // Compress uncompressed layers while writing + Blob compressedBlob = + Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { + Blobs.from(layerFile).writeTo(notifyingOutputStream); + } + }, + true); + return new PreparedLayer.Builder(cache.writeTarLayer(diffId, compressedBlob)).build(); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java new file mode 100644 index 0000000000..2a16f87a69 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -0,0 +1,151 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.base.Verify; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.Callable; +import javax.annotation.Nullable; + +/** Pulls and caches a single base image layer. */ +class ObtainBaseImageLayerStep implements Callable { + + @SuppressWarnings("InlineFormatString") + private static final String DESCRIPTION = "Pulling base image layer %s"; + + @FunctionalInterface + private interface BlobExistenceChecker { + + StateInTarget check(DescriptorDigest digest) throws IOException, RegistryException; + } + + static ObtainBaseImageLayerStep forForcedDownload( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Layer layer, + @Nullable RegistryClient sourceRegistryClient) { + BlobExistenceChecker noOpChecker = ignored -> StateInTarget.UNKNOWN; + return new ObtainBaseImageLayerStep( + buildContext, progressEventDispatcherFactory, layer, sourceRegistryClient, noOpChecker); + } + + static ObtainBaseImageLayerStep forSelectiveDownload( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Layer layer, + @Nullable RegistryClient sourceRegistryClient, + RegistryClient targetRegistryClient) { + Verify.verify(!buildContext.isOffline()); + + // TODO: also check if cross-repo blob mount is possible. + BlobExistenceChecker blobExistenceChecker = + digest -> + targetRegistryClient.checkBlob(digest).isPresent() + ? StateInTarget.EXISTING + : StateInTarget.MISSING; + + return new ObtainBaseImageLayerStep( + buildContext, + progressEventDispatcherFactory, + layer, + sourceRegistryClient, + blobExistenceChecker); + } + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + private final Layer layer; + private final @Nullable RegistryClient registryClient; + private final BlobExistenceChecker blobExistenceChecker; + + private ObtainBaseImageLayerStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Layer layer, + @Nullable RegistryClient registryClient, + BlobExistenceChecker blobExistenceChecker) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.layer = layer; + this.registryClient = registryClient; + this.blobExistenceChecker = blobExistenceChecker; + } + + @Override + public PreparedLayer call() throws IOException, CacheCorruptedException, RegistryException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); + TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, String.format(DESCRIPTION, layerDigest))) { + + StateInTarget stateInTarget = blobExistenceChecker.check(layerDigest); + if (stateInTarget == StateInTarget.EXISTING) { + eventHandlers.dispatch( + LogEvent.info( + "Skipping pull; BLOB already exists on target registry : " + + layer.getBlobDescriptor())); + return new PreparedLayer.Builder(layer).setStateInTarget(stateInTarget).build(); + } + + Cache cache = buildContext.getBaseImageLayersCache(); + + // Checks if the layer already exists in the cache. + Optional optionalCachedLayer = cache.retrieve(layerDigest); + if (optionalCachedLayer.isPresent()) { + CachedLayer cachedLayer = optionalCachedLayer.get(); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); + } else if (buildContext.isOffline()) { + throw new IOException( + "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + + layerDigest + + ". Rerun Jib in online mode with \"-Djib.alwaysCacheBaseImage=true\" to " + + "re-download the base image layers."); + } + + try (ThrottledProgressEventDispatcherWrapper progressEventDispatcherWrapper = + new ThrottledProgressEventDispatcherWrapper( + progressEventDispatcher.newChildProducer(), + "pulling base image layer " + layerDigest)) { + CachedLayer cachedLayer = + cache.writeCompressedLayer( + Verify.verifyNotNull(registryClient) + .pullBlob( + layerDigest, + progressEventDispatcherWrapper::setProgressTarget, + progressEventDispatcherWrapper::dispatchProgress)); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); + } + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PlatformChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PlatformChecker.java new file mode 100644 index 0000000000..6f58544b73 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PlatformChecker.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.PlatformNotFoundInBaseImageException; +import com.google.common.base.Verify; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; + +/** Provides helper methods to check platforms. */ +public class PlatformChecker { + + private PlatformChecker() {} + + /** + * Assuming the base image is not a manifest list, checks and warns misconfigured platforms. + * + * @param buildContext the {@link BuildContext} + * @param containerConfig container configuration JSON of the base image + */ + static void checkManifestPlatform( + BuildContext buildContext, ContainerConfigurationTemplate containerConfig) + throws PlatformNotFoundInBaseImageException { + Optional path = buildContext.getBaseImageConfiguration().getTarPath(); + String baseImageName = + path.map(Path::toString) + .orElse(buildContext.getBaseImageConfiguration().getImage().toString()); + + Set platforms = buildContext.getContainerConfiguration().getPlatforms(); + Verify.verify(!platforms.isEmpty()); + + if (platforms.size() != 1) { + String msg = + String.format( + "cannot build for multiple platforms since the base image '%s' is not a manifest list.", + baseImageName); + throw new PlatformNotFoundInBaseImageException(msg); + } else { + Platform platform = platforms.iterator().next(); + if (!platform.getArchitecture().equals(containerConfig.getArchitecture()) + || !platform.getOs().equals(containerConfig.getOs())) { + + // Unfortunately, "platforms" has amd64/linux by default even if the user didn't explicitly + // configure it. Skip reporting to suppress false alarm. + if (!(platform.getArchitecture().equals("amd64") && platform.getOs().equals("linux"))) { + String msg = + String.format( + "the configured platform (%s/%s) doesn't match the platform (%s/%s) of the base image (%s)", + platform.getArchitecture(), + platform.getOs(), + containerConfig.getArchitecture(), + containerConfig.getOs(), + baseImageName); + throw new PlatformNotFoundInBaseImageException(msg); + } + } + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java new file mode 100644 index 0000000000..52e3eda7a4 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.image.Layer; + +/** + * Layer prepared from {@link BuildAndCacheApplicationLayerStep} and {@link + * ObtainBaseImageLayerStep} to hold information about either a base image layer or an application + * layer. + */ +class PreparedLayer implements Layer { + + enum StateInTarget { + UNKNOWN, + EXISTING, + MISSING + } + + static class Builder { + + private Layer layer; + private String name = "unnamed layer"; + private StateInTarget stateInTarget = StateInTarget.UNKNOWN; + + Builder(Layer layer) { + this.layer = layer; + } + + Builder setName(String name) { + this.name = name; + return this; + } + + /** Sets whether the layer exists in a target destination. */ + Builder setStateInTarget(StateInTarget stateInTarget) { + this.stateInTarget = stateInTarget; + return this; + } + + PreparedLayer build() { + return new PreparedLayer(layer, name, stateInTarget); + } + } + + private final Layer layer; + private final String name; + private final StateInTarget stateInTarget; + + private PreparedLayer(Layer layer, String name, StateInTarget stateInTarget) { + this.layer = layer; + this.name = name; + this.stateInTarget = stateInTarget; + } + + String getName() { + return name; + } + + StateInTarget getStateInTarget() { + return stateInTarget; + } + + @Override + public Blob getBlob() { + return layer.getBlob(); + } + + @Override + public BlobDescriptor getBlobDescriptor() { + return layer.getBlobDescriptor(); + } + + @Override + public DescriptorDigest getDiffId() { + return layer.getDiffId(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java deleted file mode 100644 index 71df43c8f2..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheReader; -import com.google.cloud.tools.jib.cache.CacheWriter; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.common.io.CountingOutputStream; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; -import java.util.concurrent.Callable; -import javax.annotation.Nullable; - -/** Pulls and caches a single base image layer. */ -class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable { - - private static final String DESCRIPTION = "Pulling base image layer %s"; - - private final BuildConfiguration buildConfiguration; - private final Cache cache; - private final DescriptorDigest layerDigest; - private final @Nullable Authorization pullAuthorization; - - private final ListenableFuture listenableFuture; - - PullAndCacheBaseImageLayerStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - Cache cache, - DescriptorDigest layerDigest, - @Nullable Authorization pullAuthorization) { - this.buildConfiguration = buildConfiguration; - this.cache = cache; - this.layerDigest = layerDigest; - this.pullAuthorization = pullAuthorization; - - listenableFuture = listeningExecutorService.submit(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; - } - - @Override - public CachedLayer call() throws IOException, RegistryException { - try (Timer ignored = - new Timer(buildConfiguration.getBuildLogger(), String.format(DESCRIPTION, layerDigest))) { - RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .setAuthorization(pullAuthorization) - .newRegistryClient(); - - // Checks if the layer already exists in the cache. - CachedLayer cachedLayer = new CacheReader(cache).getLayer(layerDigest); - if (cachedLayer != null) { - return cachedLayer; - } - - CacheWriter cacheWriter = new CacheWriter(cache); - CountingOutputStream layerOutputStream = cacheWriter.getLayerOutputStream(layerDigest); - registryClient.pullBlob(layerDigest, layerOutputStream); - layerOutputStream.close(); - return cacheWriter.getCachedLayer(layerOutputStream.getCount(), layerDigest); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java deleted file mode 100644 index 26eef859b2..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -/** Pulls and caches the base image layers. */ -class PullAndCacheBaseImageLayersStep - implements AsyncStep>, - Callable> { - - private static final String DESCRIPTION = "Setting up base image caching"; - - private final BuildConfiguration buildConfiguration; - private final Cache cache; - private final PullBaseImageStep pullBaseImageStep; - - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture> listenableFuture; - - PullAndCacheBaseImageLayersStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - Cache cache, - PullBaseImageStep pullBaseImageStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.cache = cache; - this.pullBaseImageStep = pullBaseImageStep; - - listenableFuture = - Futures.whenAllSucceed(pullBaseImageStep.getFuture()).call(this, listeningExecutorService); - } - - @Override - public ListenableFuture> getFuture() { - return listenableFuture; - } - - @Override - public ImmutableList call() - throws ExecutionException, LayerPropertyNotFoundException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); - ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); - - ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = - ImmutableList.builderWithExpectedSize(baseImageLayers.size()); - for (Layer layer : baseImageLayers) { - pullAndCacheBaseImageLayerStepsBuilder.add( - new PullAndCacheBaseImageLayerStep( - listeningExecutorService, - buildConfiguration, - cache, - layer.getBlobDescriptor().getDigest(), - pullBaseImageStepResult.getBaseImageAuthorization())); - } - - return pullAndCacheBaseImageLayerStepsBuilder.build(); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 14a3811bd9..6ce49ba41a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,144 +16,261 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; +import com.google.cloud.tools.jib.api.buildplan.Platform; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestListTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.PlatformNotFoundInBaseImageException; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; +import com.google.cloud.tools.jib.image.json.UnlistedPlatformInManifestListException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticator; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import java.io.ByteArrayOutputStream; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; -/** Pulls the base image manifest. */ -class PullBaseImageStep - implements AsyncStep, Callable { +/** Pulls the base image manifests for the specified platforms. */ +class PullBaseImageStep implements Callable { private static final String DESCRIPTION = "Pulling base image manifest"; /** Structure for the result returned by this step. */ - static class BaseImageWithAuthorization { + static class ImagesAndRegistryClient { - private final Image baseImage; - private final @Nullable Authorization baseImageAuthorization; + final List images; + @Nullable final RegistryClient registryClient; - private BaseImageWithAuthorization( - Image baseImage, @Nullable Authorization baseImageAuthorization) { - this.baseImage = baseImage; - this.baseImageAuthorization = baseImageAuthorization; + ImagesAndRegistryClient(List images, @Nullable RegistryClient registryClient) { + this.images = images; + this.registryClient = registryClient; } + } - Image getBaseImage() { - return baseImage; - } + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressDispatcherFactory; - @Nullable - Authorization getBaseImageAuthorization() { - return baseImageAuthorization; - } + PullBaseImageStep( + BuildContext buildContext, ProgressEventDispatcher.Factory progressDispatcherFactory) { + this.buildContext = buildContext; + this.progressDispatcherFactory = progressDispatcherFactory; } - private final BuildConfiguration buildConfiguration; + @Override + public ImagesAndRegistryClient call() + throws IOException, RegistryException, LayerPropertyNotFoundException, + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create("pulling base image manifest", 4); + TimerEventDispatcher ignored1 = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) { - private final ListenableFuture listenableFuture; + // Skip this step if this is a scratch image + ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); + if (imageReference.isScratch()) { + Set platforms = buildContext.getContainerConfiguration().getPlatforms(); + Verify.verify(!platforms.isEmpty()); - PullBaseImageStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { - this.buildConfiguration = buildConfiguration; + eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); + ImmutableList.Builder images = ImmutableList.builder(); + for (Platform platform : platforms) { + Image.Builder imageBuilder = Image.builder(buildContext.getTargetFormat()); + imageBuilder.setArchitecture(platform.getArchitecture()).setOs(platform.getOs()); + images.add(imageBuilder.build()); + } + return new ImagesAndRegistryClient(images.build(), null); + } - listenableFuture = listeningExecutorService.submit(this); - } + eventHandlers.dispatch( + LogEvent.progress("Getting manifest for base image " + imageReference + "...")); - @Override - public ListenableFuture getFuture() { - return listenableFuture; - } + if (buildContext.isOffline()) { + List images = getCachedBaseImages(); + if (!images.isEmpty()) { + return new ImagesAndRegistryClient(images, null); + } + throw new IOException( + "Cannot run Jib in offline mode; " + imageReference + " not found in local Jib cache"); + + } else if (imageReference.getDigest().isPresent()) { + List images = getCachedBaseImages(); + if (!images.isEmpty()) { + RegistryClient noAuthRegistryClient = + buildContext.newBaseImageRegistryClientFactory().newRegistryClient(); + // TODO: passing noAuthRegistryClient may be problematic. It may return 401 unauthorized + // if layers have to be downloaded. + // https://github.com/GoogleContainerTools/jib/issues/2220 + return new ImagesAndRegistryClient(images, noAuthRegistryClient); + } + } + + Optional mirrorPull = + tryMirrors(buildContext, progressDispatcher.newChildProducer()); + if (mirrorPull.isPresent()) { + return mirrorPull.get(); + } - @Override - public BaseImageWithAuthorization call() - throws IOException, RegistryException, LayerPropertyNotFoundException, - LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, - RegistryAuthenticationFailedException { - buildConfiguration - .getBuildLogger() - .lifecycle("Getting base image " + buildConfiguration.getBaseImageReference() + "..."); - - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - // First, try with no credentials. try { - return new BaseImageWithAuthorization(pullBaseImage(null), null); + // First, try with no credentials. This works with public GCR images (but not Docker Hub). + // TODO: investigate if we should just pass credentials up front. However, this involves + // some risk. https://github.com/GoogleContainerTools/jib/pull/2200#discussion_r359069026 + // contains some related discussions. + RegistryClient noAuthRegistryClient = + buildContext.newBaseImageRegistryClientFactory().newRegistryClient(); + return new ImagesAndRegistryClient( + pullBaseImages(noAuthRegistryClient, progressDispatcher.newChildProducer()), + noAuthRegistryClient); } catch (RegistryUnauthorizedException ex) { - // If failed, then, retrieve base registry credentials and try with retrieved credentials. - // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to - // registry.credentials.RegistryCredentialsRetriever to avoid this direct executor hack. - ListeningExecutorService directExecutorService = MoreExecutors.newDirectExecutorService(); - RetrieveRegistryCredentialsStep retrieveBaseRegistryCredentialsStep = - RetrieveRegistryCredentialsStep.forBaseImage(directExecutorService, buildConfiguration); - - Authorization registryCredentials = - NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); - - try { - return new BaseImageWithAuthorization( - pullBaseImage(registryCredentials), registryCredentials); - - } catch (RegistryUnauthorizedException registryUnauthorizedException) { - // The registry requires us to authenticate using the Docker Token Authentication. - // See https://docs.docker.com/registry/spec/auth/token - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .initialize(); - if (registryAuthenticator == null) { - buildConfiguration - .getBuildLogger() - .error( - "Failed to retrieve authentication challenge for registry that required token authentication"); - throw registryUnauthorizedException; + eventHandlers.dispatch( + LogEvent.lifecycle( + "The base image requires auth. Trying again for " + imageReference + "...")); + + Credential credential = + RegistryCredentialRetriever.getBaseImageCredential(buildContext).orElse(null); + RegistryClient registryClient = + buildContext + .newBaseImageRegistryClientFactory() + .setCredential(credential) + .newRegistryClient(); + + String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate(); + if (wwwAuthenticate != null) { + eventHandlers.dispatch( + LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate)); + registryClient.authPullByWwwAuthenticate(wwwAuthenticate); + return new ImagesAndRegistryClient( + pullBaseImages(registryClient, progressDispatcher.newChildProducer()), + registryClient); + + } else { + // Not getting WWW-Authenticate is unexpected in practice, and we may just blame the + // server and fail. However, to keep some old behavior, try a few things as a last resort. + // TODO: consider removing this fallback branch. + if (credential != null && !credential.isOAuth2RefreshToken()) { + eventHandlers.dispatch( + LogEvent.debug("Trying basic auth as fallback for " + imageReference + "...")); + registryClient.configureBasicAuth(); + try { + return new ImagesAndRegistryClient( + pullBaseImages(registryClient, progressDispatcher.newChildProducer()), + registryClient); + } catch (RegistryUnauthorizedException ignored) { + // Fall back to try bearer auth. + } } - registryCredentials = - registryAuthenticator.setAuthorization(registryCredentials).authenticatePull(); - return new BaseImageWithAuthorization( - pullBaseImage(registryCredentials), registryCredentials); + eventHandlers.dispatch( + LogEvent.debug("Trying bearer auth as fallback for " + imageReference + "...")); + registryClient.doPullBearerAuth(); + return new ImagesAndRegistryClient( + pullBaseImages(registryClient, progressDispatcher.newChildProducer()), + registryClient); } } } } + @VisibleForTesting + Optional tryMirrors( + BuildContext buildContext, ProgressEventDispatcher.Factory progressDispatcherFactory) + throws LayerCountMismatchException, BadContainerConfigurationFormatException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + + Collection> mirrorEntries = + buildContext.getRegistryMirrors().entries(); + try (ProgressEventDispatcher progressDispatcher1 = + progressDispatcherFactory.create("trying mirrors", mirrorEntries.size()); + TimerEventDispatcher ignored1 = new TimerEventDispatcher(eventHandlers, "trying mirrors")) { + for (Map.Entry entry : mirrorEntries) { + String registry = entry.getKey(); + String mirror = entry.getValue(); + eventHandlers.dispatch(LogEvent.debug("mirror config: " + registry + " --> " + mirror)); + + if (!buildContext.getBaseImageConfiguration().getImageRegistry().equals(registry)) { + progressDispatcher1.dispatchProgress(1); + continue; + } + + eventHandlers.dispatch(LogEvent.info("trying mirror " + mirror + " for the base image")); + try (ProgressEventDispatcher progressDispatcher2 = + progressDispatcher1.newChildProducer().create("trying mirror " + mirror, 2)) { + RegistryClient registryClient = + buildContext.newBaseImageRegistryClientFactory(mirror).newRegistryClient(); + List images = pullPublicImages(registryClient, progressDispatcher2); + eventHandlers.dispatch(LogEvent.info("pulled manifest from mirror " + mirror)); + return Optional.of(new ImagesAndRegistryClient(images, registryClient)); + + } catch (IOException | RegistryException ex) { + // Ignore errors from this mirror and continue. + eventHandlers.dispatch( + LogEvent.debug( + "failed to get manifest from mirror " + mirror + ": " + ex.getMessage())); + } + } + return Optional.empty(); + } + } + + private List pullPublicImages( + RegistryClient registryClient, ProgressEventDispatcher progressDispatcher) + throws IOException, RegistryException, LayerCountMismatchException, + BadContainerConfigurationFormatException { + try { + // First, try with no credentials. This works with public GCR images. + return pullBaseImages(registryClient, progressDispatcher.newChildProducer()); + + } catch (RegistryUnauthorizedException ex) { + // in case if a registry requires bearer auth + registryClient.doPullBearerAuth(); + return pullBaseImages(registryClient, progressDispatcher.newChildProducer()); + } + } + /** - * Pulls the base image. + * Pulls the base images specified in the platforms list. * - * @param registryCredentials authentication credentials to possibly use - * @return the pulled image + * @param registryClient to communicate with remote registry + * @param progressDispatcherFactory the {@link ProgressEventDispatcher.Factory} for emitting + * {@link ProgressEvent}s + * @return the list of pulled base images and a registry client * @throws IOException when an I/O exception occurs during the pulling * @throws RegistryException if communicating with the registry caused a known error * @throws LayerCountMismatchException if the manifest and configuration contain conflicting layer @@ -162,48 +279,249 @@ public BaseImageWithAuthorization call() * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - private Image pullBaseImage(@Nullable Authorization registryCredentials) + private List pullBaseImages( + RegistryClient registryClient, ProgressEventDispatcher.Factory progressDispatcherFactory) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { - RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .setAuthorization(registryCredentials) - .newRegistryClient(); - - ManifestTemplate manifestTemplate = - registryClient.pullManifest(buildConfiguration.getBaseImageTag()); - - // TODO: Make schema version be enum. - switch (manifestTemplate.getSchemaVersion()) { - case 1: - V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; - return JsonToImageTranslator.toImage(v21ManifestTemplate); - - case 2: - V22ManifestTemplate v22ManifestTemplate = (V22ManifestTemplate) manifestTemplate; - if (v22ManifestTemplate.getContainerConfiguration() == null - || v22ManifestTemplate.getContainerConfiguration().getDigest() == null) { - throw new UnknownManifestFormatException( - "Invalid container configuration in Docker V2.2 manifest: \n" - + Blobs.writeToString(JsonTemplateMapper.toBlob(v22ManifestTemplate))); + Cache cache = buildContext.getBaseImageLayersCache(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); + ImageConfiguration baseImageConfig = buildContext.getBaseImageConfiguration(); + + try (ProgressEventDispatcher progressDispatcher1 = + progressDispatcherFactory.create("pulling base image manifest and container config", 2)) { + ManifestAndDigest manifestAndDigest = + registryClient.pullManifest(baseImageConfig.getImageQualifier()); + eventHandlers.dispatch( + LogEvent.lifecycle("Using base image with digest: " + manifestAndDigest.getDigest())); + progressDispatcher1.dispatchProgress(1); + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + progressDispatcher1.newChildProducer(); + + ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); + if (manifestTemplate instanceof V21ManifestTemplate) { + V21ManifestTemplate v21Manifest = (V21ManifestTemplate) manifestTemplate; + cache.writeMetadata(baseImageConfig.getImage(), v21Manifest); + return Collections.singletonList(JsonToImageTranslator.toImage(v21Manifest)); + + } else if (manifestTemplate instanceof BuildableManifestTemplate) { + // V22ManifestTemplate or OciManifestTemplate + BuildableManifestTemplate imageManifest = (BuildableManifestTemplate) manifestTemplate; + ContainerConfigurationTemplate containerConfig = + pullContainerConfigJson( + manifestAndDigest, registryClient, childProgressDispatcherFactory); + PlatformChecker.checkManifestPlatform(buildContext, containerConfig); + cache.writeMetadata(baseImageConfig.getImage(), imageManifest, containerConfig); + return Collections.singletonList( + JsonToImageTranslator.toImage(imageManifest, containerConfig)); + } + + Verify.verify(manifestTemplate instanceof ManifestListTemplate); + + List manifestsAndConfigs = new ArrayList<>(); + ImmutableList.Builder images = ImmutableList.builder(); + Set platforms = buildContext.getContainerConfiguration().getPlatforms(); + try (ProgressEventDispatcher progressDispatcher2 = + childProgressDispatcherFactory.create( + "pulling platform-specific manifests and container configs", 2L * platforms.size())) { + // If a manifest list, search for the manifests matching the given platforms. + for (Platform platform : platforms) { + String message = "Searching for architecture=%s, os=%s in the base image manifest list"; + eventHandlers.dispatch( + LogEvent.info(String.format(message, platform.getArchitecture(), platform.getOs()))); + + String manifestDigest = + lookUpPlatformSpecificImageManifest( + (ManifestListTemplate) manifestTemplate, platform); + // TODO: pull multiple manifests (+ container configs) in parallel. + ManifestAndDigest imageManifestAndDigest = registryClient.pullManifest(manifestDigest); + progressDispatcher2.dispatchProgress(1); + + BuildableManifestTemplate imageManifest = + (BuildableManifestTemplate) imageManifestAndDigest.getManifest(); + ContainerConfigurationTemplate containerConfig = + pullContainerConfigJson( + imageManifestAndDigest, registryClient, progressDispatcher2.newChildProducer()); + + manifestsAndConfigs.add( + new ManifestAndConfigTemplate(imageManifest, containerConfig, manifestDigest)); + images.add(JsonToImageTranslator.toImage(imageManifest, containerConfig)); } + } - ByteArrayOutputStream containerConfigurationOutputStream = new ByteArrayOutputStream(); - registryClient.pullBlob( - v22ManifestTemplate.getContainerConfiguration().getDigest(), - containerConfigurationOutputStream); - String containerConfigurationString = - new String(containerConfigurationOutputStream.toByteArray(), StandardCharsets.UTF_8); - - ContainerConfigurationTemplate containerConfigurationTemplate = - JsonTemplateMapper.readJson( - containerConfigurationString, ContainerConfigurationTemplate.class); - return JsonToImageTranslator.toImage(v22ManifestTemplate, containerConfigurationTemplate); + cache.writeMetadata( + baseImageConfig.getImage(), + new ImageMetadataTemplate(manifestTemplate /* manifest list */, manifestsAndConfigs)); + return images.build(); } + } + + /** + * Looks through a manifest list for the manifest matching the {@code platform} and returns the + * digest of the first manifest it finds. + */ + @VisibleForTesting + String lookUpPlatformSpecificImageManifest( + ManifestListTemplate manifestListTemplate, Platform platform) + throws UnlistedPlatformInManifestListException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + + List digests = + manifestListTemplate.getDigestsForPlatform(platform.getArchitecture(), platform.getOs()); + if (digests.isEmpty()) { + String errorTemplate = + buildContext.getBaseImageConfiguration().getImage() + + " is a manifest list, but the list does not contain an image for architecture=%s, " + + "os=%s. If your intention was to specify a platform for your image, see " + + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#how-do-i-specify-a-platform-in-the-manifest-list-or-oci-index-of-a-base-image"; + String error = String.format(errorTemplate, platform.getArchitecture(), platform.getOs()); + eventHandlers.dispatch(LogEvent.error(error)); + throw new UnlistedPlatformInManifestListException(error); + } + // TODO: perhaps we should return multiple digests matching the platform. + return digests.get(0); + } + + /** + * Pulls a container configuration JSON specified in the given manifest. + * + * @param manifestAndDigest a manifest JSON and its digest + * @param registryClient to communicate with remote registry + * @param progressDispatcherFactory the {@link ProgressEventDispatcher.Factory} for emitting + * {@link ProgressEvent}s + * @return pulled {@link ContainerConfigurationTemplate} + * @throws IOException when an I/O exception occurs during the pulling + * @throws LayerPropertyNotFoundException if adding image layers fails + */ + private ContainerConfigurationTemplate pullContainerConfigJson( + ManifestAndDigest manifestAndDigest, + RegistryClient registryClient, + ProgressEventDispatcher.Factory progressDispatcherFactory) + throws IOException, LayerPropertyNotFoundException, UnknownManifestFormatException { + BuildableManifestTemplate manifest = + (BuildableManifestTemplate) manifestAndDigest.getManifest(); + Preconditions.checkArgument(manifest.getSchemaVersion() == 2); + + if (manifest.getContainerConfiguration() == null + || manifest.getContainerConfiguration().getDigest() == null) { + throw new UnknownManifestFormatException( + "Invalid container configuration in Docker V2.2/OCI manifest: \n" + + JsonTemplateMapper.toUtf8String(manifest)); + } + + try (ThrottledProgressEventDispatcherWrapper progressDispatcherWrapper = + new ThrottledProgressEventDispatcherWrapper( + progressDispatcherFactory, + "pull container configuration " + manifest.getContainerConfiguration().getDigest())) { + + String containerConfigString = + Blobs.writeToString( + registryClient.pullBlob( + manifest.getContainerConfiguration().getDigest(), + progressDispatcherWrapper::setProgressTarget, + progressDispatcherWrapper::dispatchProgress)); + return JsonTemplateMapper.readJson( + containerConfigString, ContainerConfigurationTemplate.class); + } + } + + /** + * Retrieves the cached base images. If a base image reference is not a manifest list, returns a + * single image (if cached). If a manifest list, returns all the images matching the configured + * platforms in the manifest list but only when all of the images are cached. + * + * @return the cached images, if found + * @throws IOException when an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + * @throws LayerPropertyNotFoundException if adding image layers fails + * @throws BadContainerConfigurationFormatException if the container configuration is in a bad + * format + * @throws UnlistedPlatformInManifestListException if a cached manifest list has no manifests + * matching the configured platform + */ + @VisibleForTesting + List getCachedBaseImages() + throws IOException, CacheCorruptedException, BadContainerConfigurationFormatException, + LayerCountMismatchException, UnlistedPlatformInManifestListException, + PlatformNotFoundInBaseImageException { + ImageReference baseImage = buildContext.getBaseImageConfiguration().getImage(); + Optional metadata = + buildContext.getBaseImageLayersCache().retrieveMetadata(baseImage); + if (!metadata.isPresent()) { + return Collections.emptyList(); + } + + ManifestTemplate manifestList = metadata.get().getManifestList(); + List manifestsAndConfigs = metadata.get().getManifestsAndConfigs(); + + if (manifestList == null) { + Verify.verify(manifestsAndConfigs.size() == 1); + ManifestAndConfigTemplate manifestAndConfig = manifestsAndConfigs.get(0); + Optional cachedImage = getBaseImageIfAllLayersCached(manifestAndConfig, true); + if (!cachedImage.isPresent()) { + return Collections.emptyList(); + } + return Collections.singletonList(cachedImage.get()); + } + + // Manifest list cached. Identify matching platforms and check if all of them are cached. + ImmutableList.Builder images = ImmutableList.builder(); + for (Platform platform : buildContext.getContainerConfiguration().getPlatforms()) { + String manifestDigest = + lookUpPlatformSpecificImageManifest((ManifestListTemplate) manifestList, platform); - throw new IllegalStateException("Unknown manifest schema version"); + Optional manifestAndConfigFound = + manifestsAndConfigs.stream() + .filter(entry -> manifestDigest.equals(entry.getManifestDigest())) + .findFirst(); + if (!manifestAndConfigFound.isPresent()) { + return Collections.emptyList(); + } + Optional cachedImage = + getBaseImageIfAllLayersCached(manifestAndConfigFound.get(), false); + if (!cachedImage.isPresent()) { + return Collections.emptyList(); + } + images.add(cachedImage.get()); + } + return images.build(); + } + + /** + * Helper method to retrieve a base image from cache given manifest and container config. Does not + * return image if any layers of base image are missing in cache. + * + * @param manifestAndConfig stores an image manifest and a container config + * @param isSingleManifest true if base image is not a manifest list + * @return the single cached {@link Image} found, or {@code Optional#empty()} if the base image + * not found in cache with all layers present. + * @throws BadContainerConfigurationFormatException if the container configuration is in a bad + * format + * @throws PlatformNotFoundInBaseImageException if build target platform is not found in the base + * image + * @throws LayerCountMismatchException LayerCountMismatchException if the manifest and + * configuration contain conflicting layer information + */ + private Optional getBaseImageIfAllLayersCached( + ManifestAndConfigTemplate manifestAndConfig, boolean isSingleManifest) + throws BadContainerConfigurationFormatException, PlatformNotFoundInBaseImageException, + LayerCountMismatchException { + Cache baseImageLayersCache = buildContext.getBaseImageLayersCache(); + ManifestTemplate manifest = Verify.verifyNotNull(manifestAndConfig.getManifest()); + + // Verify all layers described in manifest are present in cache + if (!baseImageLayersCache.areAllLayersCached(manifest)) { + return Optional.empty(); + } + if (manifest instanceof V21ManifestTemplate) { + return Optional.of(JsonToImageTranslator.toImage((V21ManifestTemplate) manifest)); + } + ContainerConfigurationTemplate containerConfig = + Verify.verifyNotNull(manifestAndConfig.getConfig()); + if (isSingleManifest) { + // If base image is not a manifest list, check and warn misconfigured platforms. + PlatformChecker.checkManifestPlatform(buildContext, containerConfig); + } + return Optional.of( + JsonToImageTranslator.toImage((BuildableManifestTemplate) manifest, containerConfig)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 928d997467..291a6bd153 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,76 +16,77 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Pushes a BLOB to the target registry. */ -class PushBlobStep implements AsyncStep, Callable { +class PushBlobStep implements Callable { private static final String DESCRIPTION = "Pushing BLOB "; - private final BuildConfiguration buildConfiguration; - private final AuthenticatePushStep authenticatePushStep; + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + private final RegistryClient registryClient; private final BlobDescriptor blobDescriptor; private final Blob blob; - - private final ListenableFuture listenableFuture; + private final boolean forcePush; PushBlobStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - AuthenticatePushStep authenticatePushStep, + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, BlobDescriptor blobDescriptor, - Blob blob) { - this.buildConfiguration = buildConfiguration; - this.authenticatePushStep = authenticatePushStep; + Blob blob, + boolean forcePush) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.registryClient = registryClient; this.blobDescriptor = blobDescriptor; this.blob = blob; - - listenableFuture = - Futures.whenAllSucceed(authenticatePushStep.getFuture()) - .call(this, listeningExecutorService); + this.forcePush = forcePush; } @Override - public ListenableFuture getFuture() { - return listenableFuture; - } + public BlobDescriptor call() throws IOException, RegistryException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + DescriptorDigest blobDigest = blobDescriptor.getDigest(); + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "pushing blob " + blobDigest, blobDescriptor.getSize()); + TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, DESCRIPTION + blobDescriptor); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { - @Override - public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { - try (Timer timer = - new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION + blobDescriptor)) { - RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) - .newRegistryClient(); - registryClient.setTimer(timer); - - if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { - buildConfiguration - .getBuildLogger() - .info("BLOB : " + blobDescriptor + " already exists on registry"); + // check if the BLOB is available + if (!forcePush && registryClient.checkBlob(blobDigest).isPresent()) { + eventHandlers.dispatch( + LogEvent.info( + "Skipping push; BLOB already exists on target registry : " + blobDescriptor)); return blobDescriptor; } - registryClient.pushBlob(blobDescriptor.getDigest(), blob); - + // If base and target images are in the same registry, then use mount/from to try mounting the + // BLOB from the base image repository to the target image repository and possibly avoid + // having to push the BLOB. See + // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. + String baseRegistry = buildContext.getBaseImageConfiguration().getImageRegistry(); + String baseRepository = buildContext.getBaseImageConfiguration().getImageRepository(); + String targetRegistry = buildContext.getTargetImageConfiguration().getImageRegistry(); + String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; + registryClient.pushBlob(blobDigest, blob, sourceRepository, throttledProgressReporter); return blobDescriptor; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 12529f2f5d..24bd87dec4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,83 +16,59 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.common.io.ByteStreams; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Pushes the container configuration. */ -class PushContainerConfigurationStep - implements AsyncStep>, Callable> { +class PushContainerConfigurationStep implements Callable { private static final String DESCRIPTION = "Pushing container configuration"; - private final BuildConfiguration buildConfiguration; - private final AuthenticatePushStep authenticatePushStep; - private final BuildImageStep buildImageStep; + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture> listenableFuture; + private final RegistryClient registryClient; + private final Image builtImage; PushContainerConfigurationStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - AuthenticatePushStep authenticatePushStep, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.authenticatePushStep = authenticatePushStep; - this.buildImageStep = buildImageStep; - - listenableFuture = - Futures.whenAllSucceed(buildImageStep.getFuture()).call(this, listeningExecutorService); - } - - @Override - public ListenableFuture> getFuture() { - return listenableFuture; + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + Image builtImage) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.registryClient = registryClient; + this.builtImage = builtImage; } @Override - public AsyncStep call() throws ExecutionException { - ListenableFuture pushBlobStepFuture = - Futures.whenAllSucceed( - authenticatePushStep.getFuture(), NonBlockingSteps.get(buildImageStep).getFuture()) - .call(this::afterBuildConfigurationFutureFuture, listeningExecutorService); - return () -> pushBlobStepFuture; - } - - private PushBlobStep afterBuildConfigurationFutureFuture() - throws ExecutionException, IOException { - try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - Blob containerConfigurationBlob = - new ImageToJsonTranslator(image).getContainerConfigurationBlob(); - CountingDigestOutputStream digestOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - containerConfigurationBlob.writeTo(digestOutputStream); - - BlobDescriptor blobDescriptor = digestOutputStream.toBlobDescriptor(); + public BlobDescriptor call() throws IOException, RegistryException { + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("pushing container configuration", 1); + TimerEventDispatcher ignored = + new TimerEventDispatcher(buildContext.getEventHandlers(), DESCRIPTION)) { + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(builtImage).getContainerConfiguration(); return new PushBlobStep( - listeningExecutorService, - buildConfiguration, - authenticatePushStep, - blobDescriptor, - containerConfigurationBlob); + buildContext, + progressEventDispatcher.newChildProducer(), + registryClient, + Digests.computeDigest(containerConfiguration), + Blobs.from(containerConfiguration), + false) + .call(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 6e15f6c49a..04861253f1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,127 +16,171 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; -/** Pushes the final image. */ -class PushImageStep implements AsyncStep, Callable { - - private static final String DESCRIPTION = "Pushing new image"; - - private final BuildConfiguration buildConfiguration; - private final AuthenticatePushStep authenticatePushStep; - - private final PushLayersStep pushBaseImageLayersStep; - private final PushLayersStep pushApplicationLayersStep; - private final PushContainerConfigurationStep pushContainerConfigurationStep; - private final BuildImageStep buildImageStep; - - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; - - PushImageStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - AuthenticatePushStep authenticatePushStep, - PushLayersStep pushBaseImageLayersStep, - PushLayersStep pushApplicationLayersStep, - PushContainerConfigurationStep pushContainerConfigurationStep, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.authenticatePushStep = authenticatePushStep; - - this.pushBaseImageLayersStep = pushBaseImageLayersStep; - this.pushApplicationLayersStep = pushApplicationLayersStep; - this.pushContainerConfigurationStep = pushContainerConfigurationStep; - this.buildImageStep = buildImageStep; - - listenableFuture = - Futures.whenAllSucceed( - pushBaseImageLayersStep.getFuture(), - pushApplicationLayersStep.getFuture(), - pushContainerConfigurationStep.getFuture()) - .call(this, listeningExecutorService); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; +/** + * Pushes a manifest or a manifest list for a tag. If not a manifest list, returns the manifest + * digest ("image digest") and the container configuration digest ("image id") as {@link + * BuildResult}. If a manifest list, returns the manifest list digest only. + */ +// TODO: figure out the right return value and type when pushing a manifest list. +class PushImageStep implements Callable { + + private static final String DESCRIPTION = "Pushing manifest"; + + static ImmutableList makeList( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + BlobDescriptor containerConfigurationDigestAndSize, + Image builtImage, + boolean manifestAlreadyExists) + throws IOException { + // Gets the image manifest to push. + BuildableManifestTemplate manifestTemplate = + new ImageToJsonTranslator(builtImage) + .getManifestTemplate( + buildContext.getTargetFormat(), containerConfigurationDigestAndSize); + DescriptorDigest manifestDigest = Digests.computeJsonDigest(manifestTemplate); + Set imageQualifiers = getImageQualifiers(buildContext, builtImage, manifestDigest); + + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, "Preparing manifest pushers"); + ProgressEventDispatcher progressDispatcher = + progressEventDispatcherFactory.create( + "launching manifest pushers", imageQualifiers.size())) { + + if (JibSystemProperties.skipExistingImages() && manifestAlreadyExists) { + eventHandlers.dispatch(LogEvent.info("Skipping pushing manifest; already exists.")); + return ImmutableList.of(); + } + + return imageQualifiers.stream() + .map( + qualifier -> + new PushImageStep( + buildContext, + progressDispatcher.newChildProducer(), + registryClient, + manifestTemplate, + qualifier, + manifestDigest, + containerConfigurationDigestAndSize.getDigest())) + .collect(ImmutableList.toImmutableList()); + } } - @Override - public Void call() throws ExecutionException, InterruptedException { - ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); - dependenciesBuilder.add(authenticatePushStep.getFuture()); - for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { - dependenciesBuilder.add(pushBlobStepStep.getFuture()); + private static Set getImageQualifiers( + BuildContext buildContext, Image builtImage, DescriptorDigest manifestDigest) { + boolean singlePlatform = buildContext.getContainerConfiguration().getPlatforms().size() == 1; + Set tags = buildContext.getAllTargetImageTags(); + if (singlePlatform) { + return tags; } - for (AsyncStep pushBlobStepStep : - NonBlockingSteps.get(pushApplicationLayersStep)) { - dependenciesBuilder.add(pushBlobStepStep.getFuture()); + if (buildContext.getEnablePlatformTags()) { + String architecture = builtImage.getArchitecture(); + return tags.stream().map(tag -> tag + "-" + architecture).collect(Collectors.toSet()); } - dependenciesBuilder.add(NonBlockingSteps.get(pushContainerConfigurationStep).getFuture()); - dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); - return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::afterPushSteps, listeningExecutorService) - .get() - .get(); + return Collections.singleton(manifestDigest.toString()); } - private ListenableFuture afterPushSteps() throws ExecutionException { - List> dependencies = new ArrayList<>(); - for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { - dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); - } - for (AsyncStep pushBlobStepStep : - NonBlockingSteps.get(pushApplicationLayersStep)) { - dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); + static ImmutableList makeListForManifestList( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + ManifestTemplate manifestList, + boolean manifestListAlreadyExists) + throws IOException { + Set tags = buildContext.getAllTargetImageTags(); + + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, "Preparing manifest list pushers"); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("launching manifest list pushers", tags.size())) { + boolean singlePlatform = buildContext.getContainerConfiguration().getPlatforms().size() == 1; + if (singlePlatform) { + return ImmutableList.of(); // single image; no need to push a manifest list + } + + if (JibSystemProperties.skipExistingImages() && manifestListAlreadyExists) { + eventHandlers.dispatch(LogEvent.info("Skipping pushing manifest list; already exists.")); + return ImmutableList.of(); + } + DescriptorDigest manifestListDigest = Digests.computeJsonDigest(manifestList); + return tags.stream() + .map( + tag -> + new PushImageStep( + buildContext, + progressEventDispatcher.newChildProducer(), + registryClient, + manifestList, + tag, + manifestListDigest, + // TODO: a manifest list digest isn't an "image id". Figure out the right + // return value and type. + manifestListDigest)) + .collect(ImmutableList.toImmutableList()); } - dependencies.add( - NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)).getFuture()); - return Futures.whenAllSucceed(dependencies) - .call(this::afterAllPushed, listeningExecutorService); } - private Void afterAllPushed() throws IOException, RegistryException, ExecutionException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) - .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) - .newRegistryClient(); - - // Constructs the image. - ImageToJsonTranslator imageToJsonTranslator = - new ImageToJsonTranslator(NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep))); - - // Pushes the image manifest. - BuildableManifestTemplate manifestTemplate = - imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), - NonBlockingSteps.get( - NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)))); - registryClient.pushManifest(manifestTemplate, buildConfiguration.getTargetImageTag()); - } + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final RegistryClient registryClient; + private final ManifestTemplate manifestTemplate; + private final String imageQualifier; + private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; - return null; + PushImageStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + ManifestTemplate manifestTemplate, + String imageQualifier, + DescriptorDigest imageDigest, + DescriptorDigest imageId) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.registryClient = registryClient; + this.manifestTemplate = manifestTemplate; + this.imageQualifier = imageQualifier; + this.imageDigest = imageDigest; + this.imageId = imageId; + } + + @Override + public BuildResult call() throws IOException, RegistryException { + EventHandlers eventHandlers = buildContext.getEventHandlers(); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION); + ProgressEventDispatcher ignored2 = + progressEventDispatcherFactory.create("pushing manifest for " + imageQualifier, 1)) { + eventHandlers.dispatch(LogEvent.info("Pushing manifest for " + imageQualifier + "...")); + + registryClient.pushManifest(manifestTemplate, imageQualifier); + return new BuildResult(imageDigest, imageId, true); + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java new file mode 100644 index 0000000000..b1974c5ce5 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +class PushLayerStep implements Callable { + + static ImmutableList makeList( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + List> cachedLayers) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildContext.getEventHandlers(), "Preparing layer pushers"); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("launching layer pushers", cachedLayers.size())) { + + // Constructs a PushBlobStep for each layer. + return cachedLayers.stream() + .map( + layer -> + new PushLayerStep( + buildContext, + progressEventDispatcher.newChildProducer(), + registryClient, + layer)) + .collect(ImmutableList.toImmutableList()); + } + } + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + private final RegistryClient registryClient; + private final Future preparedLayer; + + private PushLayerStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + RegistryClient registryClient, + Future preparedLayer) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.registryClient = registryClient; + this.preparedLayer = preparedLayer; + } + + @Override + public BlobDescriptor call() + throws IOException, RegistryException, ExecutionException, InterruptedException { + PreparedLayer layer = preparedLayer.get(); + + if (layer.getStateInTarget() == StateInTarget.EXISTING) { + return layer.getBlobDescriptor(); // skip pushing if known to exist in registry + } + + boolean forcePush = layer.getStateInTarget() == StateInTarget.MISSING; + return new PushBlobStep( + buildContext, + progressEventDispatcherFactory, + registryClient, + layer.getBlobDescriptor(), + layer.getBlob(), + forcePush) + .call(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java deleted file mode 100644 index 76f139bd4a..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -class PushLayersStep - implements AsyncStep>>, - Callable>> { - - private static final String DESCRIPTION = "Setting up to push layers"; - - private final BuildConfiguration buildConfiguration; - private final AuthenticatePushStep authenticatePushStep; - private final AsyncStep>> - cachedLayerStepsStep; - - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture>> listenableFuture; - - PushLayersStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - AuthenticatePushStep authenticatePushStep, - AsyncStep>> - cachedLayerStepsStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.authenticatePushStep = authenticatePushStep; - this.cachedLayerStepsStep = cachedLayerStepsStep; - - listenableFuture = - Futures.whenAllSucceed(cachedLayerStepsStep.getFuture()) - .call(this, listeningExecutorService); - } - - @Override - public ListenableFuture>> getFuture() { - return listenableFuture; - } - - @Override - public ImmutableList> call() throws ExecutionException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - ImmutableList> cachedLayerSteps = - NonBlockingSteps.get(cachedLayerStepsStep); - - // Constructs a PushBlobStep for each layer. - ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); - for (AsyncStep cachedLayerStep : cachedLayerSteps) { - ListenableFuture pushBlobStepFuture = - Futures.whenAllSucceed(cachedLayerStep.getFuture()) - .call(() -> makePushBlobStep(cachedLayerStep), listeningExecutorService); - pushBlobStepsBuilder.add(() -> pushBlobStepFuture); - } - - return pushBlobStepsBuilder.build(); - } - } - - private PushBlobStep makePushBlobStep(AsyncStep cachedLayerStep) - throws ExecutionException { - CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep); - return new PushBlobStep( - listeningExecutorService, - buildConfiguration, - authenticatePushStep, - cachedLayer.getBlobDescriptor(), - cachedLayer.getBlob()); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java new file mode 100644 index 0000000000..4b293c91e9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import java.util.Optional; + +/** Attempts to retrieve registry credentials. */ +class RegistryCredentialRetriever { + + private RegistryCredentialRetriever() {} + + /** Retrieves credentials for the base image. */ + static Optional getBaseImageCredential(BuildContext buildContext) + throws CredentialRetrievalException { + return retrieve(buildContext.getBaseImageConfiguration(), buildContext.getEventHandlers()); + } + + /** Retrieves credentials for the target image. */ + static Optional getTargetImageCredential(BuildContext buildContext) + throws CredentialRetrievalException { + return retrieve(buildContext.getTargetImageConfiguration(), buildContext.getEventHandlers()); + } + + private static Optional retrieve( + ImageConfiguration imageConfiguration, EventHandlers eventHandlers) + throws CredentialRetrievalException { + for (CredentialRetriever retriever : imageConfiguration.getCredentialRetrievers()) { + Optional credential = retriever.retrieve(); + if (credential.isPresent()) { + return credential; + } + } + + String registry = imageConfiguration.getImageRegistry(); + String repository = imageConfiguration.getImageRepository(); + eventHandlers.dispatch( + LogEvent.info("No credentials could be retrieved for " + registry + "/" + repository)); + return Optional.empty(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java deleted file mode 100644 index 4dcccec735..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.Timer; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; -import java.util.concurrent.Callable; -import javax.annotation.Nullable; - -/** Attempts to retrieve registry credentials. */ -class RetrieveRegistryCredentialsStep implements AsyncStep, Callable { - - private static final String DESCRIPTION = "Retrieving registry credentials for %s"; - - /** - * Defines common credential helpers to use as defaults. Maps from registry suffix to credential - * helper suffix. - */ - private static final ImmutableMap COMMON_CREDENTIAL_HELPERS = - ImmutableMap.of("gcr.io", "gcr", "amazonaws.com", "ecr-login"); - - /** Retrieves credentials for the base image. */ - static RetrieveRegistryCredentialsStep forBaseImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { - return new RetrieveRegistryCredentialsStep( - listeningExecutorService, - buildConfiguration.getBuildLogger(), - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageCredentialHelperName(), - buildConfiguration.getKnownBaseRegistryCredentials()); - } - - /** Retrieves credentials for the target image. */ - static RetrieveRegistryCredentialsStep forTargetImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { - return new RetrieveRegistryCredentialsStep( - listeningExecutorService, - buildConfiguration.getBuildLogger(), - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageCredentialHelperName(), - buildConfiguration.getKnownTargetRegistryCredentials()); - } - - private final BuildLogger buildLogger; - private final String registry; - @Nullable private final String credentialHelperSuffix; - @Nullable private final RegistryCredentials knownRegistryCredentials; - private final DockerCredentialHelperFactory dockerCredentialHelperFactory; - private final DockerConfigCredentialRetriever dockerConfigCredentialRetriever; - - private final ListenableFuture listenableFuture; - - @VisibleForTesting - RetrieveRegistryCredentialsStep( - ListeningExecutorService listeningExecutorService, - BuildLogger buildLogger, - String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials, - DockerCredentialHelperFactory dockerCredentialHelperFactory, - DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { - this.buildLogger = buildLogger; - this.registry = registry; - this.credentialHelperSuffix = credentialHelperSuffix; - this.knownRegistryCredentials = knownRegistryCredentials; - this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; - this.dockerConfigCredentialRetriever = dockerConfigCredentialRetriever; - - listenableFuture = listeningExecutorService.submit(this); - } - - /** Instantiate with {@link #forBaseImage} or {@link #forTargetImage}. */ - private RetrieveRegistryCredentialsStep( - ListeningExecutorService listeningExecutorService, - BuildLogger buildLogger, - String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials) { - this( - listeningExecutorService, - buildLogger, - registry, - credentialHelperSuffix, - knownRegistryCredentials, - new DockerCredentialHelperFactory(registry), - new DockerConfigCredentialRetriever(registry)); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; - } - - @Override - @Nullable - public Authorization call() throws IOException, NonexistentDockerCredentialHelperException { - buildLogger.lifecycle(String.format(DESCRIPTION, registry) + "..."); - - try (Timer ignored = new Timer(buildLogger, String.format(DESCRIPTION, registry))) { - // Tries to get registry credentials from Docker credential helpers. - if (credentialHelperSuffix != null) { - Authorization authorization = retrieveFromCredentialHelper(credentialHelperSuffix); - if (authorization != null) { - return authorization; - } - } - - // Tries to get registry credentials from known registry credentials. - if (knownRegistryCredentials != null) { - logGotCredentialsFrom(knownRegistryCredentials.getCredentialSource()); - return knownRegistryCredentials.getAuthorization(); - } - - // Tries to infer common credential helpers for known registries. - for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) { - if (registry.endsWith(registrySuffix)) { - try { - String commonCredentialHelper = COMMON_CREDENTIAL_HELPERS.get(registrySuffix); - if (commonCredentialHelper == null) { - throw new IllegalStateException("No COMMON_CREDENTIAL_HELPERS should be null"); - } - Authorization authorization = retrieveFromCredentialHelper(commonCredentialHelper); - if (authorization != null) { - return authorization; - } - - } catch (NonexistentDockerCredentialHelperException ex) { - if (ex.getMessage() != null) { - // Warns the user that the specified (or inferred) credential helper is not on the - // system. - buildLogger.warn(ex.getMessage()); - } - } - } - } - - // Tries to get registry credentials from the Docker config. - try { - Authorization dockerConfigAuthorization = dockerConfigCredentialRetriever.retrieve(); - if (dockerConfigAuthorization != null) { - buildLogger.info("Using credentials from Docker config for " + registry); - return dockerConfigAuthorization; - } - - } catch (IOException ex) { - buildLogger.info("Unable to parse Docker config"); - } - - /* - * If no credentials found, give an info (not warning because in most cases, the base image is - * public and does not need extra credentials) and return null. - */ - buildLogger.info("No credentials could be retrieved for registry " + registry); - return null; - } - } - - /** - * Attempts to retrieve authorization for the registry using {@code - * docker-credential-[credentialHelperSuffix]}. - */ - @VisibleForTesting - @Nullable - Authorization retrieveFromCredentialHelper(String credentialHelperSuffix) - throws NonexistentDockerCredentialHelperException, IOException { - buildLogger.info("Checking credentials from docker-credential-" + credentialHelperSuffix); - - try { - Authorization authorization = - dockerCredentialHelperFactory - .withCredentialHelperSuffix(credentialHelperSuffix) - .retrieve(); - logGotCredentialsFrom("docker-credential-" + credentialHelperSuffix); - return authorization; - - } catch (NonexistentServerUrlDockerCredentialHelperException ex) { - buildLogger.info( - "No credentials for " + registry + " in docker-credential-" + credentialHelperSuffix); - return null; - } - } - - private void logGotCredentialsFrom(String credentialSource) { - buildLogger.info("Using " + credentialSource + " for " + registry); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 1ec77262be..4cfeb5c173 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,229 +16,673 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.AsyncSteps; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** * Runs steps for building an image. * - *

    Use by constructing the runner and calling {@code run...} each step. Make sure that steps are - * run before other steps that depend on them. Wait on the last step. + *

    Use by first calling {@link #begin} and then calling the individual step running methods. Note + * that order matters, so make sure that steps are run before other steps that depend on them. Wait + * on the last step by calling the respective {@code wait...} methods. */ public class StepsRunner { - private final ListeningExecutorService listeningExecutorService = - MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); - private final BuildConfiguration buildConfiguration; - private final SourceFilesConfiguration sourceFilesConfiguration; - private final Cache baseLayersCache; - private final Cache applicationLayersCache; - - @Nullable private RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; - @Nullable private AuthenticatePushStep authenticatePushStep; - @Nullable private PullBaseImageStep pullBaseImageStep; - @Nullable private PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - - @Nullable - private ImmutableList buildAndCacheApplicationLayerSteps; - - @Nullable private PushLayersStep pushBaseImageLayersStep; - @Nullable private PushLayersStep pushApplicationLayersStep; - @Nullable private BuildImageStep buildImageStep; - @Nullable private PushContainerConfigurationStep pushContainerConfigurationStep; - @Nullable private PushImageStep pushImageStep; - @Nullable private LoadDockerStep loadDockerStep; - - public StepsRunner( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Cache baseLayersCache, - Cache applicationLayersCache) { - this.buildConfiguration = buildConfiguration; - this.sourceFilesConfiguration = sourceFilesConfiguration; - this.baseLayersCache = baseLayersCache; - this.applicationLayersCache = applicationLayersCache; - } - - public StepsRunner runRetrieveTargetRegistryCredentialsStep() { - retrieveTargetRegistryCredentialsStep = - RetrieveRegistryCredentialsStep.forTargetImage( - listeningExecutorService, buildConfiguration); - return this; + /** Holds the individual step results. */ + private static class StepResults { + + private static Future failedFuture() { + return Futures.immediateFailedFuture( + new IllegalStateException("invalid usage; required step not configured")); + } + + @Nullable private List> applicationLayers; + private Future manifestListOrSingleManifest = failedFuture(); + private Future targetRegistryClient = failedFuture(); + private Future>> applicationLayerPushResults = failedFuture(); + private Future>> manifestCheckResult = + failedFuture(); + private Future>> imagePushResults = failedFuture(); + private Future buildResult = failedFuture(); + + private Future baseImagesAndRegistryClient = failedFuture(); + private Future>>> baseImagesAndLayers = failedFuture(); + private Future>>> baseImagesAndLayerPushResults = + failedFuture(); + private Future>> baseImagesAndContainerConfigPushResults = + failedFuture(); + private Future>> baseImagesAndBuiltImages = failedFuture(); } - public StepsRunner runAuthenticatePushStep() { - authenticatePushStep = - new AuthenticatePushStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(retrieveTargetRegistryCredentialsStep)); - return this; + /** + * Starts building the steps to run. + * + * @param buildContext the {@link BuildContext} + * @return a new {@link StepsRunner} + */ + public static StepsRunner begin(BuildContext buildContext) { + ExecutorService executorService = + JibSystemProperties.serializeExecution() + ? MoreExecutors.newDirectExecutorService() + : buildContext.getExecutorService(); + + return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildContext); } - public StepsRunner runPullBaseImageStep() { - pullBaseImageStep = new PullBaseImageStep(listeningExecutorService, buildConfiguration); - return this; + private static List realizeFutures(Collection> futures) + throws InterruptedException, ExecutionException { + List values = new ArrayList<>(); + for (Future future : futures) { + values.add(future.get()); + } + return values; } - public StepsRunner runPullAndCacheBaseImageLayersStep() { - pullAndCacheBaseImageLayersStep = - new PullAndCacheBaseImageLayersStep( - listeningExecutorService, - buildConfiguration, - baseLayersCache, - Preconditions.checkNotNull(pullBaseImageStep)); - return this; + private final StepResults results = new StepResults(); + + private final ExecutorService executorService; + private final BuildContext buildContext; + private final TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + + // Instead of directly running each step, we first save them as a lambda. This is only because of + // the unfortunate chicken-and-egg situation when using ProgressEventDispatcher. The current + // ProgressEventDispatcher model requires allocating the total units of work (i.e., steps) + // up front. That is, to instantiate a root ProgressEventDispatcher, we should know ahead how many + // steps we will run. However, to run a step, we need a root progress dispatcher. So, we take each + // step as a lambda and save them to run later. Then we can count the number of lambdas, create a + // root dispatcher with the count, and run the saved lambdas using the dispatcher. + private final List> stepsToRun = new ArrayList<>(); + + @Nullable private String rootProgressDescription; + + @VisibleForTesting + StepsRunner(ListeningExecutorService executorService, BuildContext buildContext) { + this.executorService = executorService; + this.buildContext = buildContext; } - public StepsRunner runPushBaseImageLayersStep() { - pushBaseImageLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep)); + /** + * Add steps for loading an image to docker daemon. + * + * @param dockerClient the docker client to load the image to + * @return this + */ + public StepsRunner dockerLoadSteps(DockerClient dockerClient) { + rootProgressDescription = "building image to Docker daemon"; + + addRetrievalSteps(true); // always pull layers for docker builds + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImages); + + // load to Docker + stepsToRun.add( + progressDispatcherFactory -> loadDocker(dockerClient, progressDispatcherFactory)); return this; } - public StepsRunner runBuildAndCacheApplicationLayerSteps() { - buildAndCacheApplicationLayerSteps = - BuildAndCacheApplicationLayerStep.makeList( - listeningExecutorService, - buildConfiguration, - sourceFilesConfiguration, - applicationLayersCache); + /** + * Add steps for writing an image as a tar file archive. + * + * @param outputPath the target file path to write the image to + * @return this + */ + public StepsRunner tarBuildSteps(Path outputPath) { + rootProgressDescription = "building image to tar file"; + + addRetrievalSteps(true); // always pull layers for tar builds + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImages); + + // create a tar + stepsToRun.add( + progressDispatcherFactory -> writeTarFile(outputPath, progressDispatcherFactory)); return this; } - public StepsRunner runBuildImageStep(ImmutableList entrypoint) { - buildImageStep = - new BuildImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), - entrypoint); + /** + * Add steps for pushing images to a remote registry. The registry is determined by the image + * name. + * + * @return this + */ + public StepsRunner registryPushSteps() { + rootProgressDescription = "building images to registry"; + boolean layersRequiredLocally = buildContext.getAlwaysCacheBaseImage(); + + stepsToRun.add(this::authenticateBearerPush); + + addRetrievalSteps(layersRequiredLocally); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImages); + stepsToRun.add(this::buildManifestListOrSingleManifest); + + // push to registry + stepsToRun.add(this::pushBaseImagesLayers); + stepsToRun.add(this::pushApplicationLayers); + stepsToRun.add(this::pushContainerConfigurations); + stepsToRun.add(this::checkManifestInTargetRegistry); + stepsToRun.add(this::pushImages); + stepsToRun.add(this::pushManifestList); return this; } - public StepsRunner runPushContainerConfigurationStep() { - pushContainerConfigurationStep = - new PushContainerConfigurationStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(buildImageStep)); - return this; + /** + * Run all steps and return a BuildResult after a build is completed. + * + * @return a {@link BuildResult} with build metadata + * @throws ExecutionException if an error occurred during asynchronous execution of steps + * @throws InterruptedException if the build was interrupted while waiting for results + */ + public BuildResult run() throws ExecutionException, InterruptedException { + Preconditions.checkNotNull(rootProgressDescription); + + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot( + buildContext.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { + stepsToRun.forEach(step -> step.accept(progressEventDispatcher.newChildProducer())); + return results.buildResult.get(); + + } catch (ExecutionException ex) { + ExecutionException unrolled = ex; + while (unrolled.getCause() instanceof ExecutionException) { + unrolled = (ExecutionException) unrolled.getCause(); + } + throw unrolled; + + } finally { + tempDirectoryProvider.close(); + } } - public StepsRunner runPushApplicationLayersStep() { - pushApplicationLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - AsyncSteps.immediate(Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps))); - return this; + private void addRetrievalSteps(boolean layersRequiredLocally) { + ImageConfiguration baseImageConfiguration = buildContext.getBaseImageConfiguration(); + + if (baseImageConfiguration.getTarPath().isPresent()) { + // If tarPath is present, a TarImage was used + stepsToRun.add(this::extractTar); + + } else if (baseImageConfiguration.getDockerClient().isPresent()) { + // If dockerClient is present, a DockerDaemonImage was used + stepsToRun.add(this::saveDocker); + + } else { + // Otherwise default to RegistryImage + stepsToRun.add(this::pullBaseImages); + stepsToRun.add( + progressDispatcherFactory -> + obtainBaseImagesLayers(layersRequiredLocally, progressDispatcherFactory)); + } } - public StepsRunner runFinalizingPushStep() { - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Arrays.asList( - Preconditions.checkNotNull(pushBaseImageLayersStep), - Preconditions.checkNotNull(pushApplicationLayersStep)), - Collections.emptyList()); - return this; + private void authenticateBearerPush(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.targetRegistryClient = + executorService.submit(new AuthenticatePushStep(buildContext, progressDispatcherFactory)); } - public StepsRunner runFinalizingBuildStep() { - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Collections.singletonList(Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep)), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps)); - return this; + private void saveDocker(ProgressEventDispatcher.Factory progressDispatcherFactory) { + Optional dockerClient = + buildContext.getBaseImageConfiguration().getDockerClient(); + Preconditions.checkArgument(dockerClient.isPresent()); + + assignLocalImageResult( + executorService.submit( + LocalBaseImageSteps.retrieveDockerDaemonLayersStep( + buildContext, + progressDispatcherFactory, + dockerClient.get(), + tempDirectoryProvider))); } - public StepsRunner runPushImageStep() { - pushImageStep = - new PushImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(pushBaseImageLayersStep), - Preconditions.checkNotNull(pushApplicationLayersStep), - Preconditions.checkNotNull(pushContainerConfigurationStep), - Preconditions.checkNotNull(buildImageStep)); - return this; + private void extractTar(ProgressEventDispatcher.Factory progressDispatcherFactory) { + Optional tarPath = buildContext.getBaseImageConfiguration().getTarPath(); + Preconditions.checkArgument(tarPath.isPresent()); + + assignLocalImageResult( + executorService.submit( + LocalBaseImageSteps.retrieveTarLayersStep( + buildContext, progressDispatcherFactory, tarPath.get(), tempDirectoryProvider))); } - public StepsRunner runLoadDockerStep() { - loadDockerStep = - new LoadDockerStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), - Preconditions.checkNotNull(buildImageStep)); - return this; + private void assignLocalImageResult(Future localImage) { + results.baseImagesAndRegistryClient = + executorService.submit( + () -> + LocalBaseImageSteps.returnImageAndRegistryClientStep( + realizeFutures(localImage.get().layers), + localImage.get().configurationTemplate) + .call()); + + results.baseImagesAndLayers = + executorService.submit( + () -> + Collections.singletonMap( + results.baseImagesAndRegistryClient.get().images.get(0), + localImage.get().layers)); } - public void waitOnPushImageStep() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(pushImageStep).getFuture().get(); + @VisibleForTesting + void pullBaseImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.baseImagesAndRegistryClient = + executorService.submit(new PullBaseImageStep(buildContext, progressDispatcherFactory)); } - public void waitOnLoadDockerStep() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(loadDockerStep).getFuture().get(); + private void obtainBaseImagesLayers( + boolean layersRequiredLocally, ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.baseImagesAndLayers = + executorService.submit( + () -> { + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "scheduling obtaining base images layers", + results.baseImagesAndRegistryClient.get().images.size())) { + + Map> preparedLayersCache = new HashMap<>(); + Map>> baseImagesAndLayers = new LinkedHashMap<>(); + for (Image baseImage : results.baseImagesAndRegistryClient.get().images) { + List> layers = + obtainBaseImageLayers( + baseImage, + layersRequiredLocally, + preparedLayersCache, + progressDispatcher.newChildProducer()); + baseImagesAndLayers.put(baseImage, layers); + } + return baseImagesAndLayers; + } + }); } - /** - * @return the layers cached by {@link #pullAndCacheBaseImageLayersStep} - * @throws ExecutionException if {@link #pullAndCacheBaseImageLayersStep} threw an exception - * during execution - */ - public List getCachedBaseImageLayers() throws ExecutionException { - ImmutableList pullAndCacheBaseImageLayerSteps = - NonBlockingSteps.get(Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep)); - - List cachedLayers = new ArrayList<>(pullAndCacheBaseImageLayerSteps.size()); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - pullAndCacheBaseImageLayerSteps) { - cachedLayers.add(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)); + // This method updates the given "preparedLayersCache" and should not be called concurrently. + @VisibleForTesting + List> obtainBaseImageLayers( + Image baseImage, + boolean layersRequiredLocally, + Map> preparedLayersCache, + ProgressEventDispatcher.Factory progressDispatcherFactory) + throws InterruptedException, ExecutionException { + List> preparedLayers = new ArrayList<>(); + + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "launching base image layer pullers", baseImage.getLayers().size())) { + for (Layer layer : baseImage.getLayers()) { + DescriptorDigest digest = layer.getBlobDescriptor().getDigest(); + Future preparedLayer = preparedLayersCache.get(digest); + + if (preparedLayer != null) { + progressDispatcher.dispatchProgress(1); + } else { // If we haven't obtained this layer yet, launcher a puller. + preparedLayer = + executorService.submit( + layersRequiredLocally + ? ObtainBaseImageLayerStep.forForcedDownload( + buildContext, + progressDispatcher.newChildProducer(), + layer, + results.baseImagesAndRegistryClient.get().registryClient) + : ObtainBaseImageLayerStep.forSelectiveDownload( + buildContext, + progressDispatcher.newChildProducer(), + layer, + results.baseImagesAndRegistryClient.get().registryClient, + results.targetRegistryClient.get())); + preparedLayersCache.put(digest, preparedLayer); + } + preparedLayers.add(preparedLayer); + } + return preparedLayers; } - return cachedLayers; } - /** - * @return the layers cached by {@link #buildAndCacheApplicationLayerSteps} - * @throws ExecutionException if {@link #buildAndCacheApplicationLayerSteps} threw an exception - * during execution - */ - public List getCachedApplicationLayers() throws ExecutionException { - List cachedLayersWithMetadata = - new ArrayList<>(Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps).size()); - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - cachedLayersWithMetadata.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); + private void pushBaseImagesLayers(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.baseImagesAndLayerPushResults = + executorService.submit( + () -> { + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "scheduling pushing base images layers", + results.baseImagesAndLayers.get().size())) { + + Map>> layerPushResults = new LinkedHashMap<>(); + for (Map.Entry>> entry : + results.baseImagesAndLayers.get().entrySet()) { + Image baseImage = entry.getKey(); + List> baseLayers = entry.getValue(); + + List> pushResults = + pushBaseImageLayers(baseLayers, progressDispatcher.newChildProducer()); + layerPushResults.put(baseImage, pushResults); + } + return layerPushResults; + } + }); + } + + private List> pushBaseImageLayers( + List> baseLayers, + ProgressEventDispatcher.Factory progressDispatcherFactory) + throws InterruptedException, ExecutionException { + return scheduleCallables( + PushLayerStep.makeList( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + baseLayers)); + } + + private void buildAndCacheApplicationLayers( + ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.applicationLayers = + scheduleCallables( + BuildAndCacheApplicationLayerStep.makeList(buildContext, progressDispatcherFactory)); + } + + @VisibleForTesting + void buildImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.baseImagesAndBuiltImages = + executorService.submit( + () -> { + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "scheduling building manifests", results.baseImagesAndLayers.get().size())) { + + Map> baseImagesAndBuiltImages = new LinkedHashMap<>(); + for (Map.Entry>> entry : + results.baseImagesAndLayers.get().entrySet()) { + Image baseImage = entry.getKey(); + List> baseLayers = entry.getValue(); + + Future builtImage = + buildImage(baseImage, baseLayers, progressDispatcher.newChildProducer()); + baseImagesAndBuiltImages.put(baseImage, builtImage); + } + return baseImagesAndBuiltImages; + } + }); + } + + private Future buildImage( + Image baseImage, + List> baseLayers, + ProgressEventDispatcher.Factory progressDispatcherFactory) { + return executorService.submit( + () -> + new BuildImageStep( + buildContext, + progressDispatcherFactory, + baseImage, + realizeFutures(baseLayers), + realizeFutures(Verify.verifyNotNull(results.applicationLayers))) + .call()); + } + + private void buildManifestListOrSingleManifest( + ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.manifestListOrSingleManifest = + executorService.submit( + () -> + new BuildManifestListOrSingleManifestStep( + buildContext, + progressDispatcherFactory, + realizeFutures(results.baseImagesAndBuiltImages.get().values())) + .call()); + } + + private void pushContainerConfigurations( + ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.baseImagesAndContainerConfigPushResults = + executorService.submit( + () -> { + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "scheduling pushing container configurations", + results.baseImagesAndBuiltImages.get().size())) { + + Map> configPushResults = new LinkedHashMap<>(); + for (Map.Entry> entry : + results.baseImagesAndBuiltImages.get().entrySet()) { + Image baseImage = entry.getKey(); + Future builtImage = entry.getValue(); + + Future pushResult = + pushContainerConfiguration(builtImage, progressDispatcher.newChildProducer()); + configPushResults.put(baseImage, pushResult); + } + return configPushResults; + } + }); + } + + private Future pushContainerConfiguration( + Future builtImage, ProgressEventDispatcher.Factory progressDispatcherFactory) { + return executorService.submit( + () -> + new PushContainerConfigurationStep( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + builtImage.get()) + .call()); + } + + private void pushApplicationLayers(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.applicationLayerPushResults = + executorService.submit( + () -> + scheduleCallables( + PushLayerStep.makeList( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + Verify.verifyNotNull(results.applicationLayers)))); + } + + private void checkManifestInTargetRegistry( + ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.manifestCheckResult = + executorService.submit( + () -> + new CheckManifestStep( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + results.manifestListOrSingleManifest.get()) + .call()); + } + + private void pushImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.imagePushResults = + executorService.submit( + () -> { + try (ProgressEventDispatcher progressDispatcher = + progressDispatcherFactory.create( + "scheduling pushing manifests", + results.baseImagesAndBuiltImages.get().size())) { + + realizeFutures(results.applicationLayerPushResults.get()); + + List> buildResults = new ArrayList<>(); + for (Map.Entry> entry : + results.baseImagesAndBuiltImages.get().entrySet()) { + Image baseImage = entry.getKey(); + Future builtImage = entry.getValue(); + + buildResults.add( + pushImage(baseImage, builtImage, progressDispatcher.newChildProducer())); + } + return buildResults; + } + }); + } + + private Future pushImage( + Image baseImage, + Future builtImage, + ProgressEventDispatcher.Factory progressDispatcherFactory) { + return executorService.submit( + () -> { + realizeFutures( + Verify.verifyNotNull(results.baseImagesAndLayerPushResults.get().get(baseImage))); + + Future containerConfigPushResult = + results.baseImagesAndContainerConfigPushResults.get().get(baseImage); + + List> manifestPushResults = + scheduleCallables( + PushImageStep.makeList( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + Verify.verifyNotNull(containerConfigPushResult).get(), + builtImage.get(), + results.manifestCheckResult.get().isPresent())); + + realizeFutures(manifestPushResults); + + return manifestPushResults.isEmpty() + ? new BuildResult( + results.manifestCheckResult.get().get().getDigest(), + Verify.verifyNotNull(containerConfigPushResult).get().getDigest(), + isImagePushed(results.manifestCheckResult.get())) + // Manifest pushers return the same BuildResult. + : manifestPushResults.get(0).get(); + }); + } + + @VisibleForTesting + boolean isImagePushed(Optional> manifestResult) { + + return !(JibSystemProperties.skipExistingImages() && manifestResult.isPresent()); + } + + private void pushManifestList(ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.buildResult = + executorService.submit( + () -> { + realizeFutures(results.imagePushResults.get()); + List> manifestListPushResults = + scheduleCallables( + PushImageStep.makeListForManifestList( + buildContext, + progressDispatcherFactory, + results.targetRegistryClient.get(), + results.manifestListOrSingleManifest.get(), + results.manifestCheckResult.get().isPresent())); + + realizeFutures(manifestListPushResults); + return manifestListPushResults.isEmpty() + ? results.imagePushResults.get().get(0).get() + : manifestListPushResults.get(0).get(); + }); + } + + private void loadDocker( + DockerClient dockerClient, ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.buildResult = + executorService.submit( + () -> { + DockerInfoDetails dockerInfoDetails = dockerClient.info(); + String osType = dockerInfoDetails.getOsType(); + String architecture = normalizeArchitecture(dockerInfoDetails.getArchitecture()); + Image builtImage = + fetchBuiltImageForLocalBuild( + osType, architecture, buildContext.getEventHandlers()); + return new LoadDockerStep( + buildContext, progressDispatcherFactory, dockerClient, builtImage) + .call(); + }); + } + + private void writeTarFile( + Path outputPath, ProgressEventDispatcher.Factory progressDispatcherFactory) { + results.buildResult = + executorService.submit( + () -> { + Verify.verify( + results.baseImagesAndBuiltImages.get().size() == 1, + "multi-platform image building not supported when building a local tar image"); + Image builtImage = + results.baseImagesAndBuiltImages.get().values().iterator().next().get(); + + return new WriteTarFileStep( + buildContext, progressDispatcherFactory, outputPath, builtImage) + .call(); + }); + } + + private List> scheduleCallables(ImmutableList> callables) { + return callables.stream().map(executorService::submit).collect(Collectors.toList()); + } + + @VisibleForTesting + String normalizeArchitecture(String architecture) { + // Create mapping based on https://docs.docker.com/engine/install/#supported-platforms + if (architecture.equals("x86_64")) { + return "amd64"; + } else if (architecture.equals("aarch64")) { + return "arm64"; + } + return architecture; + } + + @VisibleForTesting + Image fetchBuiltImageForLocalBuild( + String osType, String architecture, EventHandlers eventHandlers) + throws InterruptedException, ExecutionException { + if (results.baseImagesAndBuiltImages.get().size() > 1) { + eventHandlers.dispatch( + LogEvent.warn( + String.format( + "Detected multi-platform configuration, only building image that matches the local Docker Engine's os and architecture (%s/%s) or " + + "the first platform specified", + osType, architecture))); + for (Map.Entry> imageEntry : + results.baseImagesAndBuiltImages.get().entrySet()) { + Image image = imageEntry.getValue().get(); + if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) { + return image; + } + } } - return cachedLayersWithMetadata; + return results.baseImagesAndBuiltImages.get().values().iterator().next().get(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java new file mode 100644 index 0000000000..26a8de1612 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import com.google.common.base.Preconditions; +import java.io.Closeable; +import javax.annotation.Nullable; + +/** + * Contains a {@link ProgressEventDispatcher} and throttles dispatching progress events with the + * default delay used by {@link ThrottledAccumulatingConsumer}. This class is mutable and should + * only be used within a local context. + * + *

    This class is necessary because the total BLOb size (allocation units) is not known until the + * response headers are received, only after which can the {@link ProgressEventDispatcher} be + * created. + */ +class ThrottledProgressEventDispatcherWrapper implements Closeable { + + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final String description; + @Nullable private ProgressEventDispatcher progressEventDispatcher; + @Nullable private ThrottledAccumulatingConsumer throttledDispatcher; + + ThrottledProgressEventDispatcherWrapper( + ProgressEventDispatcher.Factory progressEventDispatcherFactory, String description) { + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.description = description; + } + + public void dispatchProgress(Long progressUnits) { + Preconditions.checkNotNull(throttledDispatcher); + throttledDispatcher.accept(progressUnits); + } + + @Override + public void close() { + Preconditions.checkNotNull(progressEventDispatcher); + Preconditions.checkNotNull(throttledDispatcher); + throttledDispatcher.close(); + progressEventDispatcher.close(); + } + + void setProgressTarget(long allocationUnits) { + Preconditions.checkState(progressEventDispatcher == null); + progressEventDispatcher = progressEventDispatcherFactory.create(description, allocationUnits); + throttledDispatcher = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java new file mode 100644 index 0000000000..f531917dad --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.ImageTarball; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +public class WriteTarFileStep implements Callable { + + private final BuildContext buildContext; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + private final Path outputPath; + private final Image builtImage; + + WriteTarFileStep( + BuildContext buildContext, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Path outputPath, + Image builtImage) { + this.buildContext = buildContext; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.outputPath = outputPath; + this.builtImage = builtImage; + } + + @Override + public BuildResult call() throws IOException { + buildContext.getEventHandlers().dispatch(LogEvent.progress("Building image to tar file...")); + + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("writing to tar file", 1)) { + // Builds the image to a tarball. + if (outputPath.getParent() != null) { + Files.createDirectories(outputPath.getParent()); + } + try (OutputStream outputStream = + new BufferedOutputStream(Files.newOutputStream(outputPath))) { + new ImageTarball( + builtImage, + buildContext.getTargetImageConfiguration().getImage(), + buildContext.getAllTargetImageTags()) + .writeTo(outputStream); + } + + return BuildResult.fromImage(builtImage, buildContext.getTargetFormat()); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 2721546e46..e46912ecbc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,132 +16,254 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.annotations.VisibleForTesting; -import java.io.BufferedOutputStream; -import java.io.Closeable; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.common.collect.ImmutableList; import java.io.IOException; -import java.io.OutputStream; import java.nio.file.Files; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; +import java.util.Collections; import java.util.List; +import java.util.Optional; +import javax.annotation.concurrent.Immutable; -/** Manages a cache. Implementation is thread-safe. */ -public class Cache implements Closeable { +/** + * Cache for storing data to be shared between Jib executions. + * + *

    This class is immutable and safe to use across threads. + */ +@Immutable +public class Cache { /** - * Initializes a cache with a directory. This also loads the cache metadata if it exists in the - * directory. + * Initializes the cache using {@code cacheDirectory} for storage. * - * @param cacheDirectory the directory to use for the cache. - * @return the initialized cache. - * @throws NotDirectoryException if {@code cacheDirectory} is not a directory. - * @throws CacheMetadataCorruptedException if loading the cache metadata fails. + * @param cacheDirectory the directory for the cache. Creates the directory if it does not exist. + * @return a new {@link Cache} + * @throws CacheDirectoryCreationException if an I/O exception occurs when creating cache + * directory */ - public static Cache init(Path cacheDirectory) - throws NotDirectoryException, CacheMetadataCorruptedException { - if (!Files.isDirectory(cacheDirectory)) { - throw new NotDirectoryException("The cache can only write to a directory"); + public static Cache withDirectory(Path cacheDirectory) throws CacheDirectoryCreationException { + try { + Files.createDirectories(cacheDirectory); + } catch (IOException ex) { + throw new CacheDirectoryCreationException(ex); } - CacheMetadata cacheMetadata = loadCacheMetadata(cacheDirectory); - - return new Cache(cacheDirectory, cacheMetadata); + return new Cache(new CacheStorageFiles(cacheDirectory)); } - private static CacheMetadata loadCacheMetadata(Path cacheDirectory) - throws CacheMetadataCorruptedException { - Path cacheMetadataJsonFile = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); - - if (!Files.exists(cacheMetadataJsonFile)) { - return CacheMetadata.builder().build(); - } - - try { - CacheMetadataTemplate cacheMetadataJson = - JsonTemplateMapper.readJsonFromFile(cacheMetadataJsonFile, CacheMetadataTemplate.class); - return CacheMetadataTranslator.fromTemplate(cacheMetadataJson, cacheDirectory); + private final CacheStorageWriter cacheStorageWriter; + private final CacheStorageReader cacheStorageReader; - } catch (IOException ex) { - // The cache metadata is probably corrupted. - throw new CacheMetadataCorruptedException(ex); - } + private Cache(CacheStorageFiles cacheStorageFiles) { + cacheStorageWriter = new CacheStorageWriter(cacheStorageFiles); + cacheStorageReader = new CacheStorageReader(cacheStorageFiles); } - /** The path to the root of the cache. */ - private final Path cacheDirectory; + /** + * Saves image metadata (a manifest list and a list of manifest/container configuration pairs) for + * an image reference. + * + * @param imageReference the image reference to save the metadata for + * @param metadata the image metadata + * @throws IOException if an I/O exception occurs + */ + public void writeMetadata(ImageReference imageReference, ImageMetadataTemplate metadata) + throws IOException { + cacheStorageWriter.writeMetadata(imageReference, metadata); + } - /** The metadata that corresponds to the cache at {@link #cacheDirectory}. */ - private final CacheMetadata cacheMetadata; + /** + * Saves a schema 2 manifest for an image reference. This is a simple wrapper around {@link + * #writeMetadata(ImageReference, ImageMetadataTemplate)} to save a single manifest without a + * manifest list. + * + * @param imageReference the image reference to save the manifest for + * @param manifest the V2.2 or OCI manifest + * @param containerConfiguration the container configuration + * @throws IOException if an I/O exception occurs + */ + public void writeMetadata( + ImageReference imageReference, + BuildableManifestTemplate manifest, + ContainerConfigurationTemplate containerConfiguration) + throws IOException { + List singleton = + Collections.singletonList(new ManifestAndConfigTemplate(manifest, containerConfiguration)); + cacheStorageWriter.writeMetadata(imageReference, new ImageMetadataTemplate(null, singleton)); + } - /** Builds the updated cache metadata to save back to the cache. */ - private final CacheMetadata.Builder cacheMetadataBuilder; + /** + * Saves a V2.1 image manifest. This is a simple wrapper around {@link + * #writeMetadata(ImageReference, ImageMetadataTemplate)} to save a single manifest without a + * manifest list. + * + * @param imageReference the image reference to save the manifest for + * @param manifestTemplate the V2.1 manifest + * @throws IOException if an I/O exception occurs + */ + public void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) + throws IOException { + List singleton = + Collections.singletonList(new ManifestAndConfigTemplate(manifestTemplate, null)); + cacheStorageWriter.writeMetadata(imageReference, new ImageMetadataTemplate(null, singleton)); + } - private Cache(Path cacheDirectory, CacheMetadata cacheMetadata) { - this.cacheDirectory = cacheDirectory; - this.cacheMetadata = cacheMetadata; - cacheMetadataBuilder = cacheMetadata.newAppendingBuilder(); + /** + * Saves a cache entry with a compressed layer {@link Blob}. Use {@link + * #writeUncompressedLayer(Blob, ImmutableList)} to save a cache entry with an uncompressed layer + * {@link Blob} and include a selector. + * + * @param compressedLayerBlob the compressed layer {@link Blob} + * @return the {@link CachedLayer} for the written layer + * @throws IOException if an I/O exception occurs + */ + public CachedLayer writeCompressedLayer(Blob compressedLayerBlob) throws IOException { + return cacheStorageWriter.writeCompressed(compressedLayerBlob); } /** - * Finishes the use of the cache by flushing any unsaved changes. + * Saves a cache entry with an uncompressed layer {@link Blob} and an additional selector digest. + * Use {@link #writeCompressedLayer(Blob)} to save a compressed layer {@link Blob}. * - * @throws IOException if saving the cache metadata fails. + * @param uncompressedLayerBlob the layer {@link Blob} + * @param layerEntries the layer entries that make up the layer + * @return the {@link CachedLayer} for the written layer + * @throws IOException if an I/O exception occurs */ - @Override - public void close() throws IOException { - saveCacheMetadata(cacheDirectory); + public CachedLayer writeUncompressedLayer( + Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { + return cacheStorageWriter.writeUncompressed( + uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries)); } /** - * Adds the cached layer to the cache metadata. This is NOT thread-safe. + * Caches a layer that was extracted from a local base image, and names the file using the + * provided diff id. * - * @param cachedLayers the layers to add + * @param diffId the diff id + * @param compressedBlob the compressed layer blob + * @return the {@link CachedLayer} for the written layer + * @throws IOException if an I/O exception occurs */ - public void addCachedLayersToMetadata(List cachedLayers) { - for (CachedLayer cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(new CachedLayerWithMetadata(cachedLayer, null)); - } + public CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) + throws IOException { + return cacheStorageWriter.writeTarLayer(diffId, compressedBlob); } /** - * Adds the cached layer to the cache metadata. This is NOT thread-safe. + * Writes a container configuration to {@code (cache directory)/local/config/(image id)}. An image + * ID is a SHA hash of a container configuration JSON. The value is also shown as IMAGE ID in + * {@code docker images}. * - * @param cachedLayersWithMetadata the layers to add + *

    Note: the {@code imageId} to the {@code containerConfiguration} is a one-way relationship; + * there is no guarantee that {@code containerConfiguration}'s SHA will be {@code imageId}, since + * the original container configuration is being rewritten here rather than being moved. + * + * @param imageId the ID of the image to store the container configuration for + * @param containerConfiguration the container configuration + * @throws IOException if an I/O exception occurs */ - public void addCachedLayersWithMetadataToMetadata( - List cachedLayersWithMetadata) { - for (CachedLayerWithMetadata cachedLayerWithMetadata : cachedLayersWithMetadata) { - cacheMetadataBuilder.addLayer(cachedLayerWithMetadata); - } + public void writeLocalConfig( + DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) + throws IOException { + cacheStorageWriter.writeLocalConfig(imageId, containerConfiguration); } - @VisibleForTesting - Path getCacheDirectory() { - return cacheDirectory; + /** + * Retrieves the cached image metadata (a manifest list and a list of manifest/container + * configuration pairs) for an image reference. + * + * @param imageReference the image reference + * @return the image metadata for the image reference, if found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + public Optional retrieveMetadata(ImageReference imageReference) + throws IOException, CacheCorruptedException { + return cacheStorageReader.retrieveMetadata(imageReference); } - @VisibleForTesting - CacheMetadata getMetadata() { - return cacheMetadata; + /** + * Returns {@code true} if all image layers described in a manifest exist in the cache. + * + * @param manifest the image manifest + * @return a boolean + */ + public boolean areAllLayersCached(ManifestTemplate manifest) { + return cacheStorageReader.areAllLayersCached(manifest); } - @VisibleForTesting - CacheMetadata getUpdatedMetadata() { - return cacheMetadataBuilder.build(); + /** + * Retrieves the {@link CachedLayer} that was built from the {@code layerEntries}. + * + * @param layerEntries the layer entries to match against + * @return a {@link CachedLayer} that was built from {@code layerEntries}, if found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + public Optional retrieve(ImmutableList layerEntries) + throws IOException, CacheCorruptedException { + Optional optionalSelectedLayerDigest = + cacheStorageReader.select(LayerEntriesSelector.generateSelector(layerEntries)); + if (!optionalSelectedLayerDigest.isPresent()) { + return Optional.empty(); + } + + return cacheStorageReader.retrieve(optionalSelectedLayerDigest.get()); } - /** Saves the updated cache metadata back to the cache. */ - private void saveCacheMetadata(Path cacheDirectory) throws IOException { - Path cacheMetadataJsonFile = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); + /** + * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. + * + * @param layerDigest the layer digest + * @return the {@link CachedLayer} referenced by the layer digest, if found + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + public Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException { + return cacheStorageReader.retrieve(layerDigest); + } - CacheMetadataTemplate cacheMetadataJson = - CacheMetadataTranslator.toTemplate(cacheMetadataBuilder.build()); + /** + * Retrieves a {@link CachedLayer} for a local base image layer with the given diff id. + * + * @param diffId the diff id + * @return the {@link CachedLayer} with the given diff id + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + public Optional retrieveTarLayer(DescriptorDigest diffId) + throws IOException, CacheCorruptedException { + return cacheStorageReader.retrieveTarLayer(diffId); + } - try (OutputStream fileOutputStream = - new BufferedOutputStream(Files.newOutputStream(cacheMetadataJsonFile))) { - JsonTemplateMapper.toBlob(cacheMetadataJson).writeTo(fileOutputStream); - } + /** + * Retrieves the {@link ContainerConfigurationTemplate} for the image saved from the given image + * ID. An image ID is a SHA hash of a container configuration JSON. The value is also shown as + * IMAGE ID in {@code docker images}. + * + *

    Note: the {@code imageId} is only used to find the {@code containerConfiguration}, and is + * not necessarily the actual SHA of {@code containerConfiguration}. There is no guarantee that + * {@code containerConfiguration}'s SHA will be {@code imageId}, since the saved container + * configuration is not a direct copy of the base image's original configuration. + * + * @param imageId the image ID + * @return the {@link ContainerConfigurationTemplate} referenced by the image ID, if found + * @throws IOException if an I/O exception occurs + */ + public Optional retrieveLocalConfig(DescriptorDigest imageId) + throws IOException { + return cacheStorageReader.retrieveLocalConfig(imageId); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java similarity index 51% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java index 44d29cf4d0..8ea1cee4bf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,22 +18,23 @@ import java.nio.file.Path; -/** - * Thrown when trying to use a directory as {@link Cache}, but the directory might be used by other - * applications. - */ -public class CacheDirectoryNotOwnedException extends Exception { - - private final Path cacheDirectory; - - /** Initializes with the cache directory that was unsafe to use. */ - CacheDirectoryNotOwnedException(Path cacheDirectory) { - super(); +/** Thrown if the the cache was found to be corrupted. */ +public class CacheCorruptedException extends Exception { - this.cacheDirectory = cacheDirectory; + CacheCorruptedException(Path cacheDirectory, String message, Throwable cause) { + super( + message + + ". You may need to clear the cache by deleting the '" + + cacheDirectory + + "' directory", + cause); } - public Path getCacheDirectory() { - return cacheDirectory; + CacheCorruptedException(Path cacheDirectory, String message) { + super( + message + + ". You may need to clear the cache by deleting the '" + + cacheDirectory + + "' directory"); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java deleted file mode 100644 index a4e46faad5..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.nio.file.Path; - -/** Methods for getting static cache filename properties. */ -class CacheFiles { - - /** Increment the version prefix when the metadata format changes. */ - static final String METADATA_FILENAME = "metadata-v2.json"; - - private static final String LAYER_FILE_EXTENSION = ".tar.gz"; - - static Path getMetadataFile(Path cacheDirectory) { - return cacheDirectory.resolve(METADATA_FILENAME); - } - - /** - * Gets the path to the file for a layer digest. The file is {@code [cache directory]/[layer - * hash].tar.gz}. - */ - static Path getLayerFile(Path cacheDirectory, DescriptorDigest layerDigest) { - return cacheDirectory.resolve(layerDigest.getHash() + LAYER_FILE_EXTENSION); - } - - private CacheFiles() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java deleted file mode 100644 index fd8e38a74f..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; -import java.nio.file.Paths; -import java.util.List; -import java.util.function.BiPredicate; -import java.util.function.Predicate; -import javax.annotation.Nullable; - -/** - * The cache stores all the layer BLOBs as separate files and the cache metadata contains - * information about each layer BLOB. - */ -class CacheMetadata { - - static class Builder { - - private final ImageLayers.Builder layersBuilder = - ImageLayers.builder(); - - private Builder(ImageLayers initialLayers) { - layersBuilder.addAll(initialLayers); - } - - private Builder() {} - - /** - * Adds a layer. This method is NOT thread-safe. - * - * @param layer the layer to add - */ - Builder addLayer(CachedLayerWithMetadata layer) { - layersBuilder.add(layer); - return this; - } - - CacheMetadata build() { - return new CacheMetadata(layersBuilder.build()); - } - } - - /** Can be used to filter layers in the metadata. */ - static class LayerFilter { - - /** - * Checks if the layer entries matches the metadata layer entries - * - * @param layerEntries the layer entries to check - * @param metadataEntries the metadata entries to match against - * @return {@code true} if the layer entries match the metadata entries; {@code false} otherwise - */ - @VisibleForTesting - static boolean doLayerEntriesMatchMetadataEntries( - ImmutableList layerEntries, - ImmutableList metadataEntries) { - // Checks the layer entries are the same as the metadata layer entries. - if (layerEntries.size() != metadataEntries.size()) { - return false; - } - return pairwiseCompareAllPass( - layerEntries, - metadataEntries, - (layerEntry, metadataEntry) -> { - // Checks extraction path not equal. - if (!layerEntry.getExtractionPath().equals(metadataEntry.getExtractionPath())) { - return false; - } - - // Checks for any source file not equal. - if (layerEntry.getSourceFiles().size() - != metadataEntry.getSourceFilesStrings().size()) { - return false; - } - return pairwiseCompareAllPass( - layerEntry.getSourceFiles(), - metadataEntry.getSourceFilesStrings(), - (sourceFile, sourceFileString) -> sourceFile.equals(Paths.get(sourceFileString))); - }); - } - - private static boolean pairwiseCompareAllPass( - List listA, List listB, BiPredicate compare) { - return Streams.zip(listA.stream(), listB.stream(), compare::test) - .allMatch(Predicate.isEqual(true)); - } - - private final ImageLayers layers; - - @Nullable private ImmutableList layerEntries; - - private LayerFilter(ImageLayers layers) { - this.layers = layers; - } - - /** Filters to a certain list of {@link LayerEntry}s. */ - LayerFilter byLayerEntries(ImmutableList layerEntries) { - this.layerEntries = layerEntries; - return this; - } - - /** Applies the filters to the metadata layers. */ - ImageLayers filter() throws CacheMetadataCorruptedException { - try { - ImageLayers.Builder filteredLayersBuilder = ImageLayers.builder(); - - for (CachedLayerWithMetadata layer : layers) { - if (layerEntries != null) { - if (layer.getMetadata() == null) { - // There is no metadata, so it doesn't pass the filter. - continue; - } - - if (!doLayerEntriesMatchMetadataEntries( - layerEntries, layer.getMetadata().getEntries())) { - // The layer entries do not match. - continue; - } - } - - filteredLayersBuilder.add(layer); - } - - return filteredLayersBuilder.build(); - - } catch (LayerPropertyNotFoundException ex) { - throw new CacheMetadataCorruptedException(ex); - } - } - } - - static Builder builder() { - return new Builder(); - } - - private final ImageLayers layers; - - private CacheMetadata(ImageLayers layers) { - this.layers = layers; - } - - ImageLayers getLayers() { - return layers; - } - - LayerFilter filterLayers() { - return new LayerFilter(layers); - } - - /** @return a {@link Builder} starts with all the layers in this metadata */ - Builder newAppendingBuilder() { - return new Builder(layers); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java deleted file mode 100644 index 36c5175bef..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerObjectTemplate; -import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate; -import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate; -import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; - -/** Translates {@link CacheMetadata} to and from {@link CacheMetadataTemplate}. */ -public class CacheMetadataTranslator { - - /** Translates {@link CacheMetadataTemplate} to {@link CacheMetadata}. */ - static CacheMetadata fromTemplate(CacheMetadataTemplate template, Path cacheDirectory) - throws CacheMetadataCorruptedException { - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - - // Converts each layer object in the template to a cache metadata layer. - for (CacheMetadataLayerObjectTemplate layerObjectTemplate : template.getLayers()) { - if (layerObjectTemplate.getDigest() == null || layerObjectTemplate.getDiffId() == null) { - throw new CacheMetadataCorruptedException( - "Cannot translate cache metadata layer without a digest or diffId"); - } - - Path layerContentFile = - CacheFiles.getLayerFile(cacheDirectory, layerObjectTemplate.getDigest()); - - // Gets the properties for a layer. Properties only exist for application layers. - CacheMetadataLayerPropertiesObjectTemplate propertiesObjectTemplate = - layerObjectTemplate.getProperties(); - - // Constructs the cache metadata layer from a cached layer and layer metadata. - LayerMetadata layerMetadata = null; - if (propertiesObjectTemplate != null) { - // Converts the layer entry templates to layer entries. - ImmutableList.Builder layerEntries = - ImmutableList.builderWithExpectedSize( - propertiesObjectTemplate.getLayerEntries().size()); - for (LayerEntryTemplate layerEntryTemplate : propertiesObjectTemplate.getLayerEntries()) { - if (layerEntryTemplate.getSourceFiles() == null - || layerEntryTemplate.getExtractionPath() == null) { - throw new CacheMetadataCorruptedException( - "Cannot translate cache metadata layer entry without source files or extraction path"); - } - layerEntries.add( - new LayerEntry( - layerEntryTemplate - .getSourceFiles() - .stream() - .map(Paths::get) - .collect(ImmutableList.toImmutableList()), - layerEntryTemplate.getExtractionPath())); - } - - layerMetadata = - LayerMetadata.from( - layerEntries.build(), propertiesObjectTemplate.getLastModifiedTime()); - } - - CachedLayer cachedLayer = - new CachedLayer( - layerContentFile, - new BlobDescriptor(layerObjectTemplate.getSize(), layerObjectTemplate.getDigest()), - layerObjectTemplate.getDiffId()); - - CachedLayerWithMetadata cachedLayerWithMetadata = - new CachedLayerWithMetadata(cachedLayer, layerMetadata); - cacheMetadataBuilder.addLayer(cachedLayerWithMetadata); - } - - return cacheMetadataBuilder.build(); - } - - /** Translates {@link CacheMetadata} to {@link CacheMetadataTemplate}. */ - static CacheMetadataTemplate toTemplate(CacheMetadata cacheMetadata) { - CacheMetadataTemplate template = new CacheMetadataTemplate(); - - for (CachedLayerWithMetadata cachedLayerWithMetadata : cacheMetadata.getLayers()) { - CacheMetadataLayerObjectTemplate layerObjectTemplate = - new CacheMetadataLayerObjectTemplate() - .setSize(cachedLayerWithMetadata.getBlobDescriptor().getSize()) - .setDigest(cachedLayerWithMetadata.getBlobDescriptor().getDigest()) - .setDiffId(cachedLayerWithMetadata.getDiffId()); - - if (cachedLayerWithMetadata.getMetadata() != null) { - // Constructs the layer entry templates to add to the layer object template. - List layerEntryTemplates = - cachedLayerWithMetadata - .getMetadata() - .getEntries() - .stream() - .map( - layerMetadataEntry -> - new LayerEntryTemplate( - layerMetadataEntry.getSourceFilesStrings(), - layerMetadataEntry.getExtractionPath())) - .collect(Collectors.toList()); - - layerObjectTemplate.setProperties( - new CacheMetadataLayerPropertiesObjectTemplate() - .setLayerEntries(layerEntryTemplates) - .setLastModifiedTime(cachedLayerWithMetadata.getMetadata().getLastModifiedTime())); - } - - template.addLayer(layerObjectTemplate); - } - - return template; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java deleted file mode 100644 index 4ae37b0a05..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.List; -import javax.annotation.Nullable; - -/** Reads image content from the cache. */ -public class CacheReader { - - /** - * @param path the file to check. - * @return the last modified time for the file at {@code path}. Recursively finds the most recent - * last modified time for all subfiles if the file is a directory. - * @throws IOException if checking the last modified time fails. - */ - private static FileTime getLastModifiedTime(Path path) throws IOException { - if (Files.isDirectory(path)) { - List subFiles = new DirectoryWalker(path).walk(); - FileTime maxLastModifiedTime = FileTime.from(Instant.MIN); - - // Finds the max last modified time for the subfiles. - for (Path subFilePath : subFiles) { - FileTime subFileLastModifiedTime = Files.getLastModifiedTime(subFilePath); - if (subFileLastModifiedTime.compareTo(maxLastModifiedTime) > 0) { - maxLastModifiedTime = subFileLastModifiedTime; - } - } - - return maxLastModifiedTime; - } - - return Files.getLastModifiedTime(path); - } - - private final Cache cache; - - public CacheReader(Cache cache) { - this.cache = cache; - } - - /** - * @param layerDigest the layer digest of the layer to get. - * @return the cached layer with digest {@code layerDigest}, or {@code null} if not found. - */ - @Nullable - public CachedLayer getLayer(DescriptorDigest layerDigest) { - return cache.getMetadata().getLayers().get(layerDigest); - } - - /** - * Finds the file that stores the content BLOB for an application layer. - * - * @param layerEntries the entries for the layer content - * @return the newest cached layer file that matches the {@code layerType} and {@code - * sourceFiles}, or {@code null} if there is no match. - * @throws CacheMetadataCorruptedException if getting the cache metadata fails. - */ - @Nullable - public Path getLayerFile(ImmutableList layerEntries) - throws CacheMetadataCorruptedException { - CacheMetadata cacheMetadata = cache.getMetadata(); - ImageLayers cachedLayers = - cacheMetadata.filterLayers().byLayerEntries(layerEntries).filter(); - - // Finds the newest cached layer for the layer type. - FileTime newestLastModifiedTime = FileTime.from(Instant.MIN); - - Path newestLayerFile = null; - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - if (cachedLayer.getMetadata() == null) { - throw new IllegalStateException("Layers with sourceFiles should have metadata"); - } - - FileTime cachedLayerLastModifiedTime = cachedLayer.getMetadata().getLastModifiedTime(); - if (cachedLayerLastModifiedTime.compareTo(newestLastModifiedTime) > 0) { - newestLastModifiedTime = cachedLayerLastModifiedTime; - newestLayerFile = cachedLayer.getContentFile(); - } - } - - return newestLayerFile; - } - - /** - * Gets an up-to-date layer that is built from the {@code sourceFiles}. - * - *

    The method returns the first up-to-date layer found. This is safe because the source files - * will not have been modified since creation of any up-to-date layer (ie. all up-to-date layers - * should have the same file contents). - * - * @param layerEntries the layer's content entries - * @return an up-to-date layer containing the source files. - * @throws IOException if reading the source files fails. - * @throws CacheMetadataCorruptedException if reading the cache metadata fails. - */ - @Nullable - public CachedLayerWithMetadata getUpToDateLayerByLayerEntries( - ImmutableList layerEntries) throws IOException, CacheMetadataCorruptedException { - // Grabs all the layers that have matching source files. - ImageLayers cachedLayersWithSourceFiles = - cache.getMetadata().filterLayers().byLayerEntries(layerEntries).filter(); - if (cachedLayersWithSourceFiles.isEmpty()) { - return null; - } - - // Determines the latest modification time for the source files. - FileTime sourceFilesLastModifiedTime = FileTime.from(Instant.MIN); - for (LayerEntry layerEntry : layerEntries) { - for (Path path : layerEntry.getSourceFiles()) { - FileTime lastModifiedTime = getLastModifiedTime(path); - if (lastModifiedTime.compareTo(sourceFilesLastModifiedTime) > 0) { - sourceFilesLastModifiedTime = lastModifiedTime; - } - } - } - - // Checks if at least one of the matched layers is up-to-date. - for (CachedLayerWithMetadata cachedLayer : cachedLayersWithSourceFiles) { - if (cachedLayer.getMetadata() == null) { - throw new IllegalStateException("Layers with sourceFiles should have metadata"); - } - - if (sourceFilesLastModifiedTime.compareTo(cachedLayer.getMetadata().getLastModifiedTime()) - <= 0) { - // This layer is an up-to-date layer. - return cachedLayer; - } - } - - return null; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java new file mode 100644 index 0000000000..798e619f9f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -0,0 +1,188 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.common.base.Splitter; +import java.nio.file.Path; +import java.security.DigestException; + +/** Resolves the files used in the default cache storage engine. */ +class CacheStorageFiles { + + private static final String LAYERS_DIRECTORY = "layers"; + private static final String LOCAL_DIRECTORY = "local"; + private static final String IMAGES_DIRECTORY = "images"; + private static final String SELECTORS_DIRECTORY = "selectors"; + private static final String TEMPORARY_DIRECTORY = "tmp"; + private static final String TEMPORARY_LAYER_FILE_NAME = ".tmp.layer"; + + /** + * Returns whether or not {@code file} is a layer contents file. + * + * @param file the file to check + * @return {@code true} if {@code file} is a layer contents file; {@code false} otherwise + */ + static boolean isLayerFile(Path file) { + return file.getFileName().toString().length() == DescriptorDigest.HASH_LENGTH; + } + + private final Path cacheDirectory; + + CacheStorageFiles(Path cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + /** + * Gets the diff ID portion of the layer filename. + * + * @param layerFile the layer file to parse for the diff ID + * @return the diff ID portion of the layer file filename + * @throws CacheCorruptedException if no valid diff ID could be parsed + */ + DescriptorDigest getDigestFromFilename(Path layerFile) throws CacheCorruptedException { + try { + String hash = layerFile.getFileName().toString(); + return DescriptorDigest.fromHash(hash); + + } catch (DigestException | IndexOutOfBoundsException ex) { + throw new CacheCorruptedException( + cacheDirectory, "Layer file did not include valid hash: " + layerFile, ex); + } + } + + /** + * Gets the cache directory. + * + * @return the cache directory + */ + Path getCacheDirectory() { + return cacheDirectory; + } + + /** + * Resolves the layer contents file. + * + * @param layerDigest the layer digest + * @param layerDiffId the layer diff Id + * @return the layer contents file + */ + Path getLayerFile(DescriptorDigest layerDigest, DescriptorDigest layerDiffId) { + return getLayerDirectory(layerDigest).resolve(getLayerFilename(layerDiffId)); + } + + /** + * Gets the filename for the layer file. The filename is in the form {@code .layer}. + * + * @param layerDiffId the layer's diff ID + * @return the layer filename + */ + String getLayerFilename(DescriptorDigest layerDiffId) { + return layerDiffId.getHash(); + } + + /** + * Resolves a selector file. + * + * @param selector the selector digest + * @return the selector file + */ + Path getSelectorFile(DescriptorDigest selector) { + return cacheDirectory.resolve(SELECTORS_DIRECTORY).resolve(selector.getHash()); + } + + /** + * Resolves the {@link #LAYERS_DIRECTORY} in the {@link #cacheDirectory}. + * + * @return the directory containing all the layer directories + */ + Path getLayersDirectory() { + return cacheDirectory.resolve(LAYERS_DIRECTORY); + } + + /** + * Gets the directory for the layer with digest {@code layerDigest}. + * + * @param layerDigest the digest of the layer + * @return the directory for that {@code layerDigest} + */ + Path getLayerDirectory(DescriptorDigest layerDigest) { + return getLayersDirectory().resolve(layerDigest.getHash()); + } + + /** + * Resolves the {@link #LOCAL_DIRECTORY} in the {@link #cacheDirectory}. + * + * @return the directory containing local base image layers + */ + Path getLocalDirectory() { + return cacheDirectory.resolve(LOCAL_DIRECTORY); + } + + /** + * Gets the directory to store the image manifest and configuration. + * + * @return the directory for the image manifest and configuration + */ + Path getImagesDirectory() { + return cacheDirectory.resolve(IMAGES_DIRECTORY); + } + + /** + * Gets the directory corresponding to the given image reference. + * + * @param imageReference the image reference + * @return a path in the form of {@code + * (jib-cache)/images/registry[!port]/repository!(tag|digest-type!digest)} + */ + Path getImageDirectory(ImageReference imageReference) { + // Replace ':' and '@' with '!' to avoid directory-naming restrictions + String replacedReference = + imageReference.toStringWithQualifier().replace(':', '!').replace('@', '!'); + + // Split image reference on '/' to build directory structure + Iterable directories = Splitter.on('/').split(replacedReference); + Path destination = getImagesDirectory(); + for (String dir : directories) { + destination = destination.resolve(dir); + } + return destination; + } + + /** + * Gets the directory to store temporary files. + * + * @return the directory for temporary files + */ + Path getTemporaryDirectory() { + return cacheDirectory.resolve(TEMPORARY_DIRECTORY); + } + + /** + * Resolves a file to use as a temporary file to write layer contents to. + * + * @param layerDirectory the directory in which to resolve the temporary layer file + * @return the temporary layer file + */ + Path getTemporaryLayerFile(Path layerDirectory) { + Path temporaryLayerFile = layerDirectory.resolve(TEMPORARY_LAYER_FILE_NAME); + temporaryLayerFile.toFile().deleteOnExit(); + return temporaryLayerFile; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java new file mode 100644 index 0000000000..0cce36506b --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -0,0 +1,268 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.LockFile; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Reads from the default cache storage engine. */ +class CacheStorageReader { + + @VisibleForTesting + static void verifyImageMetadata(ImageMetadataTemplate metadata, Path metadataCacheDirectory) + throws CacheCorruptedException { + List manifestsAndConfigs = metadata.getManifestsAndConfigs(); + if (manifestsAndConfigs.isEmpty()) { + throw new CacheCorruptedException(metadataCacheDirectory, "Manifest cache empty"); + } + if (manifestsAndConfigs.stream().anyMatch(entry -> entry.getManifest() == null)) { + throw new CacheCorruptedException(metadataCacheDirectory, "Manifest(s) missing"); + } + if (metadata.getManifestList() == null && manifestsAndConfigs.size() != 1) { + throw new CacheCorruptedException(metadataCacheDirectory, "Manifest list missing"); + } + + ManifestTemplate firstManifest = manifestsAndConfigs.get(0).getManifest(); + if (firstManifest instanceof V21ManifestTemplate) { + if (metadata.getManifestList() != null + || manifestsAndConfigs.stream().anyMatch(entry -> entry.getConfig() != null)) { + throw new CacheCorruptedException(metadataCacheDirectory, "Schema 1 manifests corrupted"); + } + } else if (firstManifest instanceof BuildableManifestTemplate) { + if (manifestsAndConfigs.stream().anyMatch(entry -> entry.getConfig() == null)) { + throw new CacheCorruptedException(metadataCacheDirectory, "Schema 2 manifests corrupted"); + } + if (metadata.getManifestList() != null + && manifestsAndConfigs.stream().anyMatch(entry -> entry.getManifestDigest() == null)) { + throw new CacheCorruptedException(metadataCacheDirectory, "Schema 2 manifests corrupted"); + } + } else { + throw new CacheCorruptedException( + metadataCacheDirectory, "Unknown manifest type: " + firstManifest); + } + } + + private final CacheStorageFiles cacheStorageFiles; + + CacheStorageReader(CacheStorageFiles cacheStorageFiles) { + this.cacheStorageFiles = cacheStorageFiles; + } + + /** + * Returns {@code true} if all image layers described in a manifest have a corresponding file + * entry in the cache. + * + * @param manifest the image manifest + * @return a boolean + */ + boolean areAllLayersCached(ManifestTemplate manifest) { + + List layerDigests; + + if (manifest instanceof V21ManifestTemplate) { + layerDigests = ((V21ManifestTemplate) manifest).getLayerDigests(); + } else if (manifest instanceof BuildableManifestTemplate) { + layerDigests = + ((BuildableManifestTemplate) manifest) + .getLayers().stream() + .map(BuildableManifestTemplate.ContentDescriptorTemplate::getDigest) + .collect(Collectors.toList()); + } else { + throw new IllegalArgumentException("Unknown manifest type: " + manifest); + } + + for (DescriptorDigest layerDigest : layerDigests) { + Path layerDirectory = cacheStorageFiles.getLayerDirectory(layerDigest); + if (!Files.exists(layerDirectory)) { + return false; + } + } + return true; + } + + /** + * Retrieves the cached image metadata (a manifest list and a list of manifest/container + * configuration pairs) for an image reference. + * + * @param imageReference the image reference + * @return the image metadata for the image reference, if found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + Optional retrieveMetadata(ImageReference imageReference) + throws IOException, CacheCorruptedException { + Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); + Path metadataPath = imageDirectory.resolve("manifests_configs.json"); + if (!Files.exists(metadataPath)) { + return Optional.empty(); + } + + ImageMetadataTemplate metadata; + try (LockFile ignored = LockFile.lock(imageDirectory.resolve("lock"))) { + metadata = JsonTemplateMapper.readJsonFromFile(metadataPath, ImageMetadataTemplate.class); + } + verifyImageMetadata(metadata, imageDirectory); + return Optional.of(metadata); + } + + /** + * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. + * + * @param layerDigest the layer digest + * @return the {@link CachedLayer} referenced by the layer digest, if found + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException { + Path layerDirectory = cacheStorageFiles.getLayerDirectory(layerDigest); + if (!Files.exists(layerDirectory)) { + return Optional.empty(); + } + + try (Stream files = Files.list(layerDirectory)) { + List layerFiles = + files.filter(CacheStorageFiles::isLayerFile).collect(Collectors.toList()); + if (layerFiles.size() != 1) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "No or multiple layer files found for layer hash " + + layerDigest.getHash() + + " in directory: " + + layerDirectory); + } + + Path layerFile = layerFiles.get(0); + return Optional.of( + CachedLayer.builder() + .setLayerDigest(layerDigest) + .setLayerSize(Files.size(layerFile)) + .setLayerBlob(Blobs.from(layerFile)) + .setLayerDiffId(cacheStorageFiles.getDigestFromFilename(layerFile)) + .build()); + } + } + + /** + * Retrieves the {@link CachedLayer} for the local base image layer with the given diff ID. + * + * @param diffId the diff ID + * @return the {@link CachedLayer} referenced by the diff ID, if found + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + Optional retrieveTarLayer(DescriptorDigest diffId) + throws IOException, CacheCorruptedException { + Path layerDirectory = cacheStorageFiles.getLocalDirectory().resolve(diffId.getHash()); + if (!Files.exists(layerDirectory)) { + return Optional.empty(); + } + + try (Stream files = Files.list(layerDirectory)) { + List layerFiles = + files.filter(CacheStorageFiles::isLayerFile).collect(Collectors.toList()); + if (layerFiles.size() != 1) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "No or multiple layer files found for layer hash " + + diffId.getHash() + + " in directory: " + + layerDirectory); + } + + Path layerFile = layerFiles.get(0); + return Optional.of( + CachedLayer.builder() + .setLayerDigest(cacheStorageFiles.getDigestFromFilename(layerFile)) + .setLayerSize(Files.size(layerFile)) + .setLayerBlob(Blobs.from(layerFile)) + .setLayerDiffId(diffId) + .build()); + } + } + + /** + * Retrieves the {@link ContainerConfigurationTemplate} for the image with the given image ID. + * + * @param imageId the image ID + * @return the {@link ContainerConfigurationTemplate} referenced by the image ID, if found + * @throws IOException if an I/O exception occurs + */ + Optional retrieveLocalConfig(DescriptorDigest imageId) + throws IOException { + Path configPath = + cacheStorageFiles.getLocalDirectory().resolve("config").resolve(imageId.getHash()); + if (!Files.exists(configPath)) { + return Optional.empty(); + } + + ContainerConfigurationTemplate config = + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + return Optional.of(config); + } + + /** + * Retrieves the layer digest selected by the {@code selector}. + * + * @param selector the selector + * @return the layer digest {@code selector} selects, if found + * @throws CacheCorruptedException if the selector file contents was not a valid layer digest + * @throws IOException if an I/O exception occurs + */ + Optional select(DescriptorDigest selector) + throws CacheCorruptedException, IOException { + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); + if (!Files.exists(selectorFile)) { + return Optional.empty(); + } + + String selectorFileContents = + new String(Files.readAllBytes(selectorFile), StandardCharsets.UTF_8); + try { + return Optional.of(DescriptorDigest.fromHash(selectorFileContents)); + + } catch (DigestException ex) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "Expected valid layer digest as contents of selector file `" + + selectorFile + + "` for selector `" + + selector.getHash() + + "`, but got: " + + selectorFileContents); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java new file mode 100644 index 0000000000..4f059fba5c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -0,0 +1,481 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.cache.Retry.Action; +import com.google.cloud.tools.jib.filesystem.LockFile; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.zip.GZIPOutputStream; +import javax.annotation.Nullable; +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; + +/** Writes to the default cache storage engine. */ +class CacheStorageWriter { + + /** Holds information about a layer that was written. */ + private static class WrittenLayer { + + private final DescriptorDigest layerDigest; + private final DescriptorDigest layerDiffId; + private final long layerSize; + + private WrittenLayer( + DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize) { + this.layerDigest = layerDigest; + this.layerDiffId = layerDiffId; + this.layerSize = layerSize; + } + } + + private static void verifyImageMetadata(ImageMetadataTemplate metadata) { + Predicate isManifestNull = pair -> pair.getManifest() == null; + Predicate isConfigNull = pair -> pair.getConfig() == null; + Predicate isDigestNull = pair -> pair.getManifestDigest() == null; + + List manifestsAndConfigs = metadata.getManifestsAndConfigs(); + Preconditions.checkArgument(!manifestsAndConfigs.isEmpty(), "no manifests given"); + Preconditions.checkArgument( + manifestsAndConfigs.stream().noneMatch(isManifestNull), "null manifest(s)"); + Preconditions.checkArgument( + metadata.getManifestList() != null || manifestsAndConfigs.size() == 1, + "manifest list missing while multiple manifests given"); + + ManifestTemplate firstManifest = manifestsAndConfigs.get(0).getManifest(); + if (firstManifest instanceof V21ManifestTemplate) { + Preconditions.checkArgument( + metadata.getManifestList() == null, "manifest list given for schema 1"); + Preconditions.checkArgument( + isConfigNull.test(manifestsAndConfigs.get(0)), "container config given for schema 1"); + } else if (firstManifest instanceof BuildableManifestTemplate) { + Preconditions.checkArgument( + manifestsAndConfigs.stream().noneMatch(isConfigNull), "null container config(s)"); + if (metadata.getManifestList() != null) { + Preconditions.checkArgument( + manifestsAndConfigs.stream().noneMatch(isDigestNull), "null manifest digest(s)"); + } + } else { + throw new IllegalArgumentException("Unknown manifest type: " + firstManifest); + } + } + + /** + * Attempts to move {@code source} to {@code destination}. If {@code destination} already exists, + * this does nothing. Attempts an atomic move first, and falls back to non-atomic if the + * filesystem does not support atomic moves. + * + * @param source the source path + * @param destination the destination path + * @throws IOException if an I/O exception occurs + */ + @VisibleForTesting + static void moveIfDoesNotExist(Path source, Path destination) throws IOException { + String errorMessage = + String.format( + "unable to move: %s to %s; such failures are often caused by interference from " + + "antivirus (https://github.com/GoogleContainerTools/jib/issues/3127#issuecomment-796838294), " + + "or rarely if the operation is not supported by the file system (for example: " + + "special non-local file system)", + source, destination); + + try { + Action rename = + () -> { + if (Files.exists(destination)) { + // If the file already exists, we skip renaming and use the existing file. + // This happens, e.g., if a new layer happens to have the same content as a + // previously-cached layer or the same layer is being cached concurrently. + return true; + } + Files.move(source, destination); + return Files.exists(destination); + }; + // Some Windows users report java.nio.file.AccessDeniedException that we suspect is caused + // by anti-virus programs, like Windows Defender, that open new files for scanning. + // Retry the rename up to 10 times, with 15ms pause between each retry. + if (!Retry.action(rename) + .maximumRetries(10) + .retryOnException(ex -> ex instanceof FileSystemException) + .sleep(15, TimeUnit.MILLISECONDS) + .run()) { + throw new IOException(errorMessage); + } + + } catch (IOException ex) { + throw new IOException(errorMessage, ex); + } + } + + /** + * Decompresses the file to obtain the diff ID. + * + * @param compressedFile the file containing the compressed contents + * @return the digest of the decompressed file + * @throws IOException if an I/O exception occurs + */ + private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile) + throws IOException { + try (InputStream in = + new CompressorStreamFactory(true) + .createCompressorInputStream( + new BufferedInputStream(Files.newInputStream(compressedFile)))) { + return Digests.computeDigest(in).getDigest(); + } catch (CompressorException e) { + throw new IOException(e); + } + } + + /** + * Writes a json template to the destination path by writing to a temporary file then moving the + * file. + * + * @param jsonTemplate the json template + * @param destination the destination path + * @throws IOException if an I/O exception occurs + */ + private static void writeJsonTemplate(JsonTemplate jsonTemplate, Path destination) + throws IOException { + Path temporaryFile = Files.createTempFile(destination.getParent(), null, null); + temporaryFile.toFile().deleteOnExit(); + try (OutputStream outputStream = Files.newOutputStream(temporaryFile)) { + JsonTemplateMapper.writeTo(jsonTemplate, outputStream); + } + + // Attempts an atomic move first, and falls back to non-atomic if the file system does not + // support atomic moves. + try { + Files.move( + temporaryFile, + destination, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + + } catch (AtomicMoveNotSupportedException ignored) { + Files.move(temporaryFile, destination, StandardCopyOption.REPLACE_EXISTING); + } + } + + private final CacheStorageFiles cacheStorageFiles; + + CacheStorageWriter(CacheStorageFiles cacheStorageFiles) { + this.cacheStorageFiles = cacheStorageFiles; + } + + /** + * Writes a compressed layer {@link Blob}. + * + *

    The {@code compressedLayerBlob} is written to the layer directory under the layers directory + * corresponding to the layer blob. + * + * @param compressedLayerBlob the compressed layer {@link Blob} to write out + * @return the {@link CachedLayer} representing the written entry + * @throws IOException if an I/O exception occurs + */ + CachedLayer writeCompressed(Blob compressedLayerBlob) throws IOException { + // Creates the layers directory if it doesn't exist. + Files.createDirectories(cacheStorageFiles.getLayersDirectory()); + + // Creates the temporary directory. + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); + + // Writes the layer file to the temporary directory. + WrittenLayer writtenLayer = + writeCompressedLayerBlobToDirectory(compressedLayerBlob, temporaryLayerDirectory); + + // Moves the temporary directory to the final location. + moveIfDoesNotExist( + temporaryLayerDirectory, cacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + + // Updates cachedLayer with the blob information. + Path layerFile = + cacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + return CachedLayer.builder() + .setLayerDigest(writtenLayer.layerDigest) + .setLayerDiffId(writtenLayer.layerDiffId) + .setLayerSize(writtenLayer.layerSize) + .setLayerBlob(Blobs.from(layerFile)) + .build(); + } + } + + /** + * Writes an uncompressed {@link Blob} out to the cache directory. + * + *

    Cache is written out in the form: + * + *

    + * + * @param uncompressedLayerBlob the {@link Blob} containing the uncompressed layer contents to + * write out + * @param selector the optional selector digest to also reference this layer data. A selector + * digest may be a secondary identifier for a layer that is distinct from the default layer + * digest. + * @return the {@link CachedLayer} representing the written entry + * @throws IOException if an I/O exception occurs + */ + CachedLayer writeUncompressed(Blob uncompressedLayerBlob, @Nullable DescriptorDigest selector) + throws IOException { + // Creates the layers directory if it doesn't exist. + Files.createDirectories(cacheStorageFiles.getLayersDirectory()); + + // Creates the temporary directory. The temporary directory must be in the same FileStore as the + // final location for Files.move to work. + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); + + // Writes the layer file to the temporary directory. + WrittenLayer writtenLayer = + writeUncompressedLayerBlobToDirectory(uncompressedLayerBlob, temporaryLayerDirectory); + + // Moves the temporary directory to the final location. + moveIfDoesNotExist( + temporaryLayerDirectory, cacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + + // Updates cachedLayer with the blob information. + Path layerFile = + cacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + CachedLayer.Builder cachedLayerBuilder = + CachedLayer.builder() + .setLayerDigest(writtenLayer.layerDigest) + .setLayerDiffId(writtenLayer.layerDiffId) + .setLayerSize(writtenLayer.layerSize) + .setLayerBlob(Blobs.from(layerFile)); + + // Write the selector file. + if (selector != null) { + writeSelector(selector, writtenLayer.layerDigest); + } + + return cachedLayerBuilder.build(); + } + } + + /** + * Saves a local base image layer. + * + * @param diffId the layer blob's diff ID + * @param compressedBlob the blob to save + * @throws IOException if an I/O exception occurs + */ + CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) throws IOException { + Files.createDirectories(cacheStorageFiles.getLocalDirectory()); + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(temporaryLayerDirectory); + + BlobDescriptor layerBlobDescriptor; + try (OutputStream fileOutputStream = + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile))) { + layerBlobDescriptor = compressedBlob.writeTo(fileOutputStream); + } + + // Renames the temporary layer file to its digest + // (temp/temp -> temp/) + String fileName = layerBlobDescriptor.getDigest().getHash(); + Path digestLayerFile = temporaryLayerDirectory.resolve(fileName); + moveIfDoesNotExist(temporaryLayerFile, digestLayerFile); + + // Moves the temporary directory to directory named with diff ID + // (temp/ -> /) + Path destination = cacheStorageFiles.getLocalDirectory().resolve(diffId.getHash()); + moveIfDoesNotExist(temporaryLayerDirectory, destination); + + return CachedLayer.builder() + .setLayerDigest(layerBlobDescriptor.getDigest()) + .setLayerDiffId(diffId) + .setLayerSize(layerBlobDescriptor.getSize()) + .setLayerBlob(Blobs.from(destination.resolve(fileName))) + .build(); + } + } + + /** + * Saves image metadata (a manifest list and a list of manifest/container configuration pairs) for + * an image reference. + * + * @param imageReference the image reference to store the metadata for + * @param metadata the image metadata + */ + void writeMetadata(ImageReference imageReference, ImageMetadataTemplate metadata) + throws IOException { + verifyImageMetadata(metadata); + Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); + Files.createDirectories(imageDirectory); + + try (LockFile ignored = LockFile.lock(imageDirectory.resolve("lock"))) { + writeJsonTemplate(metadata, imageDirectory.resolve("manifests_configs.json")); + } + } + + /** + * Writes a container configuration to {@code (cache directory)/local/config/(image id)}. + * + * @param imageId the ID of the image to store the container configuration for + * @param containerConfiguration the container configuration + * @throws IOException if an I/O exception occurs + */ + void writeLocalConfig( + DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) + throws IOException { + Path configDirectory = cacheStorageFiles.getLocalDirectory().resolve("config"); + Files.createDirectories(configDirectory); + writeJsonTemplate(containerConfiguration, configDirectory.resolve(imageId.getHash())); + } + + /** + * Writes a compressed {@code layerBlob} to the {@code layerDirectory}. + * + * @param compressedLayerBlob the compressed layer {@link Blob} + * @param layerDirectory the directory for the layer + * @return a {@link WrittenLayer} with the written layer information + * @throws IOException if an I/O exception occurs + */ + private WrittenLayer writeCompressedLayerBlobToDirectory( + Blob compressedLayerBlob, Path layerDirectory) throws IOException { + // Writes the layer file to the temporary directory. + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(layerDirectory); + + BlobDescriptor layerBlobDescriptor; + try (OutputStream fileOutputStream = + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile))) { + layerBlobDescriptor = compressedLayerBlob.writeTo(fileOutputStream); + } + + // Gets the diff ID. + DescriptorDigest layerDiffId = getDiffIdByDecompressingFile(temporaryLayerFile); + + // Renames the temporary layer file to the correct filename. + Path layerFile = layerDirectory.resolve(cacheStorageFiles.getLayerFilename(layerDiffId)); + moveIfDoesNotExist(temporaryLayerFile, layerFile); + + return new WrittenLayer( + layerBlobDescriptor.getDigest(), layerDiffId, layerBlobDescriptor.getSize()); + } + + /** + * Writes an uncompressed {@code layerBlob} to the {@code layerDirectory}. + * + * @param uncompressedLayerBlob the uncompressed layer {@link Blob} + * @param layerDirectory the directory for the layer + * @return a {@link WrittenLayer} with the written layer information + * @throws IOException if an I/O exception occurs + */ + private WrittenLayer writeUncompressedLayerBlobToDirectory( + Blob uncompressedLayerBlob, Path layerDirectory) throws IOException { + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(layerDirectory); + + try (CountingDigestOutputStream compressedDigestOutputStream = + new CountingDigestOutputStream( + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile)))) { + // Writes the layer with GZIP compression. The original bytes are captured as the layer's + // diff ID and the bytes outputted from the GZIP compression are captured as the layer's + // content descriptor. + GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream); + DescriptorDigest layerDiffId = uncompressedLayerBlob.writeTo(compressorStream).getDigest(); + + // The GZIPOutputStream must be closed in order to write out the remaining compressed data. + compressorStream.close(); + BlobDescriptor blobDescriptor = compressedDigestOutputStream.computeDigest(); + DescriptorDigest layerDigest = blobDescriptor.getDigest(); + long layerSize = blobDescriptor.getSize(); + + // Renames the temporary layer file to the correct filename. + Path layerFile = layerDirectory.resolve(cacheStorageFiles.getLayerFilename(layerDiffId)); + moveIfDoesNotExist(temporaryLayerFile, layerFile); + + return new WrittenLayer(layerDigest, layerDiffId, layerSize); + } + } + + /** + * Writes the {@code selector} to a file in the selectors directory, with contents {@code + * layerDigest}. + * + * @param selector the selector + * @param layerDigest the layer digest it selects + * @throws IOException if an I/O exception occurs + */ + private void writeSelector(DescriptorDigest selector, DescriptorDigest layerDigest) + throws IOException { + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); + + // Creates the selectors directory if it doesn't exist. + Files.createDirectories(selectorFile.getParent()); + + // Writes the selector to a temporary file and then moves the file to the intended location. + Path temporarySelectorFile = Files.createTempFile(null, null); + temporarySelectorFile.toFile().deleteOnExit(); + try (OutputStream fileOut = Files.newOutputStream(temporarySelectorFile)) { + fileOut.write(layerDigest.getHash().getBytes(StandardCharsets.UTF_8)); + } + + // Attempts an atomic move first, and falls back to non-atomic if the file system does not + // support atomic moves. + try { + Files.move( + temporarySelectorFile, + selectorFile, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + + } catch (AtomicMoveNotSupportedException ignored) { + Files.move(temporarySelectorFile, selectorFile, StandardCopyOption.REPLACE_EXISTING); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java deleted file mode 100644 index 21aa29a2e7..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; -import com.google.cloud.tools.jib.image.UnwrittenLayer; -import com.google.common.io.ByteStreams; -import com.google.common.io.CountingOutputStream; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** Writes {@link UnwrittenLayer}s to the cache. */ -public class CacheWriter { - - private final Cache cache; - - public CacheWriter(Cache cache) { - this.cache = cache; - } - - /** - * Builds an {@link UnwrittenLayer} from a {@link ReproducibleLayerBuilder} and compresses and - * writes the {@link UnwrittenLayer}'s uncompressed layer content BLOB to cache. - * - * @param reproducibleLayerBuilder the layer builder - * @return the cached layer with accompanying {@link LayerMetadata} - * @throws IOException if writing the layer to file fails - */ - public CachedLayerWithMetadata writeLayer(ReproducibleLayerBuilder reproducibleLayerBuilder) - throws IOException { - UnwrittenLayer unwrittenLayer = reproducibleLayerBuilder.build(); - - // Writes to a temporary file first because the UnwrittenLayer needs to be written first to - // obtain its digest. - Path tempLayerFile = Files.createTempFile(cache.getCacheDirectory(), null, null); - // TODO: Find a way to do this with java.nio.file - tempLayerFile.toFile().deleteOnExit(); - - // Writes the UnwrittenLayer layer BLOB to a file to convert into a CachedLayer. - try (CountingDigestOutputStream compressedDigestOutputStream = - new CountingDigestOutputStream( - new BufferedOutputStream(Files.newOutputStream(tempLayerFile)))) { - // Writes the layer with GZIP compression. The original bytes are captured as the layer's - // diff ID and the bytes outputted from the GZIP compression are captured as the layer's - // content descriptor. - GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream); - DescriptorDigest diffId = unwrittenLayer.getBlob().writeTo(compressorStream).getDigest(); - - // The GZIPOutputStream must be closed in order to write out the remaining compressed data. - compressorStream.close(); - BlobDescriptor compressedBlobDescriptor = compressedDigestOutputStream.toBlobDescriptor(); - - // Renames the temporary layer file to the correct filename. If the file already exists, we - // skip renaming and use the existing file. This happens if a new layer happens to have the - // same content as a previously-cached layer. - Path layerFile = getLayerFile(compressedBlobDescriptor.getDigest()); - Files.move(tempLayerFile, layerFile, StandardCopyOption.REPLACE_EXISTING); - - CachedLayer cachedLayer = new CachedLayer(layerFile, compressedBlobDescriptor, diffId); - LayerMetadata layerMetadata = - LayerMetadata.from( - reproducibleLayerBuilder.getLayerEntries(), FileTime.from(Instant.now())); - return new CachedLayerWithMetadata(cachedLayer, layerMetadata); - } - } - - /** - * @param layerDigest the written layer's digest. - * @return the {@link CountingOutputStream} to write to to cache a layer with the specified - * compressed digest. - * @throws IOException if writing to the layer file's output stream fails. - */ - public CountingOutputStream getLayerOutputStream(DescriptorDigest layerDigest) - throws IOException { - Path layerFile = getLayerFile(layerDigest); - return new CountingOutputStream(new BufferedOutputStream(Files.newOutputStream(layerFile))); - } - - /** - * Gets the layer that was cached by writing the contents to a file in the cache. The returned - * {@link CachedLayer} should be added to the cache metadata. - * - * @param layerSize the size of the layer - * @param layerDigest the digest of the layer to retrieve - * @return a {@link CachedLayer} from a layer digest and the {@link CountingOutputStream} the - * layer BLOB was written to - * @throws IOException if closing the output stream or getting the layer diff ID fails - */ - public CachedLayer getCachedLayer(long layerSize, DescriptorDigest layerDigest) - throws IOException { - Path layerFile = getLayerFile(layerDigest); - - return new CachedLayer( - layerFile, new BlobDescriptor(layerSize, layerDigest), getDiffId(layerFile)); - } - - /** - * @param compressedDigest the compressed digest of the layer to find. - * @return the path to the file for the layer with the specified compressed digest. - */ - private Path getLayerFile(DescriptorDigest compressedDigest) { - return CacheFiles.getLayerFile(cache.getCacheDirectory(), compressedDigest); - } - - /** - * @param layerFile the layer content file. - * @return the layer diff ID by decompressing the layer content file. - * @throws IOException if reading the layer file fails. - */ - private DescriptorDigest getDiffId(Path layerFile) throws IOException { - CountingDigestOutputStream diffIdCaptureOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - try (InputStream fileInputStream = new BufferedInputStream(Files.newInputStream(layerFile)); - GZIPInputStream decompressorStream = new GZIPInputStream(fileInputStream)) { - ByteStreams.copy(decompressorStream, diffIdCaptureOutputStream); - } - return diffIdCaptureOutputStream.toBlobDescriptor().getDigest(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index 063655dfeb..7683645f56 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,55 +16,104 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Layer; -import java.nio.file.Path; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; -/** - * A {@link Layer} that has been written out to a cache and has its file-backed content BLOB, - * digest, size, and diff ID. - */ +/** A reference to an image layer that is in the Cache. */ public class CachedLayer implements Layer { - private final Path contentFile; - private final BlobDescriptor blobDescriptor; - private final DescriptorDigest diffId; + /** Builds a {@link CachedLayer}. */ + public static class Builder { + + @Nullable private DescriptorDigest layerDigest; + @Nullable private DescriptorDigest layerDiffId; + private long layerSize = -1; + @Nullable private Blob layerBlob; + + private Builder() {} + + public Builder setLayerDigest(DescriptorDigest layerDigest) { + this.layerDigest = layerDigest; + return this; + } + + public Builder setLayerDiffId(DescriptorDigest layerDiffId) { + this.layerDiffId = layerDiffId; + return this; + } + + public Builder setLayerSize(long layerSize) { + this.layerSize = layerSize; + return this; + } + + public Builder setLayerBlob(Blob layerBlob) { + this.layerBlob = layerBlob; + return this; + } + + boolean hasLayerBlob() { + return layerBlob != null; + } + + /** + * Creates a CachedLayer instance. + * + * @return a new cached layer + */ + public CachedLayer build() { + return new CachedLayer( + Preconditions.checkNotNull(layerDigest, "layerDigest required"), + Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), + layerSize, + Preconditions.checkNotNull(layerBlob, "layerBlob required")); + } + } /** - * Initializes the layer with its file-backed content BLOB, content descriptor (digest and size), - * and diff ID. The {@code blobDescriptor} and {@code diffId} must match the BLOB stored in - * the file - no checks are made at runtime. + * Creates a new {@link Builder} for a {@link CachedLayer}. * - * @param contentFile the file with the layer's content BLOB - * @param blobDescriptor the content descriptor for the layer's content BLOB - * @param diffId the diff ID for the layer - * @see Layer + * @return the new {@link Builder} */ - public CachedLayer(Path contentFile, BlobDescriptor blobDescriptor, DescriptorDigest diffId) { - this.contentFile = contentFile; - this.blobDescriptor = blobDescriptor; - this.diffId = diffId; + public static Builder builder() { + return new Builder(); } - public Path getContentFile() { - return contentFile; + private final DescriptorDigest layerDiffId; + private final BlobDescriptor blobDescriptor; + private final Blob layerBlob; + + private CachedLayer( + DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, Blob layerBlob) { + this.layerDiffId = layerDiffId; + this.layerBlob = layerBlob; + this.blobDescriptor = new BlobDescriptor(layerSize, layerDigest); + } + + public DescriptorDigest getDigest() { + return blobDescriptor.getDigest(); + } + + public long getSize() { + return blobDescriptor.getSize(); } @Override - public Blob getBlob() { - return Blobs.from(contentFile); + public DescriptorDigest getDiffId() { + return layerDiffId; } @Override - public BlobDescriptor getBlobDescriptor() { - return blobDescriptor; + public Blob getBlob() { + return layerBlob; } @Override - public DescriptorDigest getDiffId() { - return diffId; + public BlobDescriptor getBlobDescriptor() { + return blobDescriptor; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java deleted file mode 100644 index 877a31ae37..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.common.annotations.VisibleForTesting; -import java.io.Closeable; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Manages both the base image layers cache and the application image layers cache. - * - *

    In general, the cache for base image layers should be shared between projects, while the cache - * for the application image layers should be specific to a single project. - */ -public class Caches implements Closeable { - - /** Initializes a {@link Caches} with directory paths. */ - public static class Initializer { - - /** A file to store in the default base image layers cache to check ownership by Jib. */ - private static final String OWNERSHIP_FILE_NAME = ".jib"; - - /** - * Ensures ownership of {@code cacheDirectory} by checking for the existence of {@link - * #OWNERSHIP_FILE_NAME}. - * - *

    This is a safety check to make sure we are not writing to a directory not created by Jib. - */ - @VisibleForTesting - static void ensureOwnership(Path cacheDirectory) - throws CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Path ownershipFile = cacheDirectory.resolve(OWNERSHIP_FILE_NAME); - - if (Files.exists(cacheDirectory)) { - // Checks for the ownership file. - if (!Files.exists(ownershipFile)) { - throw new CacheDirectoryNotOwnedException(cacheDirectory); - } - - } else { - try { - // Creates the cache directory and ownership file. - Files.createDirectories(cacheDirectory); - Files.createFile(ownershipFile); - - } catch (IOException ex) { - throw new CacheDirectoryCreationException(cacheDirectory, ex); - } - } - } - - private final Path baseImageLayersCacheDirectory; - private final boolean shouldEnsureOwnershipOfBaseImageLayersCacheDirectory; - private final Path applicationLayersCacheDirectory; - private final boolean shouldEnsureOwnershipOfApplicationLayersCacheDirectory; - - /** - * @param baseImageLayersCacheDirectory cache for the application image layers - usually not - * local to the application project - * @param shouldEnsureOwnershipOfBaseImageLayersCacheDirectory if {@code true}, ensures the base - * image layers cache directory is safe to write to - * @param applicationLayersCacheDirectory cache for the application image layers - usually local - * to the application project - * @param shouldEnsureOwnershipOfApplicationLayersCacheDirectory if {@code true}, ensures the - * base image layers cache directory is safe to write to - */ - public Initializer( - Path baseImageLayersCacheDirectory, - boolean shouldEnsureOwnershipOfBaseImageLayersCacheDirectory, - Path applicationLayersCacheDirectory, - boolean shouldEnsureOwnershipOfApplicationLayersCacheDirectory) { - this.baseImageLayersCacheDirectory = baseImageLayersCacheDirectory; - this.shouldEnsureOwnershipOfBaseImageLayersCacheDirectory = - shouldEnsureOwnershipOfBaseImageLayersCacheDirectory; - this.applicationLayersCacheDirectory = applicationLayersCacheDirectory; - this.shouldEnsureOwnershipOfApplicationLayersCacheDirectory = - shouldEnsureOwnershipOfApplicationLayersCacheDirectory; - } - - public Caches init() - throws CacheMetadataCorruptedException, CacheDirectoryNotOwnedException, - CacheDirectoryCreationException, IOException { - if (shouldEnsureOwnershipOfBaseImageLayersCacheDirectory) { - ensureOwnership(baseImageLayersCacheDirectory); - } - if (shouldEnsureOwnershipOfApplicationLayersCacheDirectory) { - ensureOwnership(applicationLayersCacheDirectory); - } - - return new Caches(baseImageLayersCacheDirectory, applicationLayersCacheDirectory); - } - } - - private final Cache baseCache; - private final Cache applicationCache; - - /** Instantiate with {@link Initializer#init}. */ - private Caches(Path baseCacheDirectory, Path applicationCacheDirectory) - throws CacheMetadataCorruptedException, IOException { - applicationCache = Cache.init(applicationCacheDirectory); - - // Ensures that only one Cache is initialized if using the same directory. - if (Files.isSameFile(baseCacheDirectory, applicationCacheDirectory)) { - baseCache = applicationCache; - } else { - baseCache = Cache.init(baseCacheDirectory); - } - } - - public Cache getBaseCache() { - return baseCache; - } - - public Cache getApplicationCache() { - return applicationCache; - } - - @Override - public void close() throws IOException { - applicationCache.close(); - - if (baseCache != applicationCache) { - baseCache.close(); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java new file mode 100644 index 0000000000..91172971c0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -0,0 +1,173 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Generates a selector based on {@link FileEntry}s for a layer. Selectors are secondary references + * for a cache entries. + * + *

    The selector is the SHA256 hash of the list of layer entries serialized in the following form: + * + *

    {@code
    + * [
    + *   {
    + *     "sourceFile": "source/file/for/layer/entry/1",
    + *     "extractionPath": "/extraction/path/for/layer/entry/1"
    + *     "sourceModificationTime": "2018-10-03T15:48:32.416152Z"
    + *     "targetModificationTime": "1970-01-01T00:00:01Z",
    + *     "permissions": "777",
    + *     "ownership": "0:0"
    + *   },
    + *   {
    + *     "sourceFile": "source/file/for/layer/entry/2",
    + *     "extractionPath": "/extraction/path/for/layer/entry/2"
    + *     "sourceModificationTime": "2018-10-03T15:48:32.416152Z"
    + *     "targetModificationTime": "1970-01-01T00:00:01Z",
    + *     "permissions": "777",
    + *     "ownership": "alice:1234"
    + *   }
    + * ]
    + * }
    + */ +class LayerEntriesSelector { + + /** Serialized form of a {@link FileEntry}. */ + @VisibleForTesting + static class LayerEntryTemplate implements JsonTemplate, Comparable { + + private final String sourceFile; + private final String extractionPath; + private final Instant sourceModificationTime; + private final Instant targetModificationTime; + private final String permissions; + private final String ownership; + + @VisibleForTesting + LayerEntryTemplate(FileEntry layerEntry) throws IOException { + sourceFile = layerEntry.getSourceFile().toAbsolutePath().toString(); + extractionPath = layerEntry.getExtractionPath().toString(); + sourceModificationTime = Files.getLastModifiedTime(layerEntry.getSourceFile()).toInstant(); + targetModificationTime = layerEntry.getModificationTime(); + permissions = layerEntry.getPermissions().toOctalString(); + ownership = layerEntry.getOwnership(); + } + + @Override + public int compareTo(LayerEntryTemplate otherLayerEntryTemplate) { + int sourceFileComparison = sourceFile.compareTo(otherLayerEntryTemplate.sourceFile); + if (sourceFileComparison != 0) { + return sourceFileComparison; + } + int extractionPathComparison = + extractionPath.compareTo(otherLayerEntryTemplate.extractionPath); + if (extractionPathComparison != 0) { + return extractionPathComparison; + } + int sourceModificationTimeComparison = + sourceModificationTime.compareTo(otherLayerEntryTemplate.sourceModificationTime); + if (sourceModificationTimeComparison != 0) { + return sourceModificationTimeComparison; + } + int targetModificationTimeComparison = + targetModificationTime.compareTo(otherLayerEntryTemplate.targetModificationTime); + if (targetModificationTimeComparison != 0) { + return targetModificationTimeComparison; + } + int permissionsComparison = permissions.compareTo(otherLayerEntryTemplate.permissions); + if (permissionsComparison != 0) { + return permissionsComparison; + } + return ownership.compareTo(otherLayerEntryTemplate.ownership); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LayerEntryTemplate)) { + return false; + } + LayerEntryTemplate otherLayerEntryTemplate = (LayerEntryTemplate) other; + return sourceFile.equals(otherLayerEntryTemplate.sourceFile) + && extractionPath.equals(otherLayerEntryTemplate.extractionPath) + && sourceModificationTime.equals(otherLayerEntryTemplate.sourceModificationTime) + && targetModificationTime.equals(otherLayerEntryTemplate.targetModificationTime) + && permissions.equals(otherLayerEntryTemplate.permissions) + && ownership.equals(otherLayerEntryTemplate.ownership); + } + + @Override + public int hashCode() { + return Objects.hash( + sourceFile, + extractionPath, + sourceModificationTime, + targetModificationTime, + permissions, + ownership); + } + } + + /** + * Converts a list of {@link FileEntry}s into a list of {@link LayerEntryTemplate}. The list is + * sorted by source file first, then extraction path (see {@link LayerEntryTemplate#compareTo}). + * + * @param layerEntries the list of {@link FileEntry} to convert + * @return list of {@link LayerEntryTemplate} after sorting + * @throws IOException if checking the file creation time of a layer entry fails + */ + @VisibleForTesting + static List toSortedJsonTemplates(List layerEntries) + throws IOException { + List jsonTemplates = new ArrayList<>(); + for (FileEntry entry : layerEntries) { + jsonTemplates.add(new LayerEntryTemplate(entry)); + } + Collections.sort(jsonTemplates); + return jsonTemplates; + } + + /** + * Generates a selector for the list of {@link FileEntry}s. The selector is unique to each unique + * set of layer entries, regardless of order. TODO: Should we care about order? + * + * @param layerEntries the layer entries + * @return the selector + * @throws IOException if an I/O exception occurs + */ + static DescriptorDigest generateSelector(ImmutableList layerEntries) + throws IOException { + return Digests.computeJsonDigest(toSortedJsonTemplates(layerEntries)); + } + + private LayerEntriesSelector() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java deleted file mode 100644 index 5350b6313b..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; -import java.util.List; - -/** - * Metadata about an application layer stored in the cache. This is part of the {@link - * CacheMetadata}. - */ -class LayerMetadata { - - /** Entry into the layer metadata. */ - static class LayerMetadataEntry { - - private ImmutableList sourceFilesStrings; - private String extractionPath; - - ImmutableList getSourceFilesStrings() { - return sourceFilesStrings; - } - - String getExtractionPath() { - return extractionPath; - } - - private LayerMetadataEntry(ImmutableList sourceFilesStrings, String extractionPath) { - this.sourceFilesStrings = sourceFilesStrings; - this.extractionPath = extractionPath; - } - } - - static LayerMetadata from(ImmutableList layerEntries, FileTime lastModifiedTime) { - ImmutableList.Builder entries = - ImmutableList.builderWithExpectedSize(layerEntries.size()); - - for (LayerEntry layerEntry : layerEntries) { - List sourceFiles = layerEntry.getSourceFiles(); - ImmutableList.Builder sourceFilesStrings = - ImmutableList.builderWithExpectedSize(sourceFiles.size()); - for (Path sourceFile : sourceFiles) { - sourceFilesStrings.add(sourceFile.toString()); - } - entries.add( - new LayerMetadataEntry(sourceFilesStrings.build(), layerEntry.getExtractionPath())); - } - - return new LayerMetadata(entries.build(), lastModifiedTime); - } - - /** The entries that define the layer contents. */ - private ImmutableList entries; - - /** The last time the layer was constructed. */ - private final FileTime lastModifiedTime; - - LayerMetadata(ImmutableList entries, FileTime lastModifiedTime) { - this.entries = entries; - this.lastModifiedTime = lastModifiedTime; - } - - ImmutableList getEntries() { - return entries; - } - - FileTime getLastModifiedTime() { - return lastModifiedTime; - } - - @VisibleForTesting - void setEntry(ImmutableList sourceFilesStrings, String extractionPath) { - entries = ImmutableList.of(new LayerMetadataEntry(sourceFilesStrings, extractionPath)); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java new file mode 100644 index 0000000000..384f2119c6 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.common.base.Preconditions; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +/** + * Retries an action until it succeeds, or has retried too often and failed. By default the action + * will be run up to 5 times. The action is deemed successful if it runs to completion without + * throwing an exception, and returns true. + * + *
      + *
    • Exceptions are caught and, if deemed {@link #retryOnException(Predicate) retryable} then + * the action will be re-attempted. By default, any exception is considered retryable. + *
    • The retry instance can be configured to {@link #sleep(long, TimeUnit) sleep between + * retries}. + *
    • The maximum retry count {@link #maximumRetries(int) is configurable} (5 times by default). + *
    + * + * @param the class of exceptions that may be thrown + */ +public class Retry { + + /** A runnable action that may throw an exception of type {@code E}. */ + @FunctionalInterface + public interface Action { + /** + * Perform the action. + * + * @return {@code true} if the action was successful and {@code false} otherwise + * @throws E exception thrown during the action + */ + boolean run() throws E; + } + + /** + * Create a retryable action. + * + * @param action the action to be run + * @param the class of exceptions that may be thrown + * @return the instance + */ + public static Retry action(Action action) { + return new Retry<>(action); + } + + private final Action action; + private int maximumRetries = 5; + private Predicate retryOnException = ignored -> true; // continue to retry + private long sleepMilliseconds = -1; // no sleep + + private Retry(Action action) { + this.action = action; + } + + /** + * Configure the maximum number of retries. + * + * @param maximumRetries the number of retries, must be zero or more + * @return this Retry instance + */ + public Retry maximumRetries(int maximumRetries) { + Preconditions.checkArgument(maximumRetries > 0); + this.maximumRetries = maximumRetries; + return this; + } + + /** + * Provide a predicate to determine if a thrown exception can be retried. + * + * @param retryOnException determine if provided exception is retryable. + * @return the instance for further configuration + */ + public Retry retryOnException(Predicate retryOnException) { + this.retryOnException = retryOnException; + return this; + } + + /** + * Set the sleep time between retries. + * + * @param duration the time to sleep + * @param unit the unit of time of duration + * @return the instance for further configuration + */ + public Retry sleep(long duration, TimeUnit unit) { + Preconditions.checkArgument(duration >= 0); + this.sleepMilliseconds = unit.convert(duration, TimeUnit.MILLISECONDS); + return this; + } + + /** + * Run the action until it runs successfully, to a {@link #maximumRetries(int) maximum number of + * retries} (default: 5). If an exception occurs then the action will be retried providing {@link + * #retryOnException(Predicate) the exception is retryable}. + * + * @return true if the action was run successfully, or {@code false} if the action was unable to + * complete + * @throws E exception thrown during the action + */ + public boolean run() throws E { + for (int i = 0; i < maximumRetries; i++) { + try { + // sleep between attempts, but not on the first attempt + if (i > 0 && sleepMilliseconds >= 0) { + Thread.sleep(sleepMilliseconds); + } + + // Do we need to continue? + if (action.run()) { + return true; + } + + } catch (InterruptedException ex) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + return false; + + } catch (Exception ex) { + // if this is the last iteration, no more retries + if (i + 1 == maximumRetries || !retryOnException.test(ex)) { + throw ex; + } + } + } + // we did not complete + return false; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java deleted file mode 100644 index 6f678b18d5..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.json.JsonTemplate; -import javax.annotation.Nullable; - -/** - * Inner JSON template for storing metadata about a layer in the cache as part of {@link - * CacheMetadataTemplate}. - * - * @see CacheMetadataTemplate for example - */ -public class CacheMetadataLayerObjectTemplate implements JsonTemplate { - - /** The reference to the layer. */ - private final ReferenceObject reference = new ReferenceObject(); - - /** Additional properties for the layer. */ - @Nullable private CacheMetadataLayerPropertiesObjectTemplate properties; - - /** - * The reference for a layer consists of its size (in bytes), digest, and diff ID. - * - * @see Layer for details - */ - private static class ReferenceObject implements JsonTemplate { - - private long size; - @Nullable private DescriptorDigest digest; - @Nullable private DescriptorDigest diffId; - } - - public long getSize() { - return reference.size; - } - - @Nullable - public DescriptorDigest getDigest() { - return reference.digest; - } - - @Nullable - public DescriptorDigest getDiffId() { - return reference.diffId; - } - - @Nullable - public CacheMetadataLayerPropertiesObjectTemplate getProperties() { - return properties; - } - - public CacheMetadataLayerObjectTemplate setSize(long size) { - reference.size = size; - return this; - } - - public CacheMetadataLayerObjectTemplate setDigest(DescriptorDigest digest) { - reference.digest = digest; - return this; - } - - public CacheMetadataLayerObjectTemplate setDiffId(DescriptorDigest diffId) { - reference.diffId = diffId; - return this; - } - - public CacheMetadataLayerObjectTemplate setProperties( - CacheMetadataLayerPropertiesObjectTemplate properties) { - this.properties = properties; - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java deleted file mode 100644 index 67e1d65f3f..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.json.JsonTemplate; -import java.nio.file.attribute.FileTime; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Inner JSON template for extra properties for an application layer, as part of {@link - * CacheMetadataLayerObjectTemplate}. - */ -public class CacheMetadataLayerPropertiesObjectTemplate implements JsonTemplate { - - /** Represents a pair of source files and extraction path. */ - public static class LayerEntryTemplate implements JsonTemplate { - - /** The paths to the source files that the layer was constructed from. */ - @Nullable private List sourceFiles; - - /** The intended path to extract the source files to in the container. */ - @Nullable private String extractionPath; - - @Nullable - public List getSourceFiles() { - return sourceFiles; - } - - @Nullable - public String getExtractionPath() { - return extractionPath; - } - - public LayerEntryTemplate(List sourceFiles, String extractionPath) { - this.sourceFiles = sourceFiles; - this.extractionPath = extractionPath; - } - - public LayerEntryTemplate() {} - } - - /** The content entries for the layer. */ - private List layerEntries = Collections.emptyList(); - - /** The last time the layer was constructed. */ - private long lastModifiedTime; - - public List getLayerEntries() { - return layerEntries; - } - - public FileTime getLastModifiedTime() { - return FileTime.fromMillis(lastModifiedTime); - } - - public CacheMetadataLayerPropertiesObjectTemplate setLayerEntries( - List layerEntries) { - this.layerEntries = layerEntries; - return this; - } - - public CacheMetadataLayerPropertiesObjectTemplate setLastModifiedTime(FileTime lastModifiedTime) { - this.lastModifiedTime = lastModifiedTime.toMillis(); - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java deleted file mode 100644 index 302940c275..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.json.JsonTemplate; -import java.util.ArrayList; -import java.util.List; - -/** - * JSON template for storing metadata about the cache. - * - *

    Example: - * - *

    {@code
    - * {
    - *   "layers": [
    - *     {
    - *       // This is a base image layer.
    - *       "reference": {
    - *         "size": 631,
    - *         "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
    - *         "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"
    - *       }
    - *     },
    - *     ...
    - *     {
    - *       // This is an application layer (it has properties).
    - *       "reference": {
    - *         "size": 223,
    - *         "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad",
    - *         "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"
    - *       }
    - *       "properties": {
    - *         "layerEntries": [
    - *           {
    - *             "sourceFiles": ["build/classes"],
    - *             "extractionPath": "/app/classes"
    - *           },
    - *         ],
    - *         "lastModifiedTime": 255073580723571
    - *       }
    - *     },
    - *     ...
    - *   ]
    - * }
    - * }
    - */ -public class CacheMetadataTemplate implements JsonTemplate { - - private final List layers = new ArrayList<>(); - - public List getLayers() { - return layers; - } - - public CacheMetadataTemplate addLayer(CacheMetadataLayerObjectTemplate layer) { - layers.add(layer); - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java new file mode 100644 index 0000000000..31f29eb3c1 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java @@ -0,0 +1,632 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.annotation.Nullable; + +/** + * Build context for the builder process. Includes static build configuration options as well as + * various services for execution (such as event dispatching, thread execution service, and HTTP + * client). Informational instances (particularly configuration options such as {@link + * ContainerConfiguration}, {@link ImageConfiguration}, and {@link FileEntriesLayer}) held in are + * immutable. + */ +public class BuildContext implements Closeable { + + /** The default target format of the container manifest. */ + private static final Class DEFAULT_TARGET_FORMAT = + V22ManifestTemplate.class; + + /** The default tool identifier. */ + private static final String DEFAULT_TOOL_NAME = "jib"; + + /** Builds an immutable {@link BuildContext}. Instantiate with {@link #builder}. */ + public static class Builder { + + // All the parameters below are set to their default values. + @Nullable private ImageConfiguration baseImageConfiguration; + @Nullable private ImageConfiguration targetImageConfiguration; + private ImmutableSet additionalTargetImageTags = ImmutableSet.of(); + @Nullable private ContainerConfiguration containerConfiguration; + @Nullable private Path applicationLayersCacheDirectory; + @Nullable private Path baseImageLayersCacheDirectory; + private boolean allowInsecureRegistries = false; + private boolean offline = false; + private ImmutableList layerConfigurations = ImmutableList.of(); + private Class targetFormat = DEFAULT_TARGET_FORMAT; + private String toolName = DEFAULT_TOOL_NAME; + @Nullable private String toolVersion; + private EventHandlers eventHandlers = EventHandlers.NONE; + @Nullable private ExecutorService executorService; + private boolean alwaysCacheBaseImage = false; + private ImmutableListMultimap registryMirrors = ImmutableListMultimap.of(); + private boolean enablePlatformTags = false; + + private Builder() {} + + /** + * Sets the base image configuration. + * + * @param imageConfiguration the {@link ImageConfiguration} describing the base image + * @return this + */ + public Builder setBaseImageConfiguration(ImageConfiguration imageConfiguration) { + baseImageConfiguration = imageConfiguration; + return this; + } + + /** + * Sets the target image configuration. + * + * @param imageConfiguration the {@link ImageConfiguration} describing the target image + * @return this + */ + public Builder setTargetImageConfiguration(ImageConfiguration imageConfiguration) { + targetImageConfiguration = imageConfiguration; + return this; + } + + /** + * Sets the tags to tag the target image with (in addition to the tag in the target image + * configuration image reference set via {@link #setTargetImageConfiguration}). + * + * @param tags a set of tags + * @return this + */ + public Builder setAdditionalTargetImageTags(Set tags) { + additionalTargetImageTags = ImmutableSet.copyOf(tags); + return this; + } + + /** + * Sets whether to automatically add architecture suffix to tags for platform-specific images + * when building multi-platform images. For example, when building amd64 and arm64 images for a + * given tag, the final tags will be {@code -amd64} and {@code -arm64}. + * + * @param enablePlatformTags whether to append architecture suffix to tags + * @return this + */ + public Builder setEnablePlatformTags(boolean enablePlatformTags) { + this.enablePlatformTags = enablePlatformTags; + return this; + } + + /** + * Sets configuration parameters for the container. + * + * @param containerConfiguration the {@link ContainerConfiguration} + * @return this + */ + public Builder setContainerConfiguration(ContainerConfiguration containerConfiguration) { + this.containerConfiguration = containerConfiguration; + return this; + } + + /** + * Sets the location of the cache for storing application layers. + * + * @param applicationLayersCacheDirectory the application layers cache directory + * @return this + */ + public Builder setApplicationLayersCacheDirectory(Path applicationLayersCacheDirectory) { + this.applicationLayersCacheDirectory = applicationLayersCacheDirectory; + return this; + } + + /** + * Sets the location of the cache for storing base image layers. + * + * @param baseImageLayersCacheDirectory the base image layers cache directory + * @return this + */ + public Builder setBaseImageLayersCacheDirectory(Path baseImageLayersCacheDirectory) { + this.baseImageLayersCacheDirectory = baseImageLayersCacheDirectory; + return this; + } + + /** + * Sets the target format of the container image. + * + * @param targetFormat the target format + * @return this + */ + public Builder setTargetFormat(ImageFormat targetFormat) { + this.targetFormat = + targetFormat == ImageFormat.Docker + ? V22ManifestTemplate.class + : OciManifestTemplate.class; + return this; + } + + /** + * Sets whether or not to allow communication over HTTP (as opposed to HTTPS). + * + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed + * @return this + */ + public Builder setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; + return this; + } + + /** + * Sets whether or not to perform the build in offline mode. + * + * @param offline if {@code true}, the build will run in offline mode + * @return this + */ + public Builder setOffline(boolean offline) { + this.offline = offline; + return this; + } + + /** + * Controls the optimization which skips downloading base image layers that exist in a target + * registry. If the user does not set this property then read as false. + * + * @param alwaysCacheBaseImage if {@code true}, base image layers are always pulled and cached. + * If {@code false}, base image layers will not be pulled/cached if they already exist on + * the target registry. + * @return this + */ + public Builder setAlwaysCacheBaseImage(boolean alwaysCacheBaseImage) { + this.alwaysCacheBaseImage = alwaysCacheBaseImage; + return this; + } + + /** + * Sets the layers to build. + * + * @param layerConfigurations the configurations for the layers + * @return this + */ + public Builder setLayerConfigurations(List layerConfigurations) { + this.layerConfigurations = ImmutableList.copyOf(layerConfigurations); + return this; + } + + /** + * Sets the name of the tool that is executing the build. + * + * @param toolName the tool name + * @return this + */ + public Builder setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Sets the version of the tool that is executing the build. + * + * @param toolVersion the tool version + * @return this + */ + public Builder setToolVersion(@Nullable String toolVersion) { + this.toolVersion = toolVersion; + return this; + } + + /** + * Sets the {@link EventHandlers} to dispatch events with. + * + * @param eventHandlers the {@link EventHandlers} + * @return this + */ + public Builder setEventHandlers(EventHandlers eventHandlers) { + this.eventHandlers = eventHandlers; + return this; + } + + /** + * Sets the {@link ExecutorService} Jib executes on. By default, Jib uses {@link + * Executors#newCachedThreadPool}. + * + * @param executorService the {@link ExecutorService} + * @return this + */ + public Builder setExecutorService(@Nullable ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + /** + * Sets the registry mirrors. + * + * @param registryMirrors registry mirrors + * @return this + */ + public Builder setRegistryMirrors(ListMultimap registryMirrors) { + this.registryMirrors = ImmutableListMultimap.copyOf(registryMirrors); + return this; + } + + /** + * Builds a new {@link BuildContext} using the parameters passed into the builder. + * + * @return the corresponding build context + * @throws CacheDirectoryCreationException if I/O exception occurs when creating cache directory + */ + public BuildContext build() throws CacheDirectoryCreationException { + // Validates the parameters. + List missingFields = new ArrayList<>(); + if (baseImageConfiguration == null) { + missingFields.add("base image configuration"); + } + if (targetImageConfiguration == null) { + missingFields.add("target image configuration"); + } + if (containerConfiguration == null) { + missingFields.add("container configuration"); + } + if (baseImageLayersCacheDirectory == null) { + missingFields.add("base image layers cache directory"); + } + if (applicationLayersCacheDirectory == null) { + missingFields.add("application layers cache directory"); + } + + switch (missingFields.size()) { + case 0: // No errors + Preconditions.checkNotNull(baseImageConfiguration); + if (!baseImageConfiguration.getImage().getDigest().isPresent() + && !baseImageConfiguration.getImage().isScratch()) { + eventHandlers.dispatch( + LogEvent.warn( + "Base image '" + + baseImageConfiguration.getImage() + + "' does not use a specific image digest - build may not be reproducible")); + } + + return new BuildContext( + baseImageConfiguration, + Verify.verifyNotNull(targetImageConfiguration), + additionalTargetImageTags, + Verify.verifyNotNull(containerConfiguration), + Cache.withDirectory(Preconditions.checkNotNull(baseImageLayersCacheDirectory)), + Cache.withDirectory(Preconditions.checkNotNull(applicationLayersCacheDirectory)), + targetFormat, + offline, + layerConfigurations, + toolName, + toolVersion, + eventHandlers, + // TODO: try setting global User-Agent: here + new FailoverHttpClient( + allowInsecureRegistries, + JibSystemProperties.sendCredentialsOverHttp(), + eventHandlers::dispatch), + executorService == null ? Executors.newCachedThreadPool() : executorService, + executorService == null, // shutDownExecutorService + alwaysCacheBaseImage, + registryMirrors, + enablePlatformTags); + + case 1: + throw new IllegalStateException(missingFields.get(0) + " is required but not set"); + + case 2: + throw new IllegalStateException( + missingFields.get(0) + " and " + missingFields.get(1) + " are required but not set"); + + default: + missingFields.add("and " + missingFields.remove(missingFields.size() - 1)); + StringJoiner errorMessage = new StringJoiner(", ", "", " are required but not set"); + for (String missingField : missingFields) { + errorMessage.add(missingField); + } + throw new IllegalStateException(errorMessage.toString()); + } + } + + @Nullable + @VisibleForTesting + Path getBaseImageLayersCacheDirectory() { + return baseImageLayersCacheDirectory; + } + + @Nullable + @VisibleForTesting + Path getApplicationLayersCacheDirectory() { + return applicationLayersCacheDirectory; + } + } + + /** + * Creates a new {@link Builder} to build a {@link BuildContext}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + private final ImageConfiguration baseImageConfiguration; + private final ImageConfiguration targetImageConfiguration; + private final ImmutableSet additionalTargetImageTags; + private final ContainerConfiguration containerConfiguration; + private final Cache baseImageLayersCache; + private final Cache applicationLayersCache; + private Class targetFormat; + private final boolean offline; + private final ImmutableList layerConfigurations; + private final String toolName; + @Nullable private final String toolVersion; + private final EventHandlers eventHandlers; + private final FailoverHttpClient httpClient; + private final ExecutorService executorService; + private final boolean shutDownExecutorService; + private final boolean alwaysCacheBaseImage; + private final ImmutableListMultimap registryMirrors; + private final boolean enablePlatformTags; + + /** Instantiate with {@link #builder}. */ + private BuildContext( + ImageConfiguration baseImageConfiguration, + ImageConfiguration targetImageConfiguration, + ImmutableSet additionalTargetImageTags, + ContainerConfiguration containerConfiguration, + Cache baseImageLayersCache, + Cache applicationLayersCache, + Class targetFormat, + boolean offline, + ImmutableList layerConfigurations, + String toolName, + @Nullable String toolVersion, + EventHandlers eventHandlers, + FailoverHttpClient httpClient, + ExecutorService executorService, + boolean shutDownExecutorService, + boolean alwaysCacheBaseImage, + ImmutableListMultimap registryMirrors, + boolean enablePlatformTags) { + this.baseImageConfiguration = baseImageConfiguration; + this.targetImageConfiguration = targetImageConfiguration; + this.additionalTargetImageTags = additionalTargetImageTags; + this.containerConfiguration = containerConfiguration; + this.baseImageLayersCache = baseImageLayersCache; + this.applicationLayersCache = applicationLayersCache; + this.targetFormat = targetFormat; + this.offline = offline; + this.layerConfigurations = layerConfigurations; + this.toolName = toolName; + this.toolVersion = toolVersion; + this.eventHandlers = eventHandlers; + this.httpClient = httpClient; + this.executorService = executorService; + this.shutDownExecutorService = shutDownExecutorService; + this.alwaysCacheBaseImage = alwaysCacheBaseImage; + this.registryMirrors = registryMirrors; + this.enablePlatformTags = enablePlatformTags; + } + + public ImageConfiguration getBaseImageConfiguration() { + return baseImageConfiguration; + } + + public boolean getEnablePlatformTags() { + return enablePlatformTags; + } + + public ImageConfiguration getTargetImageConfiguration() { + return targetImageConfiguration; + } + + /** + * Returns all image tags configured for this build. + * + * @return the set of image tags configured for this build + */ + public ImmutableSet getAllTargetImageTags() { + ImmutableSet.Builder allTargetImageTags = + ImmutableSet.builderWithExpectedSize(1 + additionalTargetImageTags.size()); + allTargetImageTags.add(targetImageConfiguration.getImageQualifier()); + allTargetImageTags.addAll(additionalTargetImageTags); + return allTargetImageTags.build(); + } + + public ContainerConfiguration getContainerConfiguration() { + return containerConfiguration; + } + + public Class getTargetFormat() { + return targetFormat; + } + + public String getToolName() { + return toolName; + } + + @Nullable + public String getToolVersion() { + return toolVersion; + } + + public EventHandlers getEventHandlers() { + return eventHandlers; + } + + public ExecutorService getExecutorService() { + return executorService; + } + + /** + * Gets the {@link Cache} for base image layers. + * + * @return the {@link Cache} for base image layers + */ + public Cache getBaseImageLayersCache() { + return baseImageLayersCache; + } + + /** + * Gets the {@link Cache} for application layers. + * + * @return the {@link Cache} for application layers + */ + public Cache getApplicationLayersCache() { + return applicationLayersCache; + } + + /** + * Gets whether or not to run the build in offline mode. + * + * @return {@code true} if the build will run in offline mode; {@code false} otherwise + */ + public boolean isOffline() { + return offline; + } + + /** + * Gets whether or not to force caching the base images. + * + * @return {@code true} if the user wants to force the build to always pull the image layers. + */ + public boolean getAlwaysCacheBaseImage() { + return alwaysCacheBaseImage; + } + + /** + * Gets the configurations for building the layers. + * + * @return the list of layer configurations + */ + public ImmutableList getLayerConfigurations() { + return layerConfigurations; + } + + /** + * Gets the registry mirrors. + * + * @return the registry mirrors + */ + public ImmutableListMultimap getRegistryMirrors() { + return registryMirrors; + } + + /** + * Creates a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} for the base + * image with fields from the build configuration. The server URL is derived from the base {@link + * ImageConfiguration}. + * + * @return a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} + */ + public RegistryClient.Factory newBaseImageRegistryClientFactory() { + return newBaseImageRegistryClientFactory(baseImageConfiguration.getImageRegistry()); + } + + /** + * Creates a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} for the base + * image repository on the registry {@code serverUrl}. Compared to @link + * #newBaseImageRegistryClientFactory()), this method is useful to try a mirror. + * + * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) + * @return a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} + */ + public RegistryClient.Factory newBaseImageRegistryClientFactory(String serverUrl) { + return RegistryClient.factory( + getEventHandlers(), serverUrl, baseImageConfiguration.getImageRepository(), httpClient) + .setUserAgent(makeUserAgent()); + } + + /** + * Creates a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} for the target + * image with fields from the build configuration. + * + * @return a new {@link com.google.cloud.tools.jib.registry.RegistryClient.Factory} + */ + public RegistryClient.Factory newTargetImageRegistryClientFactory() { + // if base and target are on the same registry, try enabling cross-repository mounts + if (baseImageConfiguration + .getImageRegistry() + .equals(targetImageConfiguration.getImageRegistry())) { + return RegistryClient.factory( + getEventHandlers(), + targetImageConfiguration.getImageRegistry(), + targetImageConfiguration.getImageRepository(), + baseImageConfiguration.getImageRepository(), + httpClient) + .setUserAgent(makeUserAgent()); + } + return RegistryClient.factory( + getEventHandlers(), + targetImageConfiguration.getImageRegistry(), + targetImageConfiguration.getImageRepository(), + httpClient) + .setUserAgent(makeUserAgent()); + } + + @Override + public void close() throws IOException { + if (shutDownExecutorService) { + executorService.shutdown(); + } + httpClient.shutDown(); + } + + /** + * The {@code User-Agent} is in the form of {@code jib }. For example: + * {@code jib 0.9.0 jib-maven-plugin}. + * + * @return the {@code User-Agent} header to send. The {@code User-Agent} can be disabled by + * setting the system property variable {@code _JIB_DISABLE_USER_AGENT} to any non-empty + * string. + */ + @VisibleForTesting + String makeUserAgent() { + if (!JibSystemProperties.isUserAgentEnabled()) { + return ""; + } + + StringBuilder userAgentBuilder = new StringBuilder("jib"); + userAgentBuilder.append(" ").append(toolVersion); + userAgentBuilder.append(" ").append(toolName); + if (!Strings.isNullOrEmpty(System.getProperty(JibSystemProperties.UPSTREAM_CLIENT))) { + userAgentBuilder.append(" ").append(System.getProperty(JibSystemProperties.UPSTREAM_CLIENT)); + } + return userAgentBuilder.toString(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java deleted file mode 100644 index 5d665cae98..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.filesystem.UserCacheHome; -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Represents the location of the cache. Provides static methods for resolving a location for the - * cache. - */ -public class CacheConfiguration { - - /** - * The default directory for caching the base image layers, in {@code [user cache - * home]/google-cloud-tools-java/jib}. - */ - @VisibleForTesting - static final Path DEFAULT_BASE_CACHE_DIRECTORY = - UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); - - /** - * The cache is at an arbitrary path. - * - * @param cacheDirectory the path to the cache directory. This must be a non-existent directory or - * a previously-used cache directory. - * @return the corresponding {@link CacheConfiguration} - */ - public static CacheConfiguration forPath(Path cacheDirectory) { - return new CacheConfiguration(cacheDirectory, true); - } - - /** - * The cache is a temporary directory that is deleted afterwards. - * - * @return the corresponding {@link CacheConfiguration} - * @throws CacheDirectoryCreationException if a temporary directory cannot be created - */ - public static CacheConfiguration makeTemporary() throws CacheDirectoryCreationException { - try { - Path temporaryDirectory = Files.createTempDirectory(null); - temporaryDirectory.toFile().deleteOnExit(); - return new CacheConfiguration(temporaryDirectory, false); - - } catch (IOException ex) { - throw new CacheDirectoryCreationException(ex); - } - } - - /** - * The cache is at the default user-level cache directory. This is usually to store base image - * layers, which can be shared between projects. The default user-level cache directory is {@code - * [user cache home]/google-cloud-tools-java/jib}. - * - * @return the corresponding {@link CacheConfiguration} - */ - public static CacheConfiguration forDefaultUserLevelCacheDirectory() { - return new CacheConfiguration(DEFAULT_BASE_CACHE_DIRECTORY, true); - } - - private final Path cacheDirectory; - private final boolean shouldEnsureOwnership; - - private CacheConfiguration(Path cacheDirectory, boolean shouldEnsureOwnership) { - this.cacheDirectory = cacheDirectory; - this.shouldEnsureOwnership = shouldEnsureOwnership; - } - - /** - * Gets the path to the cache directory. - * - * @return the cache directory path - */ - public Path getCacheDirectory() { - return cacheDirectory; - } - - /** - * Gets whether or not the cache directory should be checked for write safety. - * - * @return {@code true} if ownership by Jib should be checked - */ - public boolean shouldEnsureOwnership() { - return shouldEnsureOwnership; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java new file mode 100644 index 0000000000..f152705b7d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -0,0 +1,450 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** Immutable configuration options for the container. */ +public class ContainerConfiguration { + + /** Builder for instantiating a {@link ContainerConfiguration}. */ + public static class Builder { + + /** + * The default creation time of the container (constant to ensure reproducibility by default). + */ + private static final Instant DEFAULT_CREATION_TIME = Instant.EPOCH; + + // LinkedHashSet to preserve the order + private Set platforms = + new LinkedHashSet<>(Collections.singleton(new Platform("amd64", "linux"))); + private Instant creationTime = DEFAULT_CREATION_TIME; + @Nullable private ImmutableList entrypoint; + @Nullable private ImmutableList programArguments; + @Nullable private Map environmentMap; + @Nullable private Set exposedPorts; + @Nullable private Set volumes; + @Nullable private Map labels; + @Nullable private String user; + @Nullable private AbsoluteUnixPath workingDirectory; + + /** + * Sets a desired platform (properties including OS and architecture) list. If the base image + * reference is a Docker manifest list or an OCI image index, an image builder may select the + * base images matching the given platforms. If the base image reference is an image manifest, + * an image builder may ignore the given platforms and use the platform of the base image or may + * decide to raise on error. + * + *

    Note that a new container configuration starts with "amd64/linux" as the default platform. + * + * @param platforms list of platforms to select base images in case of a manifest list + * @return this + */ + public Builder setPlatforms(Set platforms) { + Preconditions.checkArgument(!platforms.isEmpty(), "platforms set cannot be empty"); + this.platforms = new LinkedHashSet<>(platforms); + return this; + } + + /** + * Adds a desired image platform (OS and architecture pair). If the base image reference is a + * Docker manifest list or an OCI image index, an image builder may select the base image + * matching the given platform. If the base image reference is an image manifest, an image + * builder may ignore the given platform and use the platform of the base image or may decide to + * raise on error. + * + *

    Note that a new container configuration starts with "amd64/linux" as the default platform. + * If you want to reset the default platform instead of adding a new one, use {@link + * #setPlatforms(Set)}. + * + * @param architecture architecture (for example, {@code amd64}) to select a base image in case + * of a manifest list + * @param os OS (for example, {@code linux}) to select a base image in case of a manifest list + * @return this + */ + public Builder addPlatform(String architecture, String os) { + platforms.add(new Platform(architecture, os)); + return this; + } + + /** + * Sets the image creation time. + * + * @param creationTime the creation time + * @return this + */ + public Builder setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + + /** + * Sets the commandline arguments for main. + * + * @param programArguments the list of arguments + * @return this + */ + public Builder setProgramArguments(@Nullable List programArguments) { + if (programArguments == null) { + this.programArguments = null; + } else { + Preconditions.checkArgument( + programArguments.stream().allMatch(Objects::nonNull), + "program arguments list contains null elements"); + this.programArguments = ImmutableList.copyOf(programArguments); + } + return this; + } + + /** + * Sets the container's environment variables, mapping variable name to value. + * + * @param environmentMap the map + * @return this + */ + public Builder setEnvironment(@Nullable Map environmentMap) { + if (environmentMap == null) { + this.environmentMap = null; + } else { + Preconditions.checkArgument( + !Iterables.any(environmentMap.keySet(), Objects::isNull), + "environment map contains null keys"); + String nullValuedKeys = + environmentMap.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .collect(Collectors.joining(", ")); + Preconditions.checkArgument( + nullValuedKeys.isEmpty(), + "environment map contains null values for key(s): " + nullValuedKeys); + this.environmentMap = new HashMap<>(environmentMap); + } + return this; + } + + /** + * Adds an environment entry to the container configuration. + * + * @param name the environment variable key + * @param value the non-null value to associate with environment variable + */ + public void addEnvironment(String name, String value) { + if (environmentMap == null) { + environmentMap = new HashMap<>(); + } + environmentMap.put(name, value); + } + + /** + * Sets the container's exposed ports. + * + * @param exposedPorts the set of ports + * @return this + */ + public Builder setExposedPorts(@Nullable Set exposedPorts) { + if (exposedPorts == null) { + this.exposedPorts = null; + } else { + Preconditions.checkArgument( + exposedPorts.stream().allMatch(Objects::nonNull), "ports list contains null elements"); + this.exposedPorts = new HashSet<>(exposedPorts); + } + return this; + } + + /** + * Adds an exposed port entry to the container configuration. + * + * @param port the non-null port to add + */ + public void addExposedPort(Port port) { + if (exposedPorts == null) { + exposedPorts = new HashSet<>(); + } + exposedPorts.add(port); + } + + /** + * Sets the container's volumes. + * + * @param volumes the set of volumes + * @return this + */ + public Builder setVolumes(@Nullable Set volumes) { + if (volumes == null) { + this.volumes = null; + } else { + Preconditions.checkArgument( + volumes.stream().allMatch(Objects::nonNull), "volumes list contains null elements"); + this.volumes = new HashSet<>(volumes); + } + return this; + } + + /** + * Adds a volume entry to the container configuration. + * + * @param volume the absolute path to add as a volume entry + */ + public void addVolume(AbsoluteUnixPath volume) { + if (volumes == null) { + volumes = new HashSet<>(); + } + volumes.add(volume); + } + + /** + * Sets the container's labels. + * + * @param labels the map of labels + * @return this + */ + public Builder setLabels(@Nullable Map labels) { + if (labels == null) { + this.labels = null; + } else { + Preconditions.checkArgument( + !Iterables.any(labels.keySet(), Objects::isNull), "labels map contains null keys"); + Preconditions.checkArgument( + !Iterables.any(labels.values(), Objects::isNull), "labels map contains null values"); + this.labels = new HashMap<>(labels); + } + return this; + } + + /** + * Add a label to the container configuration. + * + * @param key the label name to add + * @param value the value to be associated with the label + */ + public void addLabel(String key, String value) { + if (labels == null) { + labels = new HashMap<>(); + } + labels.put(key, value); + } + + /** + * Sets the container entrypoint. + * + * @param entrypoint the tokenized command to run when the container starts + * @return this + */ + public Builder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint == null) { + this.entrypoint = null; + } else { + Preconditions.checkArgument( + entrypoint.stream().allMatch(Objects::nonNull), "entrypoint contains null elements"); + this.entrypoint = ImmutableList.copyOf(entrypoint); + } + return this; + } + + /** + * Sets the user and group to run the container as. {@code user} can be a username or UID along + * with an optional groupname or GID. The following are all valid: {@code user}, {@code uid}, + * {@code user:group}, {@code uid:gid}, {@code uid:group}, {@code user:gid}. + * + * @param user the username/UID and optionally the groupname/GID + * @return this + */ + public Builder setUser(@Nullable String user) { + this.user = user; + return this; + } + + /** + * Sets the working directory in the container. + * + * @param workingDirectory the working directory + * @return this + */ + public Builder setWorkingDirectory(@Nullable AbsoluteUnixPath workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + /** + * Builds the {@link ContainerConfiguration}. + * + * @return the corresponding {@link ContainerConfiguration} + */ + public ContainerConfiguration build() { + return new ContainerConfiguration( + ImmutableSet.copyOf(platforms), + creationTime, + entrypoint, + programArguments, + environmentMap == null ? null : ImmutableMap.copyOf(environmentMap), + exposedPorts == null ? null : ImmutableSet.copyOf(exposedPorts), + volumes == null ? null : ImmutableSet.copyOf(volumes), + labels == null ? null : ImmutableMap.copyOf(labels), + user, + workingDirectory); + } + + private Builder() {} + } + + /** + * Constructs a builder for a {@link ContainerConfiguration}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + private final ImmutableSet platforms; + private final Instant creationTime; + @Nullable private final ImmutableList entrypoint; + @Nullable private final ImmutableList programArguments; + @Nullable private final ImmutableMap environmentMap; + @Nullable private final ImmutableSet exposedPorts; + @Nullable private final ImmutableSet volumes; + @Nullable private final ImmutableMap labels; + @Nullable private final String user; + @Nullable private final AbsoluteUnixPath workingDirectory; + + private ContainerConfiguration( + ImmutableSet platforms, + Instant creationTime, + @Nullable ImmutableList entrypoint, + @Nullable ImmutableList programArguments, + @Nullable ImmutableMap environmentMap, + @Nullable ImmutableSet exposedPorts, + @Nullable ImmutableSet volumes, + @Nullable ImmutableMap labels, + @Nullable String user, + @Nullable AbsoluteUnixPath workingDirectory) { + this.platforms = platforms; + this.creationTime = creationTime; + this.entrypoint = entrypoint; + this.programArguments = programArguments; + this.environmentMap = environmentMap; + this.exposedPorts = exposedPorts; + this.volumes = volumes; + this.labels = labels; + this.user = user; + this.workingDirectory = workingDirectory; + } + + public ImmutableSet getPlatforms() { + return platforms; + } + + public Instant getCreationTime() { + return creationTime; + } + + @Nullable + public ImmutableList getEntrypoint() { + return entrypoint; + } + + @Nullable + public ImmutableList getProgramArguments() { + return programArguments; + } + + @Nullable + public ImmutableMap getEnvironmentMap() { + return environmentMap; + } + + @Nullable + public ImmutableSet getExposedPorts() { + return exposedPorts; + } + + @Nullable + public ImmutableSet getVolumes() { + return volumes; + } + + @Nullable + public String getUser() { + return user; + } + + @Nullable + public ImmutableMap getLabels() { + return labels; + } + + @Nullable + public AbsoluteUnixPath getWorkingDirectory() { + return workingDirectory; + } + + @Override + @VisibleForTesting + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ContainerConfiguration)) { + return false; + } + ContainerConfiguration otherContainerConfiguration = (ContainerConfiguration) other; + return platforms.equals(otherContainerConfiguration.platforms) + && creationTime.equals(otherContainerConfiguration.creationTime) + && Objects.equals(entrypoint, otherContainerConfiguration.entrypoint) + && Objects.equals(programArguments, otherContainerConfiguration.programArguments) + && Objects.equals(environmentMap, otherContainerConfiguration.environmentMap) + && Objects.equals(exposedPorts, otherContainerConfiguration.exposedPorts) + && Objects.equals(labels, otherContainerConfiguration.labels) + && Objects.equals(user, otherContainerConfiguration.user) + && Objects.equals(workingDirectory, otherContainerConfiguration.workingDirectory); + } + + @Override + @VisibleForTesting + public int hashCode() { + return Objects.hash( + platforms, + creationTime, + entrypoint, + programArguments, + environmentMap, + exposedPorts, + labels, + user, + workingDirectory); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java new file mode 100644 index 0000000000..722be3bd89 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java @@ -0,0 +1,174 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Configuration information for performing healthchecks on a Docker container. */ +public class DockerHealthCheck { + + /** Builds the immutable {@link DockerHealthCheck}. */ + public static class Builder { + + private final ImmutableList command; + @Nullable private Duration interval; + @Nullable private Duration timeout; + @Nullable private Duration startPeriod; + @Nullable private Integer retries; + + private Builder(ImmutableList command) { + this.command = command; + } + + /** + * Sets the time between healthchecks. + * + * @param interval the duration to wait between healthchecks. + * @return this + */ + public Builder setInterval(Duration interval) { + this.interval = interval; + return this; + } + + /** + * Sets the time until a healthcheck is considered hung. + * + * @param timeout the duration to wait until considering the healthcheck to be hung. + * @return this + */ + public Builder setTimeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Sets the initialization time to wait before using healthchecks. + * + * @param startPeriod the duration to wait before using healthchecks + * @return this + */ + public Builder setStartPeriod(Duration startPeriod) { + this.startPeriod = startPeriod; + return this; + } + + /** + * Sets the number of times to retry the healthcheck before the container is considered to be + * unhealthy. + * + * @param retries the number of retries before the container is considered to be unhealthy + * @return this + */ + public Builder setRetries(int retries) { + this.retries = retries; + return this; + } + + public DockerHealthCheck build() { + return new DockerHealthCheck(command, interval, timeout, startPeriod, retries); + } + } + + /** + * Creates a new {@link DockerHealthCheck.Builder} with the specified command. + * + * @param command the command + * @return a new {@link DockerHealthCheck.Builder} + */ + public static DockerHealthCheck.Builder fromCommand(List command) { + Preconditions.checkArgument(!command.isEmpty(), "command must not be empty"); + Preconditions.checkArgument( + command.stream().allMatch(Objects::nonNull), "command must not contain null elements"); + return new Builder(ImmutableList.copyOf(command)); + } + + private final ImmutableList command; + @Nullable private final Duration interval; + @Nullable private final Duration timeout; + @Nullable private final Duration startPeriod; + @Nullable private final Integer retries; + + private DockerHealthCheck( + ImmutableList command, + @Nullable Duration interval, + @Nullable Duration timeout, + @Nullable Duration startPeriod, + @Nullable Integer retries) { + this.command = command; + this.interval = interval; + this.timeout = timeout; + this.startPeriod = startPeriod; + this.retries = retries; + } + + /** + * Gets the optional healthcheck command. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck command + */ + public List getCommand() { + return command; + } + + /** + * Gets the optional healthcheck interval. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck interval + */ + public Optional getInterval() { + return Optional.ofNullable(interval); + } + + /** + * Gets the optional healthcheck timeout. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck timeout + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + /** + * Gets the optional healthcheck start period. A missing command means that it will be inherited + * from the base image. + * + * @return the healthcheck start period + */ + public Optional getStartPeriod() { + return Optional.ofNullable(startPeriod); + } + + /** + * Gets the optional healthcheck retry count. A missing command means that it will be inherited + * from the base image. + * + * @return the healthcheck retry count + */ + public Optional getRetries() { + return Optional.ofNullable(retries); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java new file mode 100644 index 0000000000..91e8818713 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Immutable configuration options for a base or target image reference. */ +public class ImageConfiguration { + + /** Builder for instantiating an {@link ImageConfiguration}. */ + public static class Builder { + + private ImageReference imageReference; + private ImmutableList credentialRetrievers = ImmutableList.of(); + @Nullable private DockerClient dockerClient; + @Nullable private Path tarPath; + + /** + * Sets the providers for registry credentials. The order determines the priority in which the + * retrieval methods are attempted. + * + * @param credentialRetrievers the list of {@link CredentialRetriever}s + * @return this + */ + public Builder setCredentialRetrievers(List credentialRetrievers) { + Preconditions.checkArgument( + credentialRetrievers.stream().allMatch(Objects::nonNull), + "credential retriever list contains null elements"); + this.credentialRetrievers = ImmutableList.copyOf(credentialRetrievers); + return this; + } + + /** + * Sets the Docker client to be used for Docker daemon base images. + * + * @param dockerClient the Docker client + * @return this + */ + public Builder setDockerClient(DockerClient dockerClient) { + this.dockerClient = dockerClient; + return this; + } + + /** + * Sets the path for tarball base images. + * + * @param tarPath the path + * @return this + */ + public Builder setTarPath(Path tarPath) { + this.tarPath = tarPath; + return this; + } + + /** + * Builds the {@link ImageConfiguration}. + * + * @return the corresponding {@link ImageConfiguration} + */ + public ImageConfiguration build() { + int numArguments = 0; + if (!credentialRetrievers.isEmpty()) { + numArguments++; + } + if (dockerClient != null) { + numArguments++; + } + if (tarPath != null) { + numArguments++; + } + Preconditions.checkArgument(numArguments <= 1); + return new ImageConfiguration(imageReference, credentialRetrievers, dockerClient, tarPath); + } + + private Builder(ImageReference imageReference) { + this.imageReference = imageReference; + } + } + + /** + * Constructs a builder for an {@link ImageConfiguration}. + * + * @param imageReference the image reference, which is a required field + * @return the builder + */ + public static Builder builder(ImageReference imageReference) { + return new Builder(imageReference); + } + + private final ImageReference image; + private final ImmutableList credentialRetrievers; + @Nullable private DockerClient dockerClient; + @Nullable private Path tarPath; + + private ImageConfiguration( + ImageReference image, + ImmutableList credentialRetrievers, + @Nullable DockerClient dockerClient, + @Nullable Path tarPath) { + this.image = image; + this.credentialRetrievers = credentialRetrievers; + this.dockerClient = dockerClient; + this.tarPath = tarPath; + } + + public ImageReference getImage() { + return image; + } + + public String getImageRegistry() { + return image.getRegistry(); + } + + public String getImageRepository() { + return image.getRepository(); + } + + public String getImageQualifier() { + return image.getQualifier(); + } + + public ImmutableList getCredentialRetrievers() { + return credentialRetrievers; + } + + public Optional getDockerClient() { + return Optional.ofNullable(dockerClient); + } + + public Optional getTarPath() { + return Optional.ofNullable(tarPath); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java deleted file mode 100644 index e420381c39..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; -import java.util.List; - -/** Configures how to build a layer in the container image. */ -public class LayerConfiguration { - - /** Builds a {@link LayerConfiguration}. */ - public static class Builder { - - private final ImmutableList.Builder layerEntries = ImmutableList.builder(); - - private Builder() {} - - /** - * Adds an entry to the layer. - * - *

    The image consists of a base image layer and three application layers under the directories: - * - *

      - *
    • libs/ (dependency jars) - *
    • resources/ - *
    • classes - *
    - */ -public class DockerContextGenerator { - - private final SourceFilesConfiguration sourceFilesConfiguration; - - @Nullable private String baseImage; - private List jvmFlags = Collections.emptyList(); - private String mainClass = ""; - private List javaArguments = Collections.emptyList(); - private List exposedPorts = Collections.emptyList(); - - public DockerContextGenerator(SourceFilesConfiguration sourceFilesConfiguration) { - this.sourceFilesConfiguration = sourceFilesConfiguration; - } - - /** - * Sets the base image for the {@code FROM} directive. This must be called before {@link - * #generate}. - * - * @param baseImage the base image. - * @return this - */ - public DockerContextGenerator setBaseImage(String baseImage) { - this.baseImage = baseImage; - return this; - } - - /** - * Sets the JVM flags used in the {@code ENTRYPOINT}. - * - * @param jvmFlags the jvm flags. - * @return this - */ - public DockerContextGenerator setJvmFlags(List jvmFlags) { - this.jvmFlags = jvmFlags; - return this; - } - - /** - * Sets the main class used in the {@code ENTRYPOINT}. - * - * @param mainClass the name of the main class. - * @return this - */ - public DockerContextGenerator setMainClass(String mainClass) { - this.mainClass = mainClass; - return this; - } - - /** - * Sets the arguments used in the {@code CMD}. - * - * @param javaArguments the list of arguments to pass into main. - * @return this - */ - public DockerContextGenerator setJavaArguments(List javaArguments) { - this.javaArguments = javaArguments; - return this; - } - - /** - * Sets the exposed ports. - * - * @param exposedPorts the list of port numbers/port ranges to expose - * @return this - */ - public DockerContextGenerator setExposedPorts(List exposedPorts) { - this.exposedPorts = exposedPorts; - return this; - } - - /** - * Creates the Docker context in {@code #targetDirectory}. - * - * @param targetDirectory the directory to generate the Docker context in - * @throws IOException if the export fails - */ - public void generate(Path targetDirectory) throws IOException { - Preconditions.checkNotNull(baseImage); - - // Deletes the targetDir if it exists. - try { - Files.deleteIfExists(targetDirectory); - - } catch (DirectoryNotEmptyException ex) { - MoreFiles.deleteDirectoryContents(targetDirectory); - Files.delete(targetDirectory); - } - - Files.createDirectory(targetDirectory); - - // Creates the directories. - Path dependenciesDir = targetDirectory.resolve("libs"); - Path resourcesDIr = targetDirectory.resolve("resources"); - Path classesDir = targetDirectory.resolve("classes"); - Files.createDirectory(dependenciesDir); - Files.createDirectory(resourcesDIr); - Files.createDirectory(classesDir); - - // Copies dependencies. - FileOperations.copy(sourceFilesConfiguration.getDependenciesFiles(), dependenciesDir); - FileOperations.copy(sourceFilesConfiguration.getResourcesFiles(), resourcesDIr); - FileOperations.copy(sourceFilesConfiguration.getClassesFiles(), classesDir); - - // Creates the Dockerfile. - Files.write( - targetDirectory.resolve("Dockerfile"), makeDockerfile().getBytes(StandardCharsets.UTF_8)); - } - - /** - * Makes the contents of a {@code Dockerfile} using configuration data, in the following format: - * - *
    {@code
    -   * FROM [base image]
    -   *
    -   * COPY libs [path/to/dependencies]
    -   * COPY resources [path/to/resources]
    -   * COPY classes [path/to/classes]
    -   *
    -   * EXPOSE [port]
    -   * [More EXPOSE instructions, if necessary]
    -   * ENTRYPOINT java [jvm flags] -cp [classpaths] [main class]
    -   * CMD [main class args]
    -   * }
    - * - * @return the {@code Dockerfile} contents - */ - @VisibleForTesting - String makeDockerfile() throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - StringBuilder dockerfile = new StringBuilder(); - dockerfile - .append("FROM ") - .append(Preconditions.checkNotNull(baseImage)) - .append("\n\nCOPY libs ") - .append(sourceFilesConfiguration.getDependenciesPathOnImage()) - .append("\nCOPY resources ") - .append(sourceFilesConfiguration.getResourcesPathOnImage()) - .append("\nCOPY classes ") - .append(sourceFilesConfiguration.getClassesPathOnImage()) - .append("\n"); - for (String port : exposedPorts) { - dockerfile.append("\nEXPOSE ").append(port); - } - dockerfile - .append("\nENTRYPOINT ") - .append( - objectMapper.writeValueAsString( - EntrypointBuilder.makeEntrypoint(sourceFilesConfiguration, jvmFlags, mainClass))) - .append("\nCMD ") - .append(objectMapper.writeValueAsString(javaArguments)); - return dockerfile.toString(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java deleted file mode 100644 index 0936a9ae5d..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestTemplate; -import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.tar.TarStreamBuilder; -import java.io.IOException; -import java.nio.file.Path; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; - -/** Translates an {@link Image} to a tarball that can be loaded into Docker. */ -public class ImageToTarballTranslator { - - /** File name for the container configuration in the tarball. */ - private static final String CONTAINER_CONFIGURATION_JSON_FILE_NAME = "config.json"; - /** File name for the manifest in the tarball. */ - private static final String MANIFEST_JSON_FILE_NAME = "manifest.json"; - - private final Image image; - - /** - * Instantiate with an {@link Image}. - * - * @param image the image to convert into a tarball. - */ - public ImageToTarballTranslator(Image image) { - this.image = image; - } - - public Blob toTarballBlob(ImageReference imageReference) throws IOException { - TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - DockerLoadManifestTemplate manifestTemplate = new DockerLoadManifestTemplate(); - - // Adds all the layers to the tarball and manifest. - for (CachedLayer layer : image.getLayers()) { - Path layerContentFile = layer.getContentFile(); - String layerName = layerContentFile.getFileName().toString(); - - tarStreamBuilder.addEntry(new TarArchiveEntry(layerContentFile.toFile(), layerName)); - manifestTemplate.addLayerFile(layerName); - } - - // Adds the container configuration to the tarball. - Blob containerConfigurationBlob = - new ImageToJsonTranslator(image).getContainerConfigurationBlob(); - tarStreamBuilder.addEntry( - Blobs.writeToString(containerConfigurationBlob), CONTAINER_CONFIGURATION_JSON_FILE_NAME); - - // Adds the manifest to tarball. - manifestTemplate.setRepoTags(imageReference.toStringWithTag()); - tarStreamBuilder.addEntry( - Blobs.writeToString(JsonTemplateMapper.toBlob(manifestTemplate)), MANIFEST_JSON_FILE_NAME); - - return tarStreamBuilder.toBlob(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java similarity index 53% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java index 78c9d44494..5048f33c4e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,20 +16,24 @@ package com.google.cloud.tools.jib.docker.json; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.cloud.tools.jib.json.ListOfJsonTemplate; +import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** - * JSON Template for loadable Docker Manifest. The repoTags property requires a tag; i.e. if a tag - * is missing, it explicitly should use "latest". + * JSON Template for a loadable Docker Manifest entry. The RepoTags property requires a tag; i.e. if + * a tag is missing, it explicitly should use "latest". * - *

    Example manifest JSON: + *

    Note that this is a template for a single Manifest entry, while the entire Docker Manifest + * should be {@code List}. + * + *

    Example manifest entry JSON: * *

    {@code
    - * [{
    + * {
      *   "Config":"config.json",
      *   "RepoTags":["repository:tag"]
      *   "Layers": [
    @@ -37,28 +41,46 @@
      *     "ba7c544469e514f1a9a4dec59ab640540d50992b288adbb34a1a63c45bf19a24.tar.gz",
      *     "15705ab016593987662839b40f5a22fd1032996c90808d4a1371eb46974017d5.tar.gz"
      *   ]
    - * }]
    + * }
      * }
    * * @see Docker load * source */ -public class DockerLoadManifestTemplate implements ListOfJsonTemplate { +@JsonIgnoreProperties(ignoreUnknown = true) +public class DockerManifestEntryTemplate implements JsonTemplate { + + @JsonProperty("Config") + private String config = "config.json"; + + @JsonProperty("RepoTags") + private final List repoTags = new ArrayList<>(); - private final String config = "config.json"; - private List repoTags = Collections.singletonList(null); + @JsonProperty("Layers") private final List layers = new ArrayList<>(); - public void setRepoTags(String repoTags) { - this.repoTags = Collections.singletonList(repoTags); + public void setConfig(String config) { + this.config = config; + } + + public void addRepoTag(String repoTag) { + repoTags.add(repoTag); } public void addLayerFile(String layer) { layers.add(layer); } - @Override - public List getList() { - return Collections.singletonList(this); + public String getConfig() { + return config; + } + + public List getLayerFiles() { + return layers; + } + + @VisibleForTesting + public List getRepoTags() { + return repoTags; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java new file mode 100644 index 0000000000..d594410d70 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -0,0 +1,98 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.cloud.tools.jib.api.JibEvent; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import java.util.function.Consumer; + +/** Builds a set of event handlers to handle {@link JibEvent}s. */ +public class EventHandlers { + + /** Builder for {@link EventHandlers}. */ + public static class Builder { + + private final Multimap, Handler> handlers = + ArrayListMultimap.create(); + + /** + * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. + * The order in which handlers are added is the order in which they are called when the event is + * dispatched. + * + *

    Note: Implementations of {@code eventConsumer} must be thread-safe. + * + * @param eventType the event type that {@code eventConsumer} should handle + * @param eventConsumer the event handler + * @param the type of {@code eventClass} + * @return this + */ + public Builder add(Class eventType, Consumer eventConsumer) { + handlers.put(eventType, new Handler<>(eventType, eventConsumer)); + return this; + } + + public EventHandlers build() { + return new EventHandlers(handlers); + } + } + + /** An empty {@link EventHandlers}. */ + public static final EventHandlers NONE = new Builder().build(); + + /** Maps from {@link JibEvent} class to handlers for that event type. */ + private final ImmutableMultimap, Handler> handlers; + + private EventHandlers(Multimap, Handler> handlers) { + this.handlers = ImmutableMultimap.copyOf(handlers); + } + + /** + * Creates a new {@link EventHandlers.Builder}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Dispatches {@code jibEvent} to all the handlers that can handle it. + * + * @param jibEvent the {@link JibEvent} to dispatch + */ + public void dispatch(JibEvent jibEvent) { + if (handlers.isEmpty()) { + return; + } + handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); + handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); + } + + /** + * Gets the handlers added. + * + * @return the map from {@link JibEvent} type to a list of {@link Handler}s + */ + @VisibleForTesting + ImmutableMultimap, Handler> getHandlers() { + return ImmutableMultimap.copyOf(handlers); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java new file mode 100644 index 0000000000..c3d3002dcc --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.cloud.tools.jib.api.JibEvent; +import com.google.common.base.Preconditions; +import java.util.function.Consumer; + +/** Handles a dispatched {@link JibEvent}. */ +class Handler { + + private final Class eventClass; + private final Consumer eventConsumer; + + Handler(Class eventClass, Consumer eventConsumer) { + this.eventClass = eventClass; + this.eventConsumer = eventConsumer; + } + + /** + * Handles a {@link JibEvent}. + * + * @param jibEvent the event to handle + */ + void handle(JibEvent jibEvent) { + Preconditions.checkArgument(eventClass.isInstance(jibEvent)); + eventConsumer.accept(eventClass.cast(jibEvent)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java new file mode 100644 index 0000000000..613a9fcc47 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.api.JibEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; + +/** + * Event representing progress. The progress accounts for allocation units in an {@link Allocation}, + * which makes up a Decentralized Allocation Tree. + * + * @see Allocation + */ +public class ProgressEvent implements JibEvent { + + /** + * The allocation this progress is for. Each progress unit accounts for a single allocation unit + * on the {@link Allocation}. + */ + private final Allocation allocation; + + /** Units of progress. */ + private final long progressUnits; + + public ProgressEvent(Allocation allocation, long progressUnits) { + this.allocation = allocation; + this.progressUnits = progressUnits; + } + + /** + * Gets the {@link Allocation} this progress event accounts for. + * + * @return the {@link Allocation} + */ + public Allocation getAllocation() { + return allocation; + } + + /** + * Gets the units of progress this progress event accounts for in the associated {@link + * Allocation}. + * + * @return units of allocation + */ + public long getUnits() { + return progressUnits; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java new file mode 100644 index 0000000000..52fe116a6f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.api.JibEvent; +import java.time.Duration; +import java.util.Optional; + +/** + * Timer event for timing various part of Jib's execution. + * + *

    Timer events follow a specific {@link Timer} through a {@link State#START}, {@link State#LAP}, + * and {@link State#FINISHED} states. The duration indicates the duration since the last {@link + * TimerEvent} dispatched for the {@link Timer}. + * + *

    Timers can also define a hierarchy. + */ +public class TimerEvent implements JibEvent { + + /** The state of the timing. */ + public enum State { + + /** The timer has started timing. {@link #getDuration} is 0. {@link #getElapsed} is 0. */ + START, + + /** + * The timer timed a lap. {@link #getDuration} is the time since the last event. {@link + * #getElapsed} is the total elapsed time. + */ + LAP, + + /** + * The timer has finished timing. {@link #getDuration} is the time since the last event. {@link + * #getElapsed} is the total elapsed time. + */ + FINISHED + } + + /** Defines a timer hierarchy. */ + public interface Timer { + + /** + * Gets the parent of this {@link Timer}. + * + * @return the parent of this {@link Timer} + */ + Optional getParent(); + } + + private final State state; + private final Timer timer; + private final Duration duration; + private final Duration elapsed; + private final String description; + + /** + * Creates a new {@link TimerEvent}. For internal use only. + * + * @param state the state of the {@link Timer} + * @param timer the {@link Timer} + * @param duration the lap duration + * @param elapsed the total elapsed time since the timer was created + * @param description the description of this event + */ + public TimerEvent( + State state, Timer timer, Duration duration, Duration elapsed, String description) { + this.state = state; + this.timer = timer; + this.duration = duration; + this.elapsed = elapsed; + this.description = description; + } + + /** + * Gets the state of the timer. + * + * @return the state of the timer + * @see State + */ + public State getState() { + return state; + } + + /** + * Gets the timer this event is for. + * + * @return the timer + */ + public Timer getTimer() { + return timer; + } + + /** + * Gets the duration since the last {@link TimerEvent} for this timer. + * + * @return the duration since the last {@link TimerEvent} for this timer. + */ + public Duration getDuration() { + return duration; + } + + /** + * Gets the total elapsed duration since this timer was created. + * + * @return the duration since this timer was created + */ + public Duration getElapsed() { + return elapsed; + } + + /** + * Gets the description associated with this event. + * + * @return the description + */ + public String getDescription() { + return description; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java new file mode 100644 index 0000000000..af2911bfc7 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * Represents a Decentralized Allocation Tree (DAT) node. + * + *

    A DAT node is immutable and pointers only go in the direction from child to parent. Each node + * has a set number of allocated units, the total of which represents a single allocation unit of + * its parent. Each node is therefore a sub-allocation of its parent node. This allows the DAT to + * sub-allocate progress in a decentralized, asynchronous manner. + * + *

    For example, thread 1 creates node A as the root node with 2 allocation units. A subtask is + * launched on thread 1 and creates node B with 3 allocation units as a child of node A. Thread 1 + * then also launches a subtask on thread 2 that creates node C with 5 allocation units. Once the + * first subtask finishes and reports its progress, that completion would entail completion of 3 + * allocation units of node B and 1 allocation unit of node A. The second subtask finishes and + * reports its progress as well, indicating completion of 5 units of node C and thus 1 unit of node + * A. Allocation A is then deemed complete as well in terms of overall progress. + * + *

    Note that it is up to the user of the class to ensure that the number of sub-allocations does + * not exceed the number of allocation units. + */ +public class Allocation { + + /** + * Creates a new root {@link Allocation}. + * + * @param description user-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return a new {@link Allocation} + */ + public static Allocation newRoot(String description, long allocationUnits) { + return new Allocation(description, allocationUnits, null); + } + + /** The parent {@link Allocation}, or {@code null} to indicate a root node. */ + @Nullable private final Allocation parent; + + /** User-facing description of what the allocation represents. */ + private final String description; + + /** The number of allocation units this node holds. */ + private final long allocationUnits; + + /** How much of the root allocation (1.0) each allocation unit accounts for. */ + private final double fractionOfRoot; + + private Allocation(String description, long allocationUnits, @Nullable Allocation parent) { + this.description = description; + this.allocationUnits = allocationUnits < 1 ? 1 : allocationUnits; + this.parent = parent; + + this.fractionOfRoot = (parent == null ? 1.0 : parent.fractionOfRoot) / this.allocationUnits; + } + + /** + * Creates a new child {@link Allocation} (sub-allocation). + * + * @param description user-facing description of what the sub-allocation represents + * @param allocationUnits number of allocation units the child holds + * @return a new {@link Allocation} + */ + public Allocation newChild(String description, long allocationUnits) { + return new Allocation(description, allocationUnits, this); + } + + /** + * Gets the parent allocation, or {@link Optional#empty} if this is a root allocation. This + * allocation represents 1 allocation unit of the parent allocation. + * + * @return the parent {@link Allocation} + */ + public Optional getParent() { + return Optional.ofNullable(parent); + } + + /** + * Gets a user-facing description of what this allocation represents. For example, this can a + * description of the task this allocation represents. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Gets the allocation units this allocation holds. If this allocation is not the root, the full + * number of units represents 1 allocation unit of the parent. + * + * @return the allocation units + */ + public long getAllocationUnits() { + return allocationUnits; + } + + /** + * Gets how much of the root allocation each of the allocation units of this allocation accounts + * for. The entire root allocation is {@code 1.0}. + * + * @return the fraction of the root allocation this allocation's allocation units accounts for + */ + public double getFractionOfRoot() { + return fractionOfRoot; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java new file mode 100644 index 0000000000..658d444d7b --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java @@ -0,0 +1,179 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +/** + * Keeps track of the progress for {@link Allocation}s as well as their order in which they appear. + * + *

    This implementation is thread-safe. + */ +class AllocationCompletionTracker { + + /** + * Holds the progress units remaining along with a creation order (index starting from 0). This is + * used as the value of the {@link #completionMap}. + */ + private static class IndexedRemainingUnits implements Comparable { + + /** Monotonically-increasing source for {@link #index}. */ + private static final AtomicInteger currentIndex = new AtomicInteger(); + + /** The creation order that monotonically increases. */ + private final int index = currentIndex.getAndIncrement(); + + /** + * Remaining progress units until completion. This can be shared across multiple threads and + * should be updated atomically. + */ + private final AtomicLong remainingUnits; + + private final Allocation allocation; + + private IndexedRemainingUnits(Allocation allocation) { + this.allocation = allocation; + remainingUnits = new AtomicLong(allocation.getAllocationUnits()); + } + + private boolean isUnfinished() { + return remainingUnits.get() != 0; + } + + @Override + public int compareTo(IndexedRemainingUnits otherIndexedRemainingUnits) { + return index - otherIndexedRemainingUnits.index; + } + } + + /** + * Maps from {@link Allocation} to 1) the number of progress units remaining in that {@link + * Allocation}; as well as 2) the insertion order of the key. + */ + private final ConcurrentHashMap completionMap = + new ConcurrentHashMap<>(); + + /** + * Updates the progress for {@link Allocation} atomically relative to the {@code allocation}. + * + *

    For any {@link Allocation}, this method must have been called on all of its parents + * beforehand. + * + * @param allocation the {@link Allocation} to update progress for + * @param units the units of progress + * @return {@code true} if the map was updated + */ + boolean updateProgress(Allocation allocation, long units) { + AtomicBoolean mapUpdated = new AtomicBoolean(units != 0); + + completionMap.compute( + allocation, + (ignored, indexedRemainingUnits) -> { + if (indexedRemainingUnits == null) { + indexedRemainingUnits = new IndexedRemainingUnits(allocation); + mapUpdated.set(true); + } + + if (units != 0) { + updateIndexedRemainingUnits(indexedRemainingUnits, units); + } + return indexedRemainingUnits; + }); + return mapUpdated.get(); + } + + /** + * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link + * Allocation}s were encountered. This can be used to display, for example, currently executing + * tasks. The order helps to keep the displayed tasks in a deterministic order (new subtasks + * appear below older ones) and not jumbled together in some random order. + * + * @return a list of unfinished {@link Allocation}s + */ + @VisibleForTesting + List getUnfinishedAllocations() { + return completionMap.values().stream() + .filter(IndexedRemainingUnits::isUnfinished) + .sorted() + .map(remainingUnits -> remainingUnits.allocation) + .collect(Collectors.toList()); + } + + /** + * Helper method for {@link #updateProgress(Allocation, long)}. Subtract {@code units} from {@code + * indexedRemainingUnits}. Updates {@link IndexedRemainingUnits} for parent {@link Allocation}s if + * remaining units becomes 0. This method is not thread-safe for the {@code + * indexedRemainingUnits} and should be called atomically relative to the {@code + * indexedRemainingUnits}. + * + * @param indexedRemainingUnits the {@link IndexedRemainingUnits} to update progress for + * @param units the units of progress + */ + private void updateIndexedRemainingUnits( + IndexedRemainingUnits indexedRemainingUnits, long units) { + Allocation allocation = indexedRemainingUnits.allocation; + + long newUnits = indexedRemainingUnits.remainingUnits.addAndGet(-units); + if (newUnits < 0L) { + throw new IllegalStateException( + "Progress exceeds max for '" + + allocation.getDescription() + + "': " + + -newUnits + + " more beyond " + + allocation.getAllocationUnits()); + } + + // Updates the parent allocations if this allocation completed. + if (newUnits == 0L) { + allocation + .getParent() + .ifPresent( + parentAllocation -> + updateIndexedRemainingUnits( + Preconditions.checkNotNull(completionMap.get(parentAllocation)), 1L)); + } + } + + ImmutableList getUnfinishedLeafTasks() { + List allUnfinished = getUnfinishedAllocations(); + Set unfinishedLeaves = new LinkedHashSet<>(allUnfinished); // preserves order + + for (Allocation allocation : allUnfinished) { + Optional parent = allocation.getParent(); + + while (parent.isPresent()) { + unfinishedLeaves.remove(parent.get()); + parent = parent.get().getParent(); + } + } + + return ImmutableList.copyOf( + unfinishedLeaves.stream().map(Allocation::getDescription).collect(Collectors.toList())); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java new file mode 100644 index 0000000000..915790e7ed --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.function.Consumer; + +/** + * Handles {@link ProgressEvent}s by accumulating an overall progress and keeping track of which + * {@link Allocation}s are complete. + * + *

    This implementation is thread-safe. + */ +public class ProgressEventHandler implements Consumer { + + /** + * Contains the accumulated progress and which "leaf" tasks are not yet complete. Leaf tasks are + * those that do not have sub-tasks. + */ + public static class Update { + + private final double progress; + private final ImmutableList unfinishedLeafTasks; + + private Update(double progress, ImmutableList unfinishedLeafTasks) { + this.progress = progress; + this.unfinishedLeafTasks = unfinishedLeafTasks; + } + + /** + * Gets the overall progress, with {@code 1.0} meaning fully complete. + * + * @return the overall progress + */ + public double getProgress() { + return progress; + } + + /** + * Gets a list of the unfinished "leaf" tasks in the order in which those tasks were + * encountered. + * + * @return a list of unfinished "leaf" tasks + */ + public ImmutableList getUnfinishedLeafTasks() { + return unfinishedLeafTasks; + } + } + + /** Keeps track of the progress for each {@link Allocation} encountered. */ + private final AllocationCompletionTracker completionTracker = new AllocationCompletionTracker(); + + /** Accumulates an overall progress, with {@code 1.0} indicating full completion. */ + private final DoubleAdder progress = new DoubleAdder(); + + /** + * A callback to notify that {@link #progress} or {@link #completionTracker} could have changed. + * Note that every change will be reported (though multiple could be reported together), and there + * could be false positives. + */ + private final Consumer updateNotifier; + + public ProgressEventHandler(Consumer updateNotifier) { + this.updateNotifier = updateNotifier; + } + + @Override + public void accept(ProgressEvent progressEvent) { + Allocation allocation = progressEvent.getAllocation(); + long progressUnits = progressEvent.getUnits(); + double allocationFraction = allocation.getFractionOfRoot(); + + if (progressUnits != 0) { + progress.add(progressUnits * allocationFraction); + } + + if (completionTracker.updateProgress(allocation, progressUnits)) { + // Note: Could produce false positives. + updateNotifier.accept(new Update(progress.sum(), completionTracker.getUnfinishedLeafTasks())); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java new file mode 100644 index 0000000000..c3246011ac --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.io.Closeable; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Wraps a {@code Consumer} so that multiple consume calls ({@link #accept}) within a short + * period of time are merged into a single later call with the value accumulated up to that point. + */ +public class ThrottledAccumulatingConsumer implements Consumer, Closeable { + + private final Consumer consumer; + + /** Delay between each call to the underlying {@link #accept}. */ + private final Duration delayBetweenCallbacks; + + /** Last time the underlying {@link #accept} was called. */ + private Instant previousCallback; + + /** "Clock" that returns the current {@link Instant}. */ + private final Supplier getNow; + + @Nullable private Long valueSoFar; + + /** + * Wraps a consumer with the delay of 100 ms. + * + * @param callback {@link Consumer} callback to wrap + */ + public ThrottledAccumulatingConsumer(Consumer callback) { + this(callback, Duration.ofMillis(100), Instant::now); + } + + /** + * A new configured consumer. + * + * @param consumer {@link Consumer} callback to wrap + * @param delayBetweenCallbacks delay between each call to the underlying accept + * @param getNow supplies of the current time {@link Instant} + */ + public ThrottledAccumulatingConsumer( + Consumer consumer, Duration delayBetweenCallbacks, Supplier getNow) { + this.consumer = consumer; + this.delayBetweenCallbacks = delayBetweenCallbacks; + this.getNow = getNow; + + previousCallback = getNow.get(); + } + + @Override + public void accept(Long value) { + valueSoFar = valueSoFar == null ? value : valueSoFar + value; + + Instant now = getNow.get(); + Instant nextFireTime = previousCallback.plus(delayBetweenCallbacks); + if (now.isAfter(nextFireTime)) { + consumer.accept(valueSoFar); + previousCallback = now; + valueSoFar = null; + } + } + + @Override + public void close() { + if (valueSoFar != null) { + consumer.accept(valueSoFar); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java index ebb29b210e..9ba412ba6c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -90,11 +90,7 @@ public ImmutableList walk(PathConsumer pathConsumer) throws IOException { */ public ImmutableList walk() throws IOException { try (Stream fileStream = Files.walk(rootDir)) { - Stream filteredFileStream = fileStream; - if (pathFilter != null) { - filteredFileStream = fileStream.filter(pathFilter); - } - return filteredFileStream.sorted().collect(ImmutableList.toImmutableList()); + return fileStream.filter(pathFilter).sorted().collect(ImmutableList.toImmutableList()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java index 2f93e5550e..315f359e30 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.filesystem; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Files; @@ -36,7 +37,8 @@ public static void copy(ImmutableList sourceFiles, Path destDir) throws IO PathConsumer copyPathConsumer = path -> { // Creates the same path in the destDir. - Path destPath = destDir.resolve(sourceFile.getParent().relativize(path)); + Path parent = Verify.verifyNotNull(sourceFile.getParent()); + Path destPath = destDir.resolve(parent.relativize(path)); if (Files.isDirectory(path)) { Files.createDirectories(destPath); } else { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java new file mode 100644 index 0000000000..f967560ef0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.base.Preconditions; +import java.io.Closeable; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** Creates and deletes lock files. */ +public class LockFile implements Closeable { + + private static final ConcurrentHashMap lockMap = new ConcurrentHashMap<>(); + + private final Path lockFilePath; + private final FileLock fileLock; + private final OutputStream outputStream; + + private LockFile(Path lockFilePath, FileLock fileLock, OutputStream outputStream) { + this.lockFilePath = lockFilePath; + this.fileLock = fileLock; + this.outputStream = outputStream; + } + + /** + * Creates a lock file. + * + * @param lockFile the path of the lock file + * @return a new {@link LockFile} that can be released later + * @throws IOException if creating the lock file fails + */ + public static LockFile lock(Path lockFile) throws IOException { + try { + // This first lock is to prevent multiple threads from calling FileChannel.lock(), which would + // otherwise throw OverlappingFileLockException + lockMap.computeIfAbsent(lockFile, key -> new ReentrantLock()).lockInterruptibly(); + + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while trying to acquire lock", ex); + } + + Files.createDirectories(lockFile.getParent()); + FileOutputStream outputStream = new FileOutputStream(lockFile.toFile()); + FileLock fileLock = null; + try { + fileLock = outputStream.getChannel().lock(); + return new LockFile(lockFile, fileLock, outputStream); + + } finally { + if (fileLock == null) { + outputStream.close(); + } + } + } + + /** Releases the lock file. */ + @Override + public void close() { + try { + fileLock.release(); + outputStream.close(); + + } catch (IOException ex) { + throw new IllegalStateException("Unable to release lock", ex); + + } finally { + Preconditions.checkNotNull(lockMap.get(lockFilePath)).unlock(); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java index 73747cdc98..993d0f6b4a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java new file mode 100644 index 0000000000..b77aca1e41 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** Creates temporary directories and deletes them all when closed. */ +public class TempDirectoryProvider implements Closeable { + + private final Set directories = Collections.synchronizedSet(new HashSet<>()); + + /** + * Creates a new temporary directory. + * + * @return the path to the temporary directory + * @throws IOException if creating the directory fails + */ + public Path newDirectory() throws IOException { + Path path = Files.createTempDirectory(null); + directories.add(path); + return path; + } + + /** + * Creates a new temporary directory. + * + * @param parentDirectory the directory to create the temp directory inside + * @return the path to the temporary directory + * @throws IOException if creating the directory fails + */ + public Path newDirectory(Path parentDirectory) throws IOException { + Path path = Files.createTempDirectory(parentDirectory, null); + directories.add(path); + return path; + } + + @Override + public void close() { + for (Path path : directories) { + if (Files.exists(path)) { + try { + MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); + } catch (IOException ignored) { + // ignored + } + } + } + directories.clear(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java deleted file mode 100644 index 3fd83824ce..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.filesystem; - -import com.google.common.annotations.VisibleForTesting; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.logging.Logger; - -/** - * Obtains an OS-specific user cache directory based on the XDG Base Directory Specification. - * - *

    Specifically, from the specification: - * - *

      - *
    • This directory is defined by the environment variable {@code $XDG_CACHE_HOME}. - *
    • If {@code $XDG_CACHE_HOME} is either not set or empty, a default equal to {@code - * $HOME/.cache} should be used. - *
    - * - * @see https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - */ -public class UserCacheHome { - - private static final Logger logger = Logger.getLogger(UserCacheHome.class.getName()); - - public static Path getCacheHome() { - return getCacheHome(System.getProperties(), System.getenv()); - } - - /** - * Returns {@code $XDG_CACHE_HOME}, if available, or resolves the OS-specific user cache home - * based. - * - *

    For Linus, this is {@code $HOME/.cache/}. - * - *

    For Windows, this is {@code %LOCALAPPDATA%}. - * - *

    For macOS, this is {@code $HOME/Library/Application Support/}. - */ - @VisibleForTesting - static Path getCacheHome(Properties properties, Map environment) { - // Use environment variable $XDG_CACHE_HOME if set and not empty. - String xdgCacheHome = environment.get("XDG_CACHE_HOME"); - if (xdgCacheHome != null && !xdgCacheHome.trim().isEmpty()) { - return Paths.get(xdgCacheHome); - } - - String userHome = properties.getProperty("user.home"); - - Path userHomeDirectory = Paths.get(userHome); - Path xdgPath = userHomeDirectory.resolve(".cache"); - - String rawOsName = properties.getProperty("os.name"); - String osName = rawOsName.toLowerCase(Locale.ENGLISH); - - if (osName.contains("linux")) { - return xdgPath; - - } else if (osName.contains("windows")) { - // Use %LOCALAPPDATA% for Windows. - String localAppDataEnv = environment.get("LOCALAPPDATA"); - if (localAppDataEnv == null || localAppDataEnv.trim().isEmpty()) { - logger.warning("LOCALAPPDATA environment is invalid or missing"); - return xdgPath; - } - Path localAppData = Paths.get(localAppDataEnv); - if (!Files.exists(localAppData)) { - logger.warning(localAppData + " does not exist"); - return xdgPath; - } - return localAppData; - - } else if (osName.contains("mac") || osName.contains("darwin")) { - // Use '~/Library/Application Support/' for macOS. - Path applicationSupport = userHomeDirectory.resolve("Library").resolve("Application Support"); - if (!Files.exists(applicationSupport)) { - logger.warning(applicationSupport + " does not exist"); - return xdgPath; - } - return applicationSupport; - } - - throw new IllegalStateException("Unknown OS: " + rawOsName); - } - - private UserCacheHome() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java new file mode 100644 index 0000000000..fd120e6bff --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Obtains OS-specific directories based on the XDG Base Directory Specification. + * + *

    Specifically, from the specification: + * + *

      + *
    • These directories are defined by the environment variables {@code $XDG_CACHE_HOME} and + * {@code $XDG_CONFIG_HOME}. + *
    • If {@code $XDG_CACHE_HOME} / {@code $XDG_CONFIG_HOME} is either not set or empty, a + * platform-specific equivalent of {@code $HOME/.cache} / {@code $HOME/.config} should be + * used. + *
    + * + * @see https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + */ +public class XdgDirectories { + + private static final Logger LOGGER = Logger.getLogger(XdgDirectories.class.getName()); + private static final Path JIB_SUBDIRECTORY_LINUX = + Paths.get("google-cloud-tools-java").resolve("jib"); + private static final Path JIB_SUBDIRECTORY_OTHER = Paths.get("Google").resolve("Jib"); + + public static Path getCacheHome() { + return getCacheHome(System.getProperties(), System.getenv()); + } + + public static Path getConfigHome() { + return getConfigHome(System.getProperties(), System.getenv()); + } + + /** + * Returns the default OS-specific cache directory. + * + *

    For Linux, this is {@code $HOME/.cache/google-cloud-tools-java/jib/}. + * + *

    For Windows, this is {@code %LOCALAPPDATA%\Google\Jib\Cache\}. + * + *

    For macOS, this is {@code $HOME/Library/Caches/Google/Jib/}. + */ + @VisibleForTesting + static Path getCacheHome(Properties properties, Map environment) { + return getOsSpecificDirectory( + properties, environment, "XDG_CACHE_HOME", ".cache", "Cache", "Caches"); + } + + /** + * Returns the default OS-specific config directory. + * + *

    For Linux, this is {@code $HOME/.config/google-cloud-tools-java/jib/}. + * + *

    For Windows, this is {@code %LOCALAPPDATA%\Google\Jib\Config\}. + * + *

    For macOS, this is {@code $HOME/Library/Preferences/Google/Jib/}. + */ + @VisibleForTesting + static Path getConfigHome(Properties properties, Map environment) { + return getOsSpecificDirectory( + properties, environment, "XDG_CONFIG_HOME", ".config", "Config", "Preferences"); + } + + /** + * Helper method for resolving directories on different operating systems. + * + * @param xdgEnvVariable the name of the environment variable used to resolve the XDG base + * directory + * @param linuxFolder ".config" or ".cache" + * @param windowsFolder "Config" or "Cache" + * @param macFolder "Preferences" or "Caches" + * @return the full path constructed from the given parameters + */ + private static Path getOsSpecificDirectory( + Properties properties, + Map environment, + String xdgEnvVariable, + String linuxFolder, + String windowsFolder, + String macFolder) { + + Path windowsSubDirectory = JIB_SUBDIRECTORY_OTHER.resolve(windowsFolder); + String rawOsName = properties.getProperty("os.name"); + String osName = rawOsName.toLowerCase(Locale.ENGLISH); + String xdgHome = environment.get(xdgEnvVariable); + String userHome = properties.getProperty("user.home"); + Path xdgPath = Paths.get(userHome, linuxFolder); + + if (osName.contains("linux")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(JIB_SUBDIRECTORY_LINUX); + } + return xdgPath.resolve(JIB_SUBDIRECTORY_LINUX); + + } else if (osName.contains("windows")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(windowsSubDirectory); + } + + // Use %LOCALAPPDATA% for Windows. + String localAppDataEnv = environment.get("LOCALAPPDATA"); + if (localAppDataEnv == null || localAppDataEnv.trim().isEmpty()) { + LOGGER.warning("LOCALAPPDATA environment is invalid or missing"); + return xdgPath.resolve(windowsSubDirectory); + } + Path localAppData = Paths.get(localAppDataEnv); + if (!Files.exists(localAppData)) { + LOGGER.warning(() -> localAppData + " does not exist"); + return xdgPath.resolve(windowsSubDirectory); + } + return localAppData.resolve(windowsSubDirectory); + + } else if (osName.contains("mac") || osName.contains("darwin")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(JIB_SUBDIRECTORY_OTHER); + } + + // Use '~/Library/...' for macOS. + Path macDirectory = Paths.get(userHome, "Library", macFolder); + if (!Files.exists(macDirectory)) { + LOGGER.warning(() -> macDirectory + " does not exist"); + return xdgPath.resolve(JIB_SUBDIRECTORY_OTHER); + } + return macDirectory.resolve(JIB_SUBDIRECTORY_OTHER); + } + + throw new IllegalStateException("Unknown OS: " + rawOsName); + } + + private XdgDirectories() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java deleted file mode 100644 index dc1bb950cd..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.Caches; -import com.google.cloud.tools.jib.cache.Caches.Initializer; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.net.UnknownHostException; -import java.util.concurrent.ExecutionException; -import org.apache.http.conn.HttpHostConnectException; - -/** Runs a {@link BuildSteps} and builds helpful error messages. */ -public class BuildStepsRunner { - - /** - * Creates a runner to build an image. Creates a directory for the cache, if needed. - * - * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image - * @return a {@link BuildStepsRunner} for building to a registry - * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created - */ - public static BuildStepsRunner forBuildImage( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) - throws CacheDirectoryCreationException { - return new BuildStepsRunner( - BuildSteps.forBuildToDockerRegistry( - buildConfiguration, sourceFilesConfiguration, getCacheInitializer(buildConfiguration))); - } - - /** - * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. - * - * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image - * @return a {@link BuildStepsRunner} for building to a Docker daemon - * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created - */ - public static BuildStepsRunner forBuildToDockerDaemon( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) - throws CacheDirectoryCreationException { - return new BuildStepsRunner( - BuildSteps.forBuildToDockerDaemon( - buildConfiguration, sourceFilesConfiguration, getCacheInitializer(buildConfiguration))); - } - - // TODO: Move this up to somewhere where defaults for cache location are provided and ownership is - // checked rather than in Caches.Initializer. - private static Initializer getCacheInitializer(BuildConfiguration buildConfiguration) - throws CacheDirectoryCreationException { - CacheConfiguration applicationLayersCacheConfiguration = - buildConfiguration.getApplicationLayersCacheConfiguration() == null - ? CacheConfiguration.makeTemporary() - : buildConfiguration.getApplicationLayersCacheConfiguration(); - CacheConfiguration baseImageLayersCacheConfiguration = - buildConfiguration.getBaseImageLayersCacheConfiguration() == null - ? CacheConfiguration.forDefaultUserLevelCacheDirectory() - : buildConfiguration.getBaseImageLayersCacheConfiguration(); - - return new Caches.Initializer( - baseImageLayersCacheConfiguration.getCacheDirectory(), - applicationLayersCacheConfiguration.shouldEnsureOwnership(), - applicationLayersCacheConfiguration.getCacheDirectory(), - applicationLayersCacheConfiguration.shouldEnsureOwnership()); - } - - private static void handleRegistryUnauthorizedException( - RegistryUnauthorizedException registryUnauthorizedException, - BuildConfiguration buildConfiguration, - HelpfulSuggestions helpfulSuggestions) - throws BuildStepsExecutionException { - if (registryUnauthorizedException.getHttpResponseException().getStatusCode() - == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { - // No permissions for registry/repository. - throw new BuildStepsExecutionException( - helpfulSuggestions.forHttpStatusCodeForbidden( - registryUnauthorizedException.getImageReference()), - registryUnauthorizedException); - - } else { - boolean isRegistryForBase = - registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getBaseImageRegistry()); - boolean isRegistryForTarget = - registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getTargetImageRegistry()); - boolean areBaseImageCredentialsConfigured = - buildConfiguration.getBaseImageCredentialHelperName() != null - || buildConfiguration.getKnownBaseRegistryCredentials() != null; - boolean areTargetImageCredentialsConfigured = - buildConfiguration.getTargetImageCredentialHelperName() != null - || buildConfiguration.getKnownTargetRegistryCredentials() != null; - - if (isRegistryForBase && !areBaseImageCredentialsConfigured) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forNoCredentialHelpersDefinedForBaseImage( - registryUnauthorizedException.getRegistry()), - registryUnauthorizedException); - } - if (isRegistryForTarget && !areTargetImageCredentialsConfigured) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forNoCredentialHelpersDefinedForTargetImage( - registryUnauthorizedException.getRegistry()), - registryUnauthorizedException); - } - - // Credential helper probably was not configured correctly or did not have the necessary - // credentials. - throw new BuildStepsExecutionException( - helpfulSuggestions.forCredentialsNotCorrect(registryUnauthorizedException.getRegistry()), - registryUnauthorizedException); - } - } - - private final BuildSteps buildSteps; - - @VisibleForTesting - BuildStepsRunner(BuildSteps buildSteps) { - this.buildSteps = buildSteps; - } - - /** - * Runs the {@link BuildSteps}. - * - * @param helpfulSuggestions suggestions to use in help messages for exceptions - * @throws BuildStepsExecutionException if another exception is thrown during the build - */ - public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { - try { - // TODO: This logging should be injected via another logging class. - BuildLogger buildLogger = buildSteps.getBuildConfiguration().getBuildLogger(); - - buildLogger.lifecycle(""); - buildLogger.lifecycle(buildSteps.getStartupMessage()); - - // Logs the different source files used. - buildLogger.info("Containerizing application with the following files:"); - - buildLogger.info("\tClasses:"); - buildSteps - .getSourceFilesConfiguration() - .getClassesFiles() - .forEach(classesFile -> buildLogger.info("\t\t" + classesFile)); - - buildLogger.info("\tResources:"); - buildSteps - .getSourceFilesConfiguration() - .getResourcesFiles() - .forEach(resourceFile -> buildLogger.info("\t\t" + resourceFile)); - - buildLogger.info("\tDependencies:"); - buildSteps - .getSourceFilesConfiguration() - .getDependenciesFiles() - .forEach(dependencyFile -> buildLogger.info("\t\t" + dependencyFile)); - - buildSteps.run(); - - buildLogger.lifecycle(""); - buildLogger.lifecycle(buildSteps.getSuccessMessage()); - - } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forCacheNeedsClean(), cacheMetadataCorruptedException); - - } catch (ExecutionException executionException) { - BuildConfiguration buildConfiguration = buildSteps.getBuildConfiguration(); - - Throwable exceptionDuringBuildSteps = executionException.getCause(); - - if (exceptionDuringBuildSteps instanceof HttpHostConnectException) { - // Failed to connect to registry. - throw new BuildStepsExecutionException( - helpfulSuggestions.forHttpHostConnect(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof RegistryUnauthorizedException) { - handleRegistryUnauthorizedException( - (RegistryUnauthorizedException) exceptionDuringBuildSteps, - buildConfiguration, - helpfulSuggestions); - - } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException - && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { - handleRegistryUnauthorizedException( - new RegistryUnauthorizedException( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository(), - (HttpResponseException) exceptionDuringBuildSteps.getCause()), - buildConfiguration, - helpfulSuggestions); - - } else if (exceptionDuringBuildSteps instanceof UnknownHostException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forUnknownHost(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof InsecureRegistryException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forInsecureRegistry(), exceptionDuringBuildSteps); - - } else { - throw new BuildStepsExecutionException( - helpfulSuggestions.none(), executionException.getCause()); - } - - } catch (InterruptedException | IOException | CacheDirectoryCreationException ex) { - // TODO: Add more suggestions for various build failures. - throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); - - } catch (CacheDirectoryNotOwnedException ex) { - String helpfulSuggestion = - helpfulSuggestions.forCacheDirectoryNotOwned(ex.getCacheDirectory()); - CacheConfiguration applicationLayersCacheConfiguration = - buildSteps.getBuildConfiguration().getApplicationLayersCacheConfiguration(); - if (applicationLayersCacheConfiguration != null - && ex.getCacheDirectory() - .equals(applicationLayersCacheConfiguration.getCacheDirectory())) { - helpfulSuggestion = helpfulSuggestions.forCacheNeedsClean(); - } - throw new BuildStepsExecutionException(helpfulSuggestion, ex); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java new file mode 100644 index 0000000000..0d4c97b385 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -0,0 +1,328 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +/** Static factories for various {@link CredentialRetriever}s. */ +public class CredentialRetrieverFactory { + + /** Used for passing in mock {@link DockerCredentialHelper}s for testing. */ + @VisibleForTesting + @FunctionalInterface + interface DockerCredentialHelperFactory { + DockerCredentialHelper create( + String registry, Path credentialHelper, Map environment); + } + + /** Used for passing in mock {@link GoogleCredentials} for testing. */ + @VisibleForTesting + @FunctionalInterface + interface GoogleCredentialsProvider { + GoogleCredentials get() throws IOException; + } + + // com.google.api.services.storage.StorageScopes.DEVSTORAGE_READ_WRITE + // OAuth2 credentials require at least the GCS write scope for GCR push. We need to manually set + // this scope for "OAuth2 credentials" instantiated from a service account, which are not scoped + // (i.e., createScopedRequired() returns true). Note that for a service account, the IAM roles of + // the service account determine the IAM permissions. + private static final String OAUTH_SCOPE_STORAGE_READ_WRITE = + "https://www.googleapis.com/auth/devstorage.read_write"; + + /** Mapping between well-known credential helpers and registries (suffixes). */ + private static final ImmutableMap WELL_KNOWN_CREDENTIAL_HELPERS = + ImmutableMap.of( + "gcr.io", "docker-credential-gcr", "amazonaws.com", "docker-credential-ecr-login"); + + /** + * Creates a new {@link CredentialRetrieverFactory} for an image. + * + * @param imageReference the image the credential are for + * @param logger a consumer for handling log events + * @return a new {@link CredentialRetrieverFactory} + */ + public static CredentialRetrieverFactory forImage( + ImageReference imageReference, Consumer logger) { + return new CredentialRetrieverFactory( + imageReference, + logger, + DockerCredentialHelper::new, + GoogleCredentials::getApplicationDefault, + Collections.emptyMap()); + } + + /** + * Creates a new {@link CredentialRetrieverFactory} for an image. + * + * @param imageReference the image the credential are for + * @param logger a consumer for handling log events + * @param environment environment variables for credential helper + * @return a new {@link CredentialRetrieverFactory} + */ + public static CredentialRetrieverFactory forImage( + ImageReference imageReference, Consumer logger, Map environment) { + return new CredentialRetrieverFactory( + imageReference, + logger, + DockerCredentialHelper::new, + GoogleCredentials::getApplicationDefault, + environment); + } + + private final ImageReference imageReference; + private final Consumer logger; + private final DockerCredentialHelperFactory dockerCredentialHelperFactory; + private final GoogleCredentialsProvider googleCredentialsProvider; + private final Map environment; + + @VisibleForTesting + CredentialRetrieverFactory( + ImageReference imageReference, + Consumer logger, + DockerCredentialHelperFactory dockerCredentialHelperFactory, + GoogleCredentialsProvider googleCredentialsProvider, + Map environment) { + this.imageReference = imageReference; + this.logger = logger; + this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; + this.googleCredentialsProvider = googleCredentialsProvider; + this.environment = environment; + } + + /** + * Creates a new {@link CredentialRetriever} that returns a known {@link Credential}. + * + * @param credential the known credential + * @param credentialSource the source of the credentials (for logging) + * @return a new {@link CredentialRetriever} + */ + public CredentialRetriever known(Credential credential, String credentialSource) { + return () -> { + logGotCredentialsFrom("credentials from " + credentialSource); + return Optional.of(credential); + }; + } + + /** + * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential + * helper, such as {@code docker-credential-gcr}. + * + * @param credentialHelper the credential helper executable + * @return a new {@link CredentialRetriever} + */ + public CredentialRetriever dockerCredentialHelper(String credentialHelper) { + return dockerCredentialHelper(Paths.get(credentialHelper)); + } + + /** + * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential + * helper, such as {@code docker-credential-gcr}. + * + * @param credentialHelper the credential helper executable + * @return a new {@link CredentialRetriever} + * @see https://github.com/docker/docker-credential-helpers#development + */ + public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { + return () -> { + try { + return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); + + } catch (CredentialHelperUnhandledServerUrlException ex) { + logger.accept( + LogEvent.info( + "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); + return Optional.empty(); + + } catch (IOException ex) { + throw new CredentialRetrievalException(ex); + } + }; + } + + /** + * Creates a new {@link CredentialRetriever} that tries well-known Docker credential helpers to + * retrieve credentials based on the registry of the image, such as {@code docker-credential-gcr} + * for images with the registry ending with {@code gcr.io}. + * + * @return a new {@link CredentialRetriever} + */ + public CredentialRetriever wellKnownCredentialHelpers() { + return () -> { + for (Map.Entry entry : WELL_KNOWN_CREDENTIAL_HELPERS.entrySet()) { + try { + String registrySuffix = entry.getKey(); + if (imageReference.getRegistry().endsWith(registrySuffix)) { + String credentialHelper = entry.getValue(); + return Optional.of(retrieveFromDockerCredentialHelper(Paths.get(credentialHelper))); + } + + } catch (CredentialHelperNotFoundException + | CredentialHelperUnhandledServerUrlException ex) { + if (ex.getMessage() != null) { + // Warns the user that the specified (or inferred) credential helper cannot be used. + logger.accept(LogEvent.info(ex.getMessage())); + if (ex.getCause() != null && ex.getCause().getMessage() != null) { + logger.accept(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); + } + } + + } catch (IOException ex) { + throw new CredentialRetrievalException(ex); + } + } + return Optional.empty(); + }; + } + + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Docker config + * (located at {@code System.getProperty("user.home")/.docker/config.json}). + * + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig() { + return dockerConfig( + DockerConfigCredentialRetriever.create( + imageReference.getRegistry(), + Paths.get(System.getProperty("user.home"), ".docker", "config.json"))); + } + + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from a custom path + * to a Docker config. + * + * @param dockerConfigFile the path to the Docker config file + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig(Path dockerConfigFile) { + return dockerConfig( + DockerConfigCredentialRetriever.create(imageReference.getRegistry(), dockerConfigFile)); + } + + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from a legacy + * Docker config file. + * + * @param dockerConfigFile the path to a legacy docker configuration file + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever legacyDockerConfig(Path dockerConfigFile) { + return dockerConfig( + DockerConfigCredentialRetriever.createForLegacyFormat( + imageReference.getRegistry(), dockerConfigFile)); + } + + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Google Application Default + * Credentials. + * + * @return a new {@link CredentialRetriever} + * @see https://cloud.google.com/docs/authentication/production + */ + public CredentialRetriever googleApplicationDefaultCredentials() { + return () -> { + try { + if (imageReference.getRegistry().endsWith("gcr.io") + || imageReference.getRegistry().endsWith("docker.pkg.dev")) { + GoogleCredentials googleCredentials = googleCredentialsProvider.get(); + logger.accept(LogEvent.info("Google ADC found")); + if (googleCredentials.createScopedRequired()) { // not scoped if service account + // The short-lived OAuth2 access token to be generated from the service account with + // refreshIfExpired() below will have one-hour expiry (as of Aug 2019). Instead of using + // an access token, it is technically possible to use the service account private key to + // auth with GCR, but it does not worth writing complex code to achieve that. + logger.accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); + List scope = Collections.singletonList(OAUTH_SCOPE_STORAGE_READ_WRITE); + googleCredentials = googleCredentials.createScoped(scope); + } + googleCredentials.refreshIfExpired(); + + logGotCredentialsFrom("Google Application Default Credentials"); + AccessToken accessToken = googleCredentials.getAccessToken(); + // https://cloud.google.com/container-registry/docs/advanced-authentication#access_token + return Optional.of(Credential.from("oauth2accesstoken", accessToken.getTokenValue())); + } + + } catch (IOException ex) { // Includes the case where ADC is simply not available. + logger.accept( + LogEvent.info("ADC not present or error fetching access token: " + ex.getMessage())); + } + return Optional.empty(); + }; + } + + @VisibleForTesting + CredentialRetriever dockerConfig( + DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { + return () -> { + Path configFile = dockerConfigCredentialRetriever.getDockerConfigFile(); + try { + Optional credentials = dockerConfigCredentialRetriever.retrieve(logger); + if (credentials.isPresent()) { + logGotCredentialsFrom("credentials from Docker config (" + configFile + ")"); + return credentials; + } + + } catch (IOException ex) { + logger.accept(LogEvent.info("Unable to parse Docker config file: " + configFile)); + } + return Optional.empty(); + }; + } + + private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + Credential credentials = + dockerCredentialHelperFactory + .create(imageReference.getRegistry(), credentialHelper, environment) + .retrieve(); + logGotCredentialsFrom("credential helper " + credentialHelper.getFileName().toString()); + return credentials; + } + + private void logGotCredentialsFrom(String credentialSource) { + logger.accept(LogEvent.lifecycle("Using " + credentialSource + " for " + imageReference)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java deleted file mode 100644 index 602f4dc38d..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import java.nio.file.Path; -import java.util.function.Function; - -/** Builds messages that provides suggestions on how to fix the error. */ -public class HelpfulSuggestions { - - private final String messagePrefix; - private final String clearCacheCommand; - private final String baseImageCredHelperConfiguration; - private final Function baseImageAuthConfiguration; - private final String targetImageCredHelperConfiguration; - private final Function targetImageAuthConfiguration; - - /** - * Creates a new {@link HelpfulSuggestions} with frontend-specific texts. - * - * @param messagePrefix the initial message text - * @param clearCacheCommand the command for clearing the cache - * @param baseImageCredHelperConfiguration the configuration defining the credential helper name - * for the base image - * @param baseImageAuthConfiguration the way to define raw credentials for the base image - takes - * the base image registry as an argument - * @param targetImageCredHelperConfiguration the configuration defining the credential helper name - * for the target image - * @param targetImageAuthConfiguration the way to define raw credentials for the target image - - * takes the target image registry as an argument - */ - public HelpfulSuggestions( - String messagePrefix, - String clearCacheCommand, - String baseImageCredHelperConfiguration, - Function baseImageAuthConfiguration, - String targetImageCredHelperConfiguration, - Function targetImageAuthConfiguration) { - this.messagePrefix = messagePrefix; - this.clearCacheCommand = clearCacheCommand; - this.baseImageCredHelperConfiguration = baseImageCredHelperConfiguration; - this.baseImageAuthConfiguration = baseImageAuthConfiguration; - this.targetImageCredHelperConfiguration = targetImageCredHelperConfiguration; - this.targetImageAuthConfiguration = targetImageAuthConfiguration; - } - - public String forHttpHostConnect() { - return suggest("make sure your Internet is up and that the registry you are pushing to exists"); - } - - public String forUnknownHost() { - return suggest("make sure that the registry you configured exists/is spelled properly"); - } - - public String forCacheNeedsClean() { - return suggest("run '" + clearCacheCommand + "' to clear your build cache"); - } - - public String forCacheDirectoryNotOwned(Path cacheDirectory) { - return suggest( - "check that '" - + cacheDirectory - + "' is not used by another application or set the `useOnlyProjectCache` " - + "configuration"); - } - - public String forHttpStatusCodeForbidden(String imageReference) { - return suggest("make sure you have permissions for " + imageReference); - } - - public String forNoCredentialHelpersDefinedForBaseImage(String registry) { - return forNoCredentialHelpersDefined( - baseImageCredHelperConfiguration, baseImageAuthConfiguration.apply(registry)); - } - - public String forNoCredentialHelpersDefinedForTargetImage(String registry) { - return forNoCredentialHelpersDefined( - targetImageCredHelperConfiguration, targetImageAuthConfiguration.apply(registry)); - } - - public String forCredentialsNotCorrect(String registry) { - return suggest("make sure your credentials for '" + registry + "' are set up correctly"); - } - - public String forDockerContextInsecureRecursiveDelete(String directory) { - return suggest("clear " + directory + " manually before creating the Docker context"); - } - - public String forMainClassNotFound(String pluginName) { - return suggest("add a `mainClass` configuration to " + pluginName); - } - - public String forDockerNotInstalled() { - return suggest("make sure Docker is installed and you have correct privileges to run it"); - } - - /** - * @param parameter the parameter name (e.g. 'to.image' or {@literal }) - * @param buildConfigFilename the name of the build config (build.gradle or pom.xml) - * @param command an example command for passing the parameter via commandline - * @return a suggested fix for a missing target image configuration - */ - public String forToNotConfigured(String parameter, String buildConfigFilename, String command) { - return suggest( - "add a " - + parameter - + " configuration parameter to your " - + buildConfigFilename - + " or set the parameter via the commandline (e.g. '" - + command - + "')."); - } - - public String none() { - return messagePrefix; - } - - /** - * @param suggestion a suggested fix for the problem described by {@link #messagePrefix} - * @return the message containing the suggestion - */ - public String suggest(String suggestion) { - return messagePrefix + ", perhaps you should " + suggestion; - } - - private String forNoCredentialHelpersDefined( - String credHelperConfiguration, String authConfiguration) { - return suggest( - "set a credential helper name with the configuration '" - + credHelperConfiguration - + "' or " - + authConfiguration); - } - - public String forInsecureRegistry() { - return suggest( - "use a registry that supports HTTPS or set the configuration parameter 'allowInsecureRegistries'"); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java deleted file mode 100644 index 42babaa9da..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Modifier; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.CtMethod; -import javassist.NotFoundException; -import javax.annotation.Nullable; - -/** Infers the main class in an application. */ -public class MainClassFinder { - - /** - * If {@code mainClass} is {@code null}, tries to infer main class in this order: - * - *

      - *
    • 1. Looks in a {@code jar} plugin provided by {@code projectProperties} ({@code - * maven-jar-plugin} for maven or {@code jar} task for gradle). - *
    • 2. Searches for a class defined with a main method. - *
    - * - *

    Warns if main class is not valid, or throws an error if no valid main class is not found. - * - * @param mainClass the explicitly configured main class ({@code null} if not configured). - * @param projectProperties properties containing plugin information and help messages. - * @return the name of the main class to be used for the container entrypoint. - * @throws MainClassInferenceException if no valid main class is configured or discovered. - */ - public static String resolveMainClass( - @Nullable String mainClass, ProjectProperties projectProperties) - throws MainClassInferenceException { - BuildLogger logger = projectProperties.getLogger(); - if (mainClass == null) { - logger.info( - "Searching for main class... Add a 'mainClass' configuration to '" - + projectProperties.getPluginName() - + "' to improve build speed."); - mainClass = projectProperties.getMainClassFromJar(); - if (mainClass == null || !BuildConfiguration.isValidJavaClass(mainClass)) { - logger.debug( - "Could not find a valid main class specified in " - + projectProperties.getJarPluginName() - + "; attempting to infer main class."); - - try { - // Adds each file in the classes output directory to the classes files list. - Set visitedRoots = new HashSet<>(); - List mainClasses = new ArrayList<>(); - for (Path classPath : projectProperties.getSourceFilesConfiguration().getClassesFiles()) { - Path root = classPath.getParent(); - if (visitedRoots.contains(root)) { - continue; - } - visitedRoots.add(root); - mainClasses.addAll(findMainClasses(root, logger)); - } - - if (mainClasses.size() == 1) { - // Valid class found; use inferred main class - mainClass = mainClasses.get(0); - } else if (mainClasses.size() == 0 && mainClass == null) { - // No main class found anywhere - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Main class was not found") - .forMainClassNotFound(projectProperties.getPluginName())); - } else if (mainClasses.size() > 1 && mainClass == null) { - // More than one main class found with no jar plugin to fall back on; error - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: " + String.join(", ", mainClasses)) - .forMainClassNotFound(projectProperties.getPluginName())); - } - } catch (IOException ex) { - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Failed to get main class") - .forMainClassNotFound(projectProperties.getPluginName()), - ex); - } - } - } - Preconditions.checkNotNull(mainClass); - if (!BuildConfiguration.isValidJavaClass(mainClass)) { - logger.warn("'mainClass' is not a valid Java class : " + mainClass); - } - - return mainClass; - } - - /** - * Finds the classes with {@code public static void main(String[] args)} in {@code rootDirectory}. - * - * @param rootDirectory directory containing the {@code .class} files. - * @param buildLogger used for displaying status messages. - * @return a list of class names containing a main method. - * @throws IOException if searching the root directory fails. - */ - @VisibleForTesting - static List findMainClasses(Path rootDirectory, BuildLogger buildLogger) - throws IOException { - // Makes sure rootDirectory is valid. - if (!Files.exists(rootDirectory) || !Files.isDirectory(rootDirectory)) { - return Collections.emptyList(); - } - - List classNames = new ArrayList<>(); - - ClassPool classPool = new ClassPool(); - classPool.appendSystemPath(); - - try { - CtClass[] mainMethodParams = new CtClass[] {classPool.get("java.lang.String[]")}; - - new DirectoryWalker(rootDirectory) - .filter(Files::isRegularFile) - .filter(path -> path.toString().endsWith(".class")) - .walk( - classFile -> { - try (InputStream classFileInputStream = Files.newInputStream(classFile)) { - CtClass fileClass = classPool.makeClass(classFileInputStream); - - // Check if class contains 'public static void main(String[] args)'. - CtMethod mainMethod = fileClass.getDeclaredMethod("main", mainMethodParams); - - if (CtClass.voidType.equals(mainMethod.getReturnType()) - && Modifier.isStatic(mainMethod.getModifiers()) - && Modifier.isPublic(mainMethod.getModifiers())) { - classNames.add(fileClass.getName()); - } - - } catch (NotFoundException ex) { - // Ignores main method not found. - - } catch (IOException ex) { - // Could not read class file. - buildLogger.warn("Could not read class file: " + classFile); - } - }); - - return classNames; - - } catch (NotFoundException ex) { - // Thrown if 'java.lang.String' is not found in classPool. - throw new RuntimeException(ex); - } - } - - private MainClassFinder() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java deleted file mode 100644 index 1ddfa8460c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import java.nio.file.Path; -import javax.annotation.Nullable; - -/** Project property methods that require maven/gradle-specific implementations. */ -public interface ProjectProperties { - - /** Directory name for the cache. The directory will be relative to the build output directory. */ - String CACHE_DIRECTORY_NAME = "jib-cache"; - - BuildLogger getLogger(); - - String getPluginName(); - - SourceFilesConfiguration getSourceFilesConfiguration(); - - Path getCacheDirectory(); - - String getJarPluginName(); - - /** @return the name of the main class configured in a jar plugin, or null if none is found. */ - @Nullable - String getMainClassFromJar(); - - /** - * @param prefix the prefix message for the {@link HelpfulSuggestions}. - * @return a {@link HelpfulSuggestions} instance for main class inference failure. - */ - HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java new file mode 100644 index 0000000000..d3fb7d3ee8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -0,0 +1,148 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.global; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.Range; + +/** Names of system properties defined/used by Jib. */ +public class JibSystemProperties { + + public static final String UPSTREAM_CLIENT = "_JIB_UPSTREAM_CLIENT"; + private static final String DISABLE_USER_AGENT = "_JIB_DISABLE_USER_AGENT"; + + @VisibleForTesting public static final String HTTP_TIMEOUT = "jib.httpTimeout"; + + @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; + + public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; + public static final String SERIALIZE = "jib.serialize"; + + @VisibleForTesting public static final String SKIP_EXISTING_IMAGES = "jib.skipExistingImages"; + + /** + * Gets the HTTP connection/read timeouts for registry interactions in milliseconds. This is + * defined by the {@code jib.httpTimeout} system property. The default value is 20000 if the + * system property is not set, and 0 indicates an infinite timeout. + * + * @return the HTTP connection/read timeouts for registry interactions in milliseconds + */ + public static int getHttpTimeout() { + if (Integer.getInteger(HTTP_TIMEOUT) == null) { + return 20000; + } + return Integer.getInteger(HTTP_TIMEOUT); + } + + /** + * Gets whether or not to use cross-repository blob mounts when uploading image layers + * ({@code mount/from}). This is defined by the {@code jib.blobMounts} system property. + * + * @return {@code true} if {@code mount/from} should be used, {@code false} if not, defaulting to + * {@code true} + */ + public static boolean useCrossRepositoryBlobMounts() { + return System.getProperty(CROSS_REPOSITORY_BLOB_MOUNTS) == null + || Boolean.getBoolean(CROSS_REPOSITORY_BLOB_MOUNTS); + } + + /** + * Gets whether or not to serialize Jib's execution. This is defined by the {@code jib.serialize} + * system property. + * + * @return {@code true} if Jib's execution should be serialized, {@code false} if not + */ + public static boolean serializeExecution() { + return Boolean.getBoolean(SERIALIZE); + } + + /** + * Gets whether or not to allow sending authentication information over insecure HTTP connections. + * This is defined by the {@code sendCredentialsOverHttp} system property. + * + * @return {@code true} if authentication information is allowed to be sent over insecure + * connections, {@code false} if not + */ + public static boolean sendCredentialsOverHttp() { + return Boolean.getBoolean(SEND_CREDENTIALS_OVER_HTTP); + } + + /** + * Gets whether or not to enable the User-Agent header. This is defined by the {@code + * _JIB_DISABLE_USER_AGENT} system property. + * + * @return {@code true} if the User-Agent header is enabled, {@code false} if not + */ + public static boolean isUserAgentEnabled() { + return Strings.isNullOrEmpty(System.getProperty(DISABLE_USER_AGENT)); + } + + /** + * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) + * values. + * + * @throws NumberFormatException if invalid values + */ + public static void checkHttpTimeoutProperty() throws NumberFormatException { + checkNumericSystemProperty(HTTP_TIMEOUT, Range.atLeast(0)); + } + + /** + * Checks if {@code http.proxyPort} and {@code https.proxyPort} system properties are in the + * [0..65535] range when set. + * + * @throws NumberFormatException if invalid values + */ + public static void checkProxyPortProperty() throws NumberFormatException { + checkNumericSystemProperty("http.proxyPort", Range.closed(0, 65535)); + checkNumericSystemProperty("https.proxyPort", Range.closed(0, 65535)); + } + + /** + * Gets whether or not to skip pushing tags to existing images. This is defined by the {@code + * jib.skipExistingImages} system property. + * + * @return {@code true} if Jib should skip pushing tags to existing images, {@code false} if not + */ + public static boolean skipExistingImages() { + return Boolean.getBoolean(SKIP_EXISTING_IMAGES); + } + + private static void checkNumericSystemProperty(String property, Range validRange) { + String value = System.getProperty(property); + if (value == null) { + return; + } + + int parsed; + try { + parsed = Integer.parseInt(value); + } catch (NumberFormatException ex) { + throw new NumberFormatException(property + " must be an integer: " + value); + } + if (validRange.hasLowerBound() && validRange.lowerEndpoint() > parsed) { + throw new NumberFormatException( + property + " cannot be less than " + validRange.lowerEndpoint() + ": " + value); + } else if (validRange.hasUpperBound() && validRange.upperEndpoint() < parsed) { + throw new NumberFormatException( + property + " cannot be greater than " + validRange.upperEndpoint() + ": " + value); + } + } + + private JibSystemProperties() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java index 5ee76c3f50..ad91e0b07d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.hash; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.io.OutputStream; import java.security.DigestException; @@ -30,8 +30,7 @@ public class CountingDigestOutputStream extends DigestOutputStream { private static final String SHA_256_ALGORITHM = "SHA-256"; - /** Keeps track of the total number of bytes appended. */ - private long totalBytes = 0; + private long bytesSoFar = 0; /** * Wraps the {@code outputStream}. @@ -49,12 +48,13 @@ public CountingDigestOutputStream(OutputStream outputStream) { } /** - * Builds a {@link BlobDescriptor} with the hash and size of the bytes written. The buffer resets - * after this method is called, so this method should only be called once per BlobDescriptor. + * Computes the hash and returns it along with the size of the bytes written to compute the hash. + * The buffer resets after this method is called, so this method should only be called once per + * computation. * - * @return the built {@link BlobDescriptor}. + * @return the computed hash and the size of the bytes consumed */ - public BlobDescriptor toBlobDescriptor() { + public BlobDescriptor computeDigest() { try { byte[] hashedBytes = digest.digest(); @@ -65,28 +65,25 @@ public BlobDescriptor toBlobDescriptor() { } String hash = stringBuilder.toString(); - DescriptorDigest digest = DescriptorDigest.fromHash(hash); - return new BlobDescriptor(totalBytes, digest); + BlobDescriptor blobDescriptor = + new BlobDescriptor(bytesSoFar, DescriptorDigest.fromHash(hash)); + bytesSoFar = 0; + return blobDescriptor; } catch (DigestException ex) { throw new RuntimeException("SHA-256 algorithm produced invalid hash: " + ex.getMessage(), ex); } } - /** @return the total number of bytes that were hashed */ - public long getTotalBytes() { - return totalBytes; - } - @Override public void write(byte[] data, int offset, int length) throws IOException { super.write(data, offset, length); - totalBytes += length; + bytesSoFar += length; } @Override public void write(int singleByte) throws IOException { super.write(singleByte); - totalBytes++; + bytesSoFar++; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java new file mode 100644 index 0000000000..3fd5df1551 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.hash; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * Utility class for computing a digest for various inputs while optionally writing to an output + * stream. + */ +// Note: intentionally this class does not depend on Blob, as Blob classes depend on this class. +// TODO: BlobDescriptor is merely a tuple of (size, digest). Rename BlobDescriptor to something +// more general. +public class Digests { + + private Digests() {} + + public static DescriptorDigest computeJsonDigest(JsonTemplate template) throws IOException { + return computeDigest(template, ByteStreams.nullOutputStream()).getDigest(); + } + + public static DescriptorDigest computeJsonDigest(List templates) + throws IOException { + WritableContents contents = contentsOut -> JsonTemplateMapper.writeTo(templates, contentsOut); + return computeDigest(contents, ByteStreams.nullOutputStream()).getDigest(); + } + + public static BlobDescriptor computeDigest(JsonTemplate template) throws IOException { + return computeDigest(template, ByteStreams.nullOutputStream()); + } + + public static BlobDescriptor computeDigest(JsonTemplate template, OutputStream outStream) + throws IOException { + WritableContents contents = contentsOut -> JsonTemplateMapper.writeTo(template, contentsOut); + return computeDigest(contents, outStream); + } + + public static BlobDescriptor computeDigest(InputStream inStream) throws IOException { + return computeDigest(inStream, ByteStreams.nullOutputStream()); + } + + /** + * Computes the digest by consuming the contents. + * + * @param contents the contents for which the digest is computed + * @return computed digest and bytes consumed + * @throws IOException if reading fails + */ + public static BlobDescriptor computeDigest(WritableContents contents) throws IOException { + return computeDigest(contents, ByteStreams.nullOutputStream()); + } + + /** + * Computes the digest by consuming the contents of an {@link InputStream} while copying it to an + * {@link OutputStream}. Returns the computed digest along with the size of the bytes consumed to + * compute the digest. Does not close either stream. + * + * @param inStream the stream to read the contents from + * @param outStream the stream to which the contents are copied + * @return computed digest and bytes consumed + * @throws IOException if reading from or writing fails + */ + public static BlobDescriptor computeDigest(InputStream inStream, OutputStream outStream) + throws IOException { + WritableContents contents = contentsOut -> ByteStreams.copy(inStream, contentsOut); + return computeDigest(contents, outStream); + } + + /** + * Computes the digest by consuming the contents while copying it to an {@link OutputStream}. + * Returns the computed digest along with the size of the bytes consumed to compute the digest. + * Does not close the stream. + * + * @param contents the contents to compute digest for + * @param outStream the stream to which the contents are copied + * @return computed digest and bytes consumed + * @throws IOException if reading from or writing fails + */ + public static BlobDescriptor computeDigest(WritableContents contents, OutputStream outStream) + throws IOException { + CountingDigestOutputStream digestOutStream = new CountingDigestOutputStream(outStream); + contents.writeTo(digestOutStream); + digestOutStream.flush(); + return digestOutStream.computeDigest(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java similarity index 63% rename from jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java index 5a9fd0ae0e..5fc4fae624 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2019 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,17 +14,19 @@ * the License. */ -package com.google.cloud.tools.jib.blob; +package com.google.cloud.tools.jib.hash; import java.io.IOException; import java.io.OutputStream; /** - * This function writes the contents of a BLOB. This function should write the same BLOB regardless - * of the number of times it is called. + * As a function, writes some contents to an output stream. As a class, represents contents that can + * be written to an output stream. This may be "unrealized-before-write" contents; for example, a + * file may be open and read for input contents only when this function is called to write to an + * output stream. */ @FunctionalInterface -public interface BlobWriter { +public interface WritableContents { void writeTo(OutputStream outputStream) throws IOException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index f488439f75..66d7899b1f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,10 @@ package com.google.cloud.tools.jib.http; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + /** * Holds the credentials for an HTTP {@code Authorization} header. * @@ -25,10 +29,33 @@ */ public class Authorization { + /** + * Create an authentication from basic credentials. + * + * @param username the username + * @param secret the secret + * @return an {@link Authorization} with a {@code Basic} credentials + */ + public static Authorization fromBasicCredentials(String username, String secret) { + String credentials = username + ":" + secret; + String token = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + return new Authorization("Basic", token); + } + + /** + * Create an authentication from bearer token. + * + * @param token the token + * @return an {@link Authorization} with a {@code Bearer} token + */ + public static Authorization fromBearerToken(String token) { + return new Authorization("Bearer", token); + } + private final String scheme; private final String token; - Authorization(String scheme, String token) { + private Authorization(String scheme, String token) { this.scheme = scheme; this.token = token; } @@ -46,4 +73,21 @@ public String getToken() { public String toString() { return scheme + " " + token; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Authorization)) { + return false; + } + Authorization otherAuthorization = (Authorization) other; + return scheme.equals(otherAuthorization.scheme) && token.equals(otherAuthorization.token); + } + + @Override + public int hashCode() { + return Objects.hash(scheme, token); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java deleted file mode 100644 index b393dd4258..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.util.Base64; -import java.nio.charset.StandardCharsets; - -/** Static initializers for {@link Authorization}. */ -public class Authorizations { - - /** - * @param token the token - * @return an {@link Authorization} with a {@code Bearer} token - */ - public static Authorization withBearerToken(String token) { - return new Authorization("Bearer", token); - } - - /** - * @param username the username - * @param secret the secret - * @return an {@link Authorization} with a {@code Basic} credentials - */ - public static Authorization withBasicCredentials(String username, String secret) { - String credentials = username + ":" + secret; - String token = - new String( - Base64.encodeBase64(credentials.getBytes(StandardCharsets.US_ASCII)), - StandardCharsets.UTF_8); - return new Authorization("Basic", token); - } - - /** - * @param token the token - * @return an {@link Authorization} with a base64-encoded {@code username:password} string - */ - public static Authorization withBasicToken(String token) { - return new Authorization("Basic", token); - } - - private Authorizations() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java index 54be335739..ff3acd0ea0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -20,16 +20,30 @@ import com.google.cloud.tools.jib.blob.Blob; import java.io.IOException; import java.io.OutputStream; +import java.util.function.Consumer; /** {@link Blob}-backed {@link HttpContent}. */ public class BlobHttpContent implements HttpContent { private final Blob blob; private final String contentType; + private final Consumer writtenByteCountListener; public BlobHttpContent(Blob blob, String contentType) { + this(blob, contentType, ignored -> {}); + } + + /** + * Create a new BlobHttpClient. + * + * @param blob a blob to wrap + * @param contentType the http contentType descriptor + * @param writtenByteCountListener to listen for written byte feedback + */ + public BlobHttpContent(Blob blob, String contentType, Consumer writtenByteCountListener) { this.blob = blob; this.contentType = contentType; + this.writtenByteCountListener = writtenByteCountListener; } @Override @@ -45,12 +59,12 @@ public String getType() { @Override public boolean retrySupported() { - return false; + return blob.isRetryable(); } @Override public void writeTo(OutputStream outputStream) throws IOException { - blob.writeTo(outputStream); + blob.writeTo(new NotifyingOutputStream(outputStream, writtenByteCountListener)); outputStream.flush(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java deleted file mode 100644 index 9df7dd6dd6..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.apache.ApacheHttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import java.io.Closeable; -import java.io.IOException; -import java.net.URL; -import javax.annotation.Nullable; -import org.apache.http.NoHttpResponseException; - -/** - * Sends an HTTP {@link Request} and stores the {@link Response}. - * - *

    Example usage: - * - *

    {@code
    - * try (Connection connection = new Connection(url)) {
    - *   Response response = connection.get(request);
    - *   // ... process the response
    - * }
    - * }
    - */ -public class Connection implements Closeable { - - /** - * Do not use {@link NetHttpTransport}. It does not process response errors properly. A new {@link - * ApacheHttpTransport} needs to be created for each connection because otherwise HTTP connection - * persistence causes the connection to throw {@link NoHttpResponseException}. - * - * @see https://github.com/google/google-http-java-client/issues/39 - */ - private HttpRequestFactory requestFactory = new ApacheHttpTransport().createRequestFactory(); - - @Nullable private HttpResponse httpResponse; - - /** The URL to send the request to. */ - private final GenericUrl url; - - /** - * Make sure to wrap with a try-with-resource to ensure that the connection is closed after usage. - * - * @param url the url to send the request to - */ - public Connection(URL url) { - this.url = new GenericUrl(url); - } - - @Override - public void close() throws IOException { - if (httpResponse == null) { - return; - } - - httpResponse.disconnect(); - } - - /** - * Sends the request with method GET. - * - * @param request the request to send - * @return the response to the sent request - * @throws IOException if sending the request fails - */ - public Response get(Request request) throws IOException { - return send(HttpMethods.GET, request); - } - - /** - * Sends the request with method POST. - * - * @param request the request to send - * @return the response to the sent request - * @throws IOException if sending the request fails - */ - public Response post(Request request) throws IOException { - return send(HttpMethods.POST, request); - } - - /** - * Sends the request with method PUT. - * - * @param request the request to send - * @return the response to the sent request - * @throws IOException if sending the request fails - */ - public Response put(Request request) throws IOException { - return send(HttpMethods.PUT, request); - } - - /** - * Sends the request. - * - * @param httpMethod the HTTP request method - * @param request the request to send - * @return the response to the sent request - * @throws IOException if building the HTTP request fails. - */ - public Response send(String httpMethod, Request request) throws IOException { - httpResponse = - requestFactory - .buildRequest(httpMethod, url, request.getHttpContent()) - .setHeaders(request.getHeaders()) - .execute(); - return new Response(httpResponse); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java new file mode 100644 index 0000000000..f7e1f5daa2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -0,0 +1,398 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpIOExceptionHandler; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.SslUtils; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.net.ConnectException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.net.ssl.SSLException; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClientBuilder; + +/** + * Thread-safe HTTP client that can automatically failover from secure HTTPS to insecure HTTPS or + * HTTP. Intended to be created once and shared to be called at multiple places. Callers should + * close the returned {@link Response}. + * + *

    The failover (if enabled) in the following way: + * + *

      + *
    • When a port is provided (for example {@code my-registry:5000/my-repo}): + *
        + *
      1. Attempts secure HTTPS on the specified port. + *
      2. If (1) fails due to {@link SSLException}, re-attempts secure HTTPS on the specified + * port but disabling certificate validation. + *
      3. If (2) fails again due to {@link SSLException}, attempts plain-HTTP on the specified + * port. + *
      + *
    • When a port is not provided (for example {@code my-registry/my-repo}): + *
        + *
      1. Attempts secure HTTPS on port 443 (default HTTPS port). + *
      2. If (1) fails due to {@link SSLException}, re-attempts secure HTTPS on port 443 but + * disabling certificate validation. + *
      3. If (2) fails again due to {@link SSLException}, attempts plain-HTTP on port 80 + * (default HTTP port). + *
      4. Or, if (1) fails due to non-timeout {@link ConnectException}, attempts plain-HTTP on + * port 80. + *
      + *
    + * + *

    This failover behavior is similar to how the Docker client works: + * https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry + */ +public class FailoverHttpClient { + + /** Represents failover actions taken. To be recorded in the failover history. */ + private static enum Failover { + NONE, // no failover (secure HTTPS) + INSECURE_HTTPS, // HTTPS with certificate validation disabled + HTTP // plain HTTP + } + + private static boolean isHttpsProtocol(URL url) { + return "https".equals(url.getProtocol()); + } + + private static URL toHttp(URL url) { + GenericUrl httpUrl = new GenericUrl(url); + httpUrl.setScheme("http"); + return httpUrl.toURL(); + } + + private static HttpTransport getSecureHttpTransport() { + // Do not use NetHttpTransport. It does not process response errors properly. + // See https://github.com/google/google-http-java-client/issues/39 + // + // A new ApacheHttpTransport needs to be created for each connection because otherwise HTTP + // connection persistence causes the connection to throw NoHttpResponseException. + HttpClientBuilder httpClientBuilder = + ApacheHttpTransport.newDefaultHttpClientBuilder() + // using "system socket factory" to enable sending client certificate + // https://github.com/GoogleContainerTools/jib/issues/2585 + .setSSLSocketFactory(SSLConnectionSocketFactory.getSystemSocketFactory()); + return new ApacheHttpTransport(httpClientBuilder.build()); + } + + private static HttpTransport getInsecureHttpTransport() { + try { + HttpClientBuilder httpClientBuilder = + ApacheHttpTransport.newDefaultHttpClientBuilder() + .setSSLSocketFactory(null) // creates new factory with the SSLContext given below + .setSSLContext(SslUtils.trustAllSSLContext()) + .setSSLHostnameVerifier(new NoopHostnameVerifier()); + // Do not use NetHttpTransport. See comments in getConnectionFactory for details. + return new ApacheHttpTransport(httpClientBuilder.build()); + } catch (GeneralSecurityException ex) { + throw new RuntimeException("platform does not support TLS protocol", ex); + } + } + + private final boolean enableHttpAndInsecureFailover; + private final boolean sendAuthorizationOverHttp; + private final Consumer logger; + private final Supplier secureHttpTransportFactory; + private final Supplier insecureHttpTransportFactory; + + private final ConcurrentHashMap failoverHistory = new ConcurrentHashMap<>(); + + private final Deque transportsCreated = new ArrayDeque<>(); + private final Deque responsesCreated = new ArrayDeque<>(); + private final boolean enableRetry; + + /** + * Create a new FailoverHttpclient. + * + * @param enableHttpAndInsecureFailover to enable automatic failover to insecure connection types + * @param sendAuthorizationOverHttp allow sending auth over http connections + * @param logger to receive log events + */ + public FailoverHttpClient( + boolean enableHttpAndInsecureFailover, + boolean sendAuthorizationOverHttp, + Consumer logger) { + this(enableHttpAndInsecureFailover, sendAuthorizationOverHttp, logger, true); + } + + @VisibleForTesting + FailoverHttpClient( + boolean enableHttpAndInsecureFailover, + boolean sendAuthorizationOverHttp, + Consumer logger, + boolean enableRetry) { + this( + enableHttpAndInsecureFailover, + sendAuthorizationOverHttp, + logger, + FailoverHttpClient::getSecureHttpTransport, + FailoverHttpClient::getInsecureHttpTransport, + enableRetry); + } + + @VisibleForTesting + FailoverHttpClient( + boolean enableHttpAndInsecureFailover, + boolean sendAuthorizationOverHttp, + Consumer logger, + Supplier secureHttpTransportFactory, + Supplier insecureHttpTransportFactory, + boolean enableRetry) { + this.enableHttpAndInsecureFailover = enableHttpAndInsecureFailover; + this.sendAuthorizationOverHttp = sendAuthorizationOverHttp; + this.logger = logger; + this.secureHttpTransportFactory = secureHttpTransportFactory; + this.insecureHttpTransportFactory = insecureHttpTransportFactory; + this.enableRetry = enableRetry; + } + + /** + * Closes all connections and allocated resources, whether they are currently used or not. + * + *

    If an I/O error occurs, shutdown attempts stop immediately, resulting in partial resource + * release up to that point. The method can be called again later to re-attempt releasing all + * resources. + * + * @throws IOException when I/O error shutting down resources + */ + public void shutDown() throws IOException { + synchronized (transportsCreated) { + while (!transportsCreated.isEmpty()) { + transportsCreated.peekFirst().shutdown(); + transportsCreated.removeFirst(); + } + } + synchronized (responsesCreated) { + while (!responsesCreated.isEmpty()) { + responsesCreated.peekFirst().close(); + responsesCreated.removeFirst(); + } + } + } + + /** + * Sends the request with method GET. + * + * @param url endpoint URL + * @param request the request to send + * @return the response to the sent request + * @throws IOException if sending the request fails + */ + public Response get(URL url, Request request) throws IOException { + return call(HttpMethods.GET, url, request); + } + + /** + * Sends the request with method POST. + * + * @param url endpoint URL + * @param request the request to send + * @return the response to the sent request + * @throws IOException if sending the request fails + */ + public Response post(URL url, Request request) throws IOException { + return call(HttpMethods.POST, url, request); + } + + /** + * Sends the request with method PUT. + * + * @param url endpoint URL + * @param request the request to send + * @return the response to the sent request + * @throws IOException if sending the request fails + */ + public Response put(URL url, Request request) throws IOException { + return call(HttpMethods.PUT, url, request); + } + + /** + * Sends the request. + * + * @param httpMethod the HTTP request method + * @param url endpoint URL + * @param request the request to send + * @return the response to the sent request + * @throws IOException if building the HTTP request fails. + */ + public Response call(String httpMethod, URL url, Request request) throws IOException { + if (!isHttpsProtocol(url)) { + if (enableHttpAndInsecureFailover) { // HTTP requested. We only care if HTTP is enabled. + return call(httpMethod, url, request, getHttpTransport(true), true); + } + throw new SSLException("insecure HTTP connection not allowed: " + url); + } + + Optional fastPathResponse = followFailoverHistory(httpMethod, url, request); + if (fastPathResponse.isPresent()) { + return fastPathResponse.get(); + } + + try { + return call(httpMethod, url, request, getHttpTransport(true), !enableHttpAndInsecureFailover); + + } catch (SSLException ex) { + if (!enableHttpAndInsecureFailover) { + throw ex; + } + + try { + logInsecureHttpsFailover(url); + Response response = call(httpMethod, url, request, getHttpTransport(false), false); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.INSECURE_HTTPS); + return response; + + } catch (SSLException ignored) { // This is usually when the server is plain-HTTP. + logHttpFailover(url); + Response response = call(httpMethod, toHttp(url), request, getHttpTransport(true), true); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.HTTP); + return response; + } + + } catch (ConnectException ex) { + // It is observed that Open/Oracle JDKs sometimes throw SocketTimeoutException but other times + // ConnectException for connection timeout. (Could be a JDK bug.) Note SocketTimeoutException + // does not extend ConnectException (or vice versa), and we want to be consistent to error out + // on timeouts: https://github.com/GoogleContainerTools/jib/issues/1895#issuecomment-527544094 + if (ex.getMessage() == null || !ex.getMessage().contains("timed out")) { + // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS + // port 443) and we could not connect to 443. It's worth trying port 80. + if (enableHttpAndInsecureFailover && isHttpsProtocol(url) && url.getPort() == -1) { + logHttpFailover(url); + Response response = call(httpMethod, toHttp(url), request, getHttpTransport(true), true); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.HTTP); + return response; + } + } + throw ex; + } + } + + private Optional followFailoverHistory(String httpMethod, URL url, Request request) + throws IOException { + Preconditions.checkArgument(isHttpsProtocol(url)); + switch (failoverHistory.getOrDefault(url.getHost() + ":" + url.getPort(), Failover.NONE)) { + case HTTP: + return Optional.of(call(httpMethod, toHttp(url), request, getHttpTransport(true), true)); + case INSECURE_HTTPS: + return Optional.of(call(httpMethod, url, request, getHttpTransport(false), true)); + default: + return Optional.empty(); // No history found. Should go for normal execution path. + } + } + + // TODO: remove retryOnIoException and turn on/off retry based on whether it's an SSLException or + // not: https://github.com/GoogleContainerTools/jib/issues/3422 + private Response call( + String httpMethod, + URL url, + Request request, + HttpTransport httpTransport, + boolean retryOnIoException) // See https://github.com/GoogleContainerTools/jib/issues/3424 + throws IOException { + boolean clearAuthorization = !isHttpsProtocol(url) && !sendAuthorizationOverHttp; + + HttpHeaders requestHeaders = + clearAuthorization + ? request.getHeaders().clone().setAuthorization((String) null) // deep clone implemented + : request.getHeaders(); + + HttpRequest httpRequest = + httpTransport + .createRequestFactory() + .buildRequest(httpMethod, new GenericUrl(url), request.getHttpContent()) + .setUseRawRedirectUrls(true) + .setHeaders(requestHeaders); + if (enableRetry && retryOnIoException) { + httpRequest.setIOExceptionHandler(createBackOffRetryHandler()); + } + if (request.getHttpTimeout() != null) { + httpRequest.setConnectTimeout(request.getHttpTimeout()); + httpRequest.setReadTimeout(request.getHttpTimeout()); + } + + try { + Response response = new Response(httpRequest.execute()); + synchronized (responsesCreated) { + responsesCreated.add(response); + } + return response; + } catch (HttpResponseException ex) { + throw new ResponseException(ex, clearAuthorization); + } + } + + private HttpIOExceptionHandler createBackOffRetryHandler() { + return new HttpBackOffIOExceptionHandler(new ExponentialBackOff()) { + @Override + public boolean handleIOException(HttpRequest request, boolean supportsRetry) + throws IOException { + String requestUrl = request.getRequestMethod() + " " + request.getUrl(); + if (super.handleIOException(request, supportsRetry)) { + logger.accept(LogEvent.warn(requestUrl + " failed and will be retried")); + return true; + } + logger.accept(LogEvent.warn(requestUrl + " failed and will NOT be retried")); + return false; + } + }; + } + + private HttpTransport getHttpTransport(boolean secureTransport) { + HttpTransport transport = + secureTransport ? secureHttpTransportFactory.get() : insecureHttpTransportFactory.get(); + synchronized (transportsCreated) { + transportsCreated.add(transport); + } + return transport; + } + + private void logHttpFailover(URL url) { + String log = "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP."; + logger.accept(LogEvent.warn(log)); + } + + private void logInsecureHttpsFailover(URL url) { + String log = "Cannot verify server at " + url + ". Attempting again with no TLS verification."; + logger.accept(LogEvent.warn(log)); + } + + @VisibleForTesting + public Deque getTransportsCreated() { + return transportsCreated; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java new file mode 100644 index 0000000000..74023ec141 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.function.Consumer; + +/** Counts the number of bytes written and reports the count to a callback. */ +public class NotifyingOutputStream extends OutputStream { + + /** The underlying {@link OutputStream} to wrap and forward bytes to. */ + private final OutputStream underlyingOutputStream; + + /** Receives a count of bytes written since the last call. */ + private final Consumer byteCountListener; + + /** Number of bytes to provide to {@link #byteCountListener}. */ + private long byteCount = 0; + + /** + * Wraps the {@code underlyingOutputStream} to count the bytes written. + * + * @param underlyingOutputStream the wrapped {@link OutputStream} + * @param byteCountListener the byte count {@link Consumer} + */ + public NotifyingOutputStream( + OutputStream underlyingOutputStream, Consumer byteCountListener) { + this.underlyingOutputStream = underlyingOutputStream; + this.byteCountListener = byteCountListener; + } + + @Override + public void write(int singleByte) throws IOException { + underlyingOutputStream.write(singleByte); + countAndCallListener(1); + } + + @Override + public void write(byte[] byteArray) throws IOException { + underlyingOutputStream.write(byteArray); + countAndCallListener(byteArray.length); + } + + @Override + public void write(byte[] byteArray, int offset, int length) throws IOException { + underlyingOutputStream.write(byteArray, offset, length); + countAndCallListener(length); + } + + @Override + public void flush() throws IOException { + underlyingOutputStream.flush(); + countAndCallListener(0); + } + + @Override + public void close() throws IOException { + underlyingOutputStream.close(); + countAndCallListener(0); + } + + private void countAndCallListener(int written) { + this.byteCount += written; + if (byteCount == 0) { + return; + } + + byteCountListener.accept(byteCount); + byteCount = 0; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java index b3fae5cf59..fc5a766ccb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.http; +import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpHeaders; import java.util.List; import javax.annotation.Nullable; @@ -27,12 +28,16 @@ public class Request { private final HttpHeaders headers; /** The HTTP request body. */ - @Nullable private BlobHttpContent body; + @Nullable private final HttpContent body; + + /** HTTP connection and read timeout. */ + @Nullable private final Integer httpTimeout; public static class Builder { private final HttpHeaders headers = new HttpHeaders().setAccept("*/*"); - @Nullable private BlobHttpContent body; + @Nullable private HttpContent body; + @Nullable private Integer httpTimeout; public Request build() { return new Request(this); @@ -45,9 +50,7 @@ public Request build() { * @return this */ public Builder setAuthorization(@Nullable Authorization authorization) { - if (authorization != null) { - headers.setAuthorization(authorization.toString()); - } + headers.setAuthorization(authorization == null ? null : authorization.toString()); return this; } @@ -68,19 +71,31 @@ public Builder setAccept(List mimeTypes) { * @param userAgent the user agent * @return this */ - public Builder setUserAgent(String userAgent) { + public Builder setUserAgent(@Nullable String userAgent) { headers.setUserAgent(userAgent); return this; } + /** + * Sets the HTTP connection and read timeout in milliseconds. {@code null} uses the default + * timeout and {@code 0} an infinite timeout. + * + * @param httpTimeout timeout in milliseconds + * @return this + */ + public Builder setHttpTimeout(@Nullable Integer httpTimeout) { + this.httpTimeout = httpTimeout; + return this; + } + /** * Sets the body and its corresponding {@code Content-Type} header. * - * @param blobHttpContent the body content + * @param httpContent the body content * @return this */ - public Builder setBody(@Nullable BlobHttpContent blobHttpContent) { - this.body = blobHttpContent; + public Builder setBody(@Nullable HttpContent httpContent) { + this.body = httpContent; return this; } } @@ -92,6 +107,7 @@ public static Builder builder() { private Request(Builder builder) { this.headers = builder.headers; this.body = builder.body; + this.httpTimeout = builder.httpTimeout; } HttpHeaders getHeaders() { @@ -99,7 +115,12 @@ HttpHeaders getHeaders() { } @Nullable - BlobHttpContent getHttpContent() { + HttpContent getHttpContent() { return body; } + + @Nullable + Integer getHttpTimeout() { + return httpTimeout; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java index b86afd388a..eddfb21dca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,14 +18,14 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponse; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.common.net.HttpHeaders; +import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** Holds an HTTP response. */ -public class Response { +public class Response implements Closeable { private final HttpResponse httpResponse; @@ -33,12 +33,18 @@ public class Response { this.httpResponse = httpResponse; } - /** @return the HTTP status code of the response */ + /** + * Returns the HTTP status code of the response. + * + * @return the HTTP status code of the response + */ public int getStatusCode() { return httpResponse.getStatusCode(); } /** + * Returns a list of the header string values for the given header name. + * * @param headerName the header name * @return a list of headers in the response */ @@ -47,6 +53,8 @@ public List getHeader(String headerName) { } /** + * Returns the content length from the header. + * * @return the first {@code Content-Length} header, or {@code -1} if not found * @throws NumberFormatException if parsing the content length header fails */ @@ -65,15 +73,26 @@ public long getContentLength() throws NumberFormatException { } /** - * @return the HTTP response body as a {@link Blob}. + * Returns the content of the HTTP response. + * + * @return the HTTP response body as an {@link InputStream}. * @throws IOException if getting the HTTP response content fails. */ - public Blob getBody() throws IOException { - return Blobs.from(httpResponse.getContent()); + public InputStream getBody() throws IOException { + return httpResponse.getContent(); } - /** @return the original request URL */ + /** + * Returns the original request URL. + * + * @return the original request URL + */ public GenericUrl getRequestUrl() { return httpResponse.getRequest().getUrl(); } + + @Override + public void close() throws IOException { + httpResponse.disconnect(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java new file mode 100644 index 0000000000..d798e01ce0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; +import java.io.IOException; + +/** Holds an HTTP response exception. */ +public class ResponseException extends IOException { + + private final HttpResponseException httpResponseException; + private final boolean requestAuthorizationCleared; + + ResponseException( + HttpResponseException httpResponseException, boolean requestAuthorizationCleared) { + super(httpResponseException.getMessage(), httpResponseException); + this.httpResponseException = httpResponseException; + this.requestAuthorizationCleared = requestAuthorizationCleared; + } + + public int getStatusCode() { + return httpResponseException.getStatusCode(); + } + + public String getContent() { + return httpResponseException.getContent(); + } + + public HttpHeaders getHeaders() { + return httpResponseException.getHeaders(); + } + + /** + * Returns whether the {@code Authorization} HTTP header was cleared (and thus not sent). + * + * @return whether the {@code Authorization} HTTP header was cleared + */ + public boolean requestAuthorizationCleared() { + return requestAuthorizationCleared; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java index 0de5df3ebb..fdeebc6074 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index b2735dd523..687c9a1289 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,33 +16,94 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; +import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; /** Represents an image. */ -public class Image { +public class Image { /** Builds the immutable {@link Image}. */ - public static class Builder { + public static class Builder { - private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); - private final ImmutableList.Builder environmentBuilder = ImmutableList.builder(); + private final Class imageFormat; + private final ImmutableList.Builder imageLayersBuilder = ImmutableList.builder(); + private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); - private ImmutableList entrypoint = ImmutableList.of(); - private ImmutableList javaArguments = ImmutableList.of(); - private ImmutableList exposedPorts = ImmutableList.of(); + // Don't use ImmutableMap.Builder because it does not allow for replacing existing keys with new + // values. + private final Map environmentBuilder = new HashMap<>(); + private final Map labelsBuilder = new HashMap<>(); + private final Set exposedPortsBuilder = new HashSet<>(); + private final Set volumesBuilder = new HashSet<>(); + + @Nullable private Instant created; + private String architecture = "amd64"; + private String os = "linux"; + @Nullable private ImmutableList entrypoint; + @Nullable private ImmutableList programArguments; + @Nullable private DockerHealthCheck healthCheck; + @Nullable private String workingDirectory; + @Nullable private String user; + + private Builder(Class imageFormat) { + this.imageFormat = imageFormat; + } + + /** + * Sets the image creation time. + * + * @param created the creation time + * @return this + */ + public Builder setCreated(Instant created) { + this.created = created; + return this; + } /** - * Sets the environment with a map from environment variable names to values. + * Sets the image architecture. + * + * @param architecture the architecture + * @return this + */ + public Builder setArchitecture(String architecture) { + this.architecture = architecture; + return this; + } + + /** + * Sets the image operating system. + * + * @param os the operating system + * @return this + */ + public Builder setOs(String os) { + this.os = os; + return this; + } + + /** + * Adds a map of environment variables to the current map. * * @param environment the map of environment variables * @return this */ - public Builder setEnvironment(Map environment) { - for (Map.Entry environmentVariable : environment.entrySet()) { - setEnvironmentVariable(environmentVariable.getKey(), environmentVariable.getValue()); + public Builder addEnvironment(@Nullable Map environment) { + if (environment != null) { + environmentBuilder.putAll(environment); } return this; } @@ -54,52 +115,115 @@ public Builder setEnvironment(Map environment) { * @param value the value to set it to * @return this */ - public Builder setEnvironmentVariable(String name, String value) { - environmentBuilder.add(name + "=" + value); + public Builder addEnvironmentVariable(String name, String value) { + environmentBuilder.put(name, value); return this; } /** - * Adds an environment variable definition in the format {@code NAME=VALUE}. + * Sets the entrypoint of the image. * - * @param environmentVariableDefinition the definition to add + * @param entrypoint the list of entrypoint tokens * @return this */ - public Builder addEnvironmentVariableDefinition(String environmentVariableDefinition) { - environmentBuilder.add(environmentVariableDefinition); + public Builder setEntrypoint(@Nullable List entrypoint) { + this.entrypoint = (entrypoint == null) ? null : ImmutableList.copyOf(entrypoint); return this; } /** - * Sets the entrypoint of the image. + * Sets the user/group to run the container as. * - * @param entrypoint the list of entrypoint tokens + * @param user the username/UID and optionally the groupname/GID + * @return this + */ + public Builder setUser(@Nullable String user) { + this.user = user; + return this; + } + + /** + * Sets the items in the "Cmd" field in the container configuration. + * + * @param programArguments the list of arguments to append to the image entrypoint + * @return this + */ + public Builder setProgramArguments(@Nullable List programArguments) { + this.programArguments = + (programArguments == null) ? null : ImmutableList.copyOf(programArguments); + return this; + } + + /** + * Sets the container's healthcheck configuration. + * + * @param healthCheck the healthcheck configuration + * @return this + */ + public Builder setHealthCheck(@Nullable DockerHealthCheck healthCheck) { + this.healthCheck = healthCheck; + return this; + } + + /** + * Adds items to the "ExposedPorts" field in the container configuration. + * + * @param exposedPorts the exposed ports to add + * @return this + */ + public Builder addExposedPorts(@Nullable Set exposedPorts) { + if (exposedPorts != null) { + exposedPortsBuilder.addAll(exposedPorts); + } + return this; + } + + /** + * Adds items to the "Volumes" field in the container configuration. + * + * @param volumes the directories to create volumes + * @return this + */ + public Builder addVolumes(@Nullable Set volumes) { + if (volumes != null) { + volumesBuilder.addAll(ImmutableSet.copyOf(volumes)); + } + return this; + } + + /** + * Adds items to the "Labels" field in the container configuration. + * + * @param labels the map of labels to add * @return this */ - public Builder setEntrypoint(List entrypoint) { - this.entrypoint = ImmutableList.copyOf(entrypoint); + public Builder addLabels(@Nullable Map labels) { + if (labels != null) { + labelsBuilder.putAll(labels); + } return this; } /** - * Sets the items in the "Cmd" field in the container configuration (i.e. the main args). + * Adds an item to the "Labels" field in the container configuration. * - * @param javaArguments the list of main args to add + * @param name the name of the label + * @param value the value of the label * @return this */ - public Builder setJavaArguments(List javaArguments) { - this.javaArguments = ImmutableList.copyOf(javaArguments); + public Builder addLabel(String name, String value) { + labelsBuilder.put(name, value); return this; } /** - * Sets the items in the "ExposedPorts" field in the container configuration. + * Sets the item in the "WorkingDir" field in the container configuration. * - * @param exposedPorts the list of exposed ports to add + * @param workingDirectory the working directory * @return this */ - public Builder setExposedPorts(ImmutableList exposedPorts) { - this.exposedPorts = exposedPorts; + public Builder setWorkingDirectory(@Nullable String workingDirectory) { + this.workingDirectory = workingDirectory; return this; } @@ -110,70 +234,196 @@ public Builder setExposedPorts(ImmutableList exposedPorts) { * @return this * @throws LayerPropertyNotFoundException if adding the layer fails */ - public Builder addLayer(T layer) throws LayerPropertyNotFoundException { + public Builder addLayer(Layer layer) throws LayerPropertyNotFoundException { imageLayersBuilder.add(layer); return this; } - public Image build() { - return new Image<>( + /** + * Adds a history element to the image. + * + * @param history the history object to add + * @return this + */ + public Builder addHistory(HistoryEntry history) { + historyBuilder.add(history); + return this; + } + + /** + * Create an {@link Image} instance. + * + * @return a new {@link Image} instance + */ + public Image build() { + return new Image( + imageFormat, + created, + architecture, + os, imageLayersBuilder.build(), - environmentBuilder.build(), - ImmutableList.copyOf(entrypoint), - ImmutableList.copyOf(javaArguments), - exposedPorts); + historyBuilder.build(), + ImmutableMap.copyOf(environmentBuilder), + entrypoint, + programArguments, + healthCheck, + ImmutableSet.copyOf(exposedPortsBuilder), + ImmutableSet.copyOf(volumesBuilder), + ImmutableMap.copyOf(labelsBuilder), + workingDirectory, + user); } } - public static Builder builder() { - return new Builder<>(); + public static Builder builder(Class imageFormat) { + return new Builder(imageFormat); } + /** The image format. */ + private final Class imageFormat; + + /** The image creation time. */ + @Nullable private final Instant created; + + /** The image architecture. */ + private final String architecture; + + /** The image operating system. */ + private final String os; + /** The layers of the image, in the order in which they are applied. */ - private final ImageLayers layers; + private final ImmutableList layers; + + /** The commands used to build each layer of the image. */ + private final ImmutableList history; /** Environment variable definitions for running the image, in the format {@code NAME=VALUE}. */ - private final ImmutableList environmentBuilder; + @Nullable private final ImmutableMap environment; /** Initial command to run when running the image. */ - private final ImmutableList entrypoint; + @Nullable private final ImmutableList entrypoint; + + /** Arguments to append to the image entrypoint when running the image. */ + @Nullable private final ImmutableList programArguments; - /** Arguments to pass into main when running the image. */ - private final ImmutableList javaArguments; + /** Healthcheck configuration. */ + @Nullable private final DockerHealthCheck healthCheck; /** Ports that the container listens on. */ - private final ImmutableList exposedPorts; + @Nullable private final ImmutableSet exposedPorts; + + /** Directories to mount as volumes. */ + @Nullable private final ImmutableSet volumes; + + /** Labels on the container configuration. */ + @Nullable private final ImmutableMap labels; + + /** Working directory on the container configuration. */ + @Nullable private final String workingDirectory; + + /** User on the container configuration. */ + @Nullable private final String user; private Image( - ImageLayers layers, - ImmutableList environment, - ImmutableList entrypoint, - ImmutableList javaArguments, - ImmutableList exposedPorts) { + Class imageFormat, + @Nullable Instant created, + String architecture, + String os, + ImmutableList layers, + ImmutableList history, + @Nullable ImmutableMap environment, + @Nullable ImmutableList entrypoint, + @Nullable ImmutableList programArguments, + @Nullable DockerHealthCheck healthCheck, + @Nullable ImmutableSet exposedPorts, + @Nullable ImmutableSet volumes, + @Nullable ImmutableMap labels, + @Nullable String workingDirectory, + @Nullable String user) { + this.imageFormat = imageFormat; + this.created = created; + this.architecture = architecture; + this.os = os; this.layers = layers; - this.environmentBuilder = environment; + this.history = history; + this.environment = environment; this.entrypoint = entrypoint; - this.javaArguments = javaArguments; + this.programArguments = programArguments; + this.healthCheck = healthCheck; this.exposedPorts = exposedPorts; + this.volumes = volumes; + this.labels = labels; + this.workingDirectory = workingDirectory; + this.user = user; + } + + public Class getImageFormat() { + return imageFormat; + } + + @Nullable + public Instant getCreated() { + return created; + } + + public String getArchitecture() { + return architecture; + } + + public String getOs() { + return os; } - public ImmutableList getEnvironment() { - return environmentBuilder; + @Nullable + public ImmutableMap getEnvironment() { + return environment; } + @Nullable public ImmutableList getEntrypoint() { return entrypoint; } - public ImmutableList getJavaArguments() { - return javaArguments; + @Nullable + public ImmutableList getProgramArguments() { + return programArguments; + } + + @Nullable + public DockerHealthCheck getHealthCheck() { + return healthCheck; } - public ImmutableList getExposedPorts() { + @Nullable + public ImmutableSet getExposedPorts() { return exposedPorts; } - public ImmutableList getLayers() { - return layers.getLayers(); + @Nullable + public ImmutableSet getVolumes() { + return volumes; + } + + @Nullable + public ImmutableMap getLabels() { + return labels; + } + + @Nullable + public String getWorkingDirectory() { + return workingDirectory; + } + + @Nullable + public String getUser() { + return user; + } + + public ImmutableList getLayers() { + return layers; + } + + public ImmutableList getHistory() { + return history; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java deleted file mode 100644 index ed80a0346d..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2018 Google LLC. all rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; - -/** Enumeration of {@link BuildableManifestTemplate}s. */ -public enum ImageFormat { - Docker(V22ManifestTemplate.class), - OCI(OCIManifestTemplate.class); - - private final Class manifestTemplateClass; - - ImageFormat(Class manifestTemplateClass) { - this.manifestTemplateClass = manifestTemplateClass; - } - - public Class getManifestTemplateClass() { - return manifestTemplateClass; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java deleted file mode 100644 index a52dfd7280..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import java.util.Iterator; -import javax.annotation.Nullable; - -/** Holds the layers for an image. Makes sure that each layer is only added once. */ -public class ImageLayers implements Iterable { - - public static class Builder { - - private final ImmutableList.Builder layersBuilder = ImmutableList.builder(); - private final ImmutableSet.Builder layerDigestsBuilder = - ImmutableSet.builder(); - - /** The last layer added. */ - @Nullable private T lastLayer; - - /** - * Adds a layer. - * - * @param layer the layer to add - * @return this - * @throws LayerPropertyNotFoundException if adding the layer fails - */ - public Builder add(T layer) throws LayerPropertyNotFoundException { - // Doesn't add the layer if the last layer is the same. - if (!isSameAsLastLayer(layer)) { - layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); - layersBuilder.add(layer); - lastLayer = layer; - } - - return this; - } - - /** - * Adds all layers in {@code layers}. - * - * @param child type of {@link Layer} - * @param layers the layers to add - * @return this - * @throws LayerPropertyNotFoundException if adding a layer fails - */ - public Builder addAll(ImageLayers layers) - throws LayerPropertyNotFoundException { - for (U layer : layers) { - add(layer); - } - return this; - } - - public ImageLayers build() { - return new ImageLayers<>(layersBuilder.build(), layerDigestsBuilder.build()); - } - - /** - * @param layer the layer to compare - * @return {@code true} if {@code layer} is the same as the last layer in {@link #layers} - * @throws LayerPropertyNotFoundException if getting the last layer's blob descriptor fails - */ - private boolean isSameAsLastLayer(T layer) throws LayerPropertyNotFoundException { - return lastLayer != null - && layer - .getBlobDescriptor() - .getDigest() - .equals(lastLayer.getBlobDescriptor().getDigest()); - } - } - - public static Builder builder() { - return new Builder<>(); - } - - /** The layers of the image, in the order in which they are applied. */ - private final ImmutableList layers; - - /** Keeps track of the layers already added. */ - private final ImmutableSet layerDigests; - - private ImageLayers(ImmutableList layers, ImmutableSet layerDigests) { - this.layers = layers; - this.layerDigests = layerDigests; - } - - /** @return a read-only view of the image layers. */ - public ImmutableList getLayers() { - return layers; - } - - /** @return the layer count */ - public int size() { - return layers.size(); - } - - public boolean isEmpty() { - return layers.isEmpty(); - } - - /** - * @param index the index of the layer to get - * @return the layer at the specified index - */ - public T get(int index) { - return layers.get(index); - } - - /** - * @param digest the digest used to retrieve the layer - * @return the layer found, or {@code null} if not found - * @throws LayerPropertyNotFoundException if getting the layer's blob descriptor fails - */ - @Nullable - public T get(DescriptorDigest digest) throws LayerPropertyNotFoundException { - if (!has(digest)) { - return null; - } - for (T layer : layers) { - if (layer.getBlobDescriptor().getDigest().equals(digest)) { - return layer; - } - } - throw new IllegalStateException("Layer digest exists but layer not found"); - } - - /** - * @param digest the digest to check for - * @return true if the layer with the specified digest exists; false otherwise - */ - public boolean has(DescriptorDigest digest) { - return layerDigests.contains(digest); - } - - @Override - public Iterator iterator() { - return getLayers().iterator(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java deleted file mode 100644 index 17ffc2fa65..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.common.base.Strings; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; - -/** - * Represents an image reference. - * - * @see https://github.com/docker/distribution/blob/master/reference/reference.go - * @see https://docs.docker.com/engine/reference/commandline/tag/#extended-description - */ -public class ImageReference { - - private static final String DOCKER_HUB_REGISTRY = "registry.hub.docker.com"; - private static final String DEFAULT_TAG = "latest"; - private static final String LIBRARY_REPOSITORY_PREFIX = "library/"; - - /** - * Matches all sequences of alphanumeric characters possibly separated by any number of dashes in - * the middle. - */ - private static final String REGISTRY_COMPONENT_REGEX = - "(?:[a-zA-Z\\d]|(?:[a-zA-Z\\d][a-zA-Z\\d-]*[a-zA-Z\\d]))"; - - /** - * Matches sequences of {@code REGISTRY_COMPONENT_REGEX} separated by a dot, with an optional - * {@code :port} at the end. - */ - private static final String REGISTRY_REGEX = - String.format("%s(?:\\.%s)*(?::\\d+)?", REGISTRY_COMPONENT_REGEX, REGISTRY_COMPONENT_REGEX); - - /** - * Matches all sequences of alphanumeric characters separated by a separator. - * - *

    A separator is either an underscore, a dot, two underscores, or any number of dashes. - */ - private static final String REPOSITORY_COMPONENT_REGEX = - "[a-z\\d]+(?:(?:[_.]|__|[-]*)[a-z\\d]+)*"; - - /** Matches all repetitions of {@code REPOSITORY_COMPONENT_REGEX} separated by a backslash. */ - private static final String REPOSITORY_REGEX = - String.format("(?:%s/)*%s", REPOSITORY_COMPONENT_REGEX, REPOSITORY_COMPONENT_REGEX); - - /** Matches a tag of max length 128. */ - private static final String TAG_REGEX = "[\\w][\\w.-]{0,127}"; - - /** - * Matches a full image reference, which is the registry, repository, and tag/digest separated by - * backslashes. The repository is required, but the registry and tag/digest are optional. - */ - private static final String REFERENCE_REGEX = - String.format( - "^(?:(%s)/)?(%s)(?:(?::(%s))|(?:@(%s)))?$", - REGISTRY_REGEX, REPOSITORY_REGEX, TAG_REGEX, DescriptorDigest.DIGEST_REGEX); - - private static final Pattern REFERENCE_PATTERN = Pattern.compile(REFERENCE_REGEX); - - /** - * @param reference the string to parse - * @return an {@link ImageReference} parsed from the string - * @throws InvalidImageReferenceException if {@code reference} is formatted incorrectly - */ - public static ImageReference parse(String reference) throws InvalidImageReferenceException { - Matcher matcher = REFERENCE_PATTERN.matcher(reference); - - if (!matcher.find() || matcher.groupCount() < 4) { - throw new InvalidImageReferenceException(reference); - } - - String registry = matcher.group(1); - String repository = matcher.group(2); - String tag = matcher.group(3); - String digest = matcher.group(4); - - // If no registry was matched, use Docker Hub by default. - if (Strings.isNullOrEmpty(registry)) { - registry = DOCKER_HUB_REGISTRY; - } - - if (Strings.isNullOrEmpty(repository)) { - throw new InvalidImageReferenceException(reference); - } - /* - * If a registry was matched but it does not contain any dots or colons, it should actually be - * part of the repository unless it is "localhost". - * - * See https://github.com/docker/distribution/blob/245ca4659e09e9745f3cc1217bf56e946509220c/reference/normalize.go#L62 - */ - if (!registry.contains(".") && !registry.contains(":") && !"localhost".equals(registry)) { - repository = registry + "/" + repository; - registry = DOCKER_HUB_REGISTRY; - } - /* - * For Docker Hub, if the repository is only one component, then it should be prefixed with - * 'library/'. - * - * See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-from-docker-hub - */ - if (DOCKER_HUB_REGISTRY.equals(registry) && repository.indexOf('/') < 0) { - repository = LIBRARY_REPOSITORY_PREFIX + repository; - } - - if (!Strings.isNullOrEmpty(tag)) { - if (!Strings.isNullOrEmpty(digest)) { - // Cannot have matched both tag and digest. - throw new InvalidImageReferenceException(reference); - } - } else if (!Strings.isNullOrEmpty(digest)) { - tag = digest; - } else { - tag = DEFAULT_TAG; - } - - return new ImageReference(registry, repository, tag); - } - - /** - * @param registry the image registry - * @param repository the image repository - * @param tag the image tag - * @return an {@link ImageReference} built from the given registry, repository, and tag - */ - public static ImageReference of( - @Nullable String registry, String repository, @Nullable String tag) { - if (Strings.isNullOrEmpty(registry)) { - registry = DOCKER_HUB_REGISTRY; - } - if (Strings.isNullOrEmpty(tag)) { - tag = DEFAULT_TAG; - } - return new ImageReference(registry, repository, tag); - } - - /** - * @param registry the registry to check - * @return {@code true} if is a valid registry; {@code false} otherwise - */ - public static boolean isValidRegistry(String registry) { - return registry.matches(REGISTRY_REGEX); - } - - /** - * @param repository the repository to check - * @return {@code true} if is a valid repository; {@code false} otherwise - */ - public static boolean isValidRepository(String repository) { - return repository.matches(REPOSITORY_REGEX); - } - - /** - * @param tag the tag to check - * @return {@code true} if is a valid tag; {@code false} otherwise - */ - public static boolean isValidTag(String tag) { - return tag.matches(TAG_REGEX); - } - - private final String registry; - private final String repository; - private final String tag; - - /** Use {@link #parse} to construct. */ - private ImageReference(String registry, String repository, String tag) { - this.registry = registry; - this.repository = repository; - this.tag = tag; - } - - public String getRegistry() { - return registry; - } - - public String getRepository() { - return repository; - } - - public String getTag() { - return tag; - } - - public boolean usesDefaultTag() { - return DEFAULT_TAG.equals(tag); - } - - /** @return the image reference in Docker-readable format (inverse of {@link #parse}) */ - @Override - public String toString() { - StringBuilder referenceString = new StringBuilder(); - - if (!DOCKER_HUB_REGISTRY.equals(registry)) { - // Use registry and repository if not Docker Hub. - referenceString.append(registry).append('/').append(repository); - - } else if (repository.startsWith(LIBRARY_REPOSITORY_PREFIX)) { - // If Docker Hub and repository has 'library/' prefix, remove the 'library/' prefix. - referenceString.append(repository.substring(LIBRARY_REPOSITORY_PREFIX.length())); - - } else { - // Use just repository if Docker Hub. - referenceString.append(repository); - } - - // Use tag if not the default tag. - if (!DEFAULT_TAG.equals(tag)) { - referenceString.append(':').append(tag); - } - - return referenceString.toString(); - } - - /** @return the image reference in Docker-readable format, without hiding the tag. */ - public String toStringWithTag() { - return this + (usesDefaultTag() ? ":" + DEFAULT_TAG : ""); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java new file mode 100644 index 0000000000..4d558c4d10 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java @@ -0,0 +1,181 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.tar.TarStreamBuilder; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; + +/** Translates an {@link Image} to a tarball that can be loaded into Docker. */ +public class ImageTarball { + + /** File name for the container configuration in the tarball. */ + private static final String CONTAINER_CONFIGURATION_JSON_FILE_NAME = "config.json"; + + /** File name for the manifest in the tarball. */ + private static final String MANIFEST_JSON_FILE_NAME = "manifest.json"; + + /** File name extension for the layer content files. */ + private static final String LAYER_FILE_EXTENSION = ".tar.gz"; + + /** Time that entry is set in the tar. */ + private static final Instant TAR_ENTRY_MODIFICATION_TIME = Instant.EPOCH; + + private static final String BLOB_PREFIX = "blobs/sha256/"; + + private final Image image; + private final ImageReference imageReference; + private final ImmutableSet allTargetImageTags; + + /** + * Instantiate with an {@link Image}. + * + * @param image the image to convert into a tarball + * @param imageReference image reference to set in the manifest (note that the tag portion of the + * image reference is ignored) + * @param allTargetImageTags the tags to tag the image with + */ + public ImageTarball( + Image image, ImageReference imageReference, ImmutableSet allTargetImageTags) { + this.image = image; + this.imageReference = imageReference; + this.allTargetImageTags = allTargetImageTags; + } + + /** + * Writes image tar bar in configured {@link Image#getImageFormat()} of OCI or Docker to output + * stream. + * + * @param out the target output stream + * @throws IOException if an error occurs writing out the image to stream + */ + public void writeTo(OutputStream out) throws IOException { + if (image.getImageFormat() == OciManifestTemplate.class) { + ociWriteTo(out); + } else { + dockerWriteTo(out); + } + } + + private void ociWriteTo(OutputStream out) throws IOException { + TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); + OciManifestTemplate manifest = new OciManifestTemplate(); + + // Adds all the layers to the tarball and manifest + for (Layer layer : image.getLayers()) { + DescriptorDigest digest = layer.getBlobDescriptor().getDigest(); + long size = layer.getBlobDescriptor().getSize(); + + tarStreamBuilder.addBlobEntry( + layer.getBlob(), size, BLOB_PREFIX + digest.getHash(), TAR_ENTRY_MODIFICATION_TIME); + manifest.addLayer(size, digest); + } + + // Adds the container configuration to the tarball and manifest + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(image).getContainerConfiguration(); + BlobDescriptor configDescriptor = Digests.computeDigest(containerConfiguration); + manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(containerConfiguration), + BLOB_PREFIX + configDescriptor.getDigest().getHash(), + TAR_ENTRY_MODIFICATION_TIME); + + // Adds the manifest to the tarball + BlobDescriptor manifestDescriptor = Digests.computeDigest(manifest); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(manifest), + BLOB_PREFIX + manifestDescriptor.getDigest().getHash(), + TAR_ENTRY_MODIFICATION_TIME); + + // Adds the oci-layout and index.json + tarStreamBuilder.addByteEntry( + "{\"imageLayoutVersion\": \"1.0.0\"}".getBytes(StandardCharsets.UTF_8), + "oci-layout", + TAR_ENTRY_MODIFICATION_TIME); + OciIndexTemplate index = new OciIndexTemplate(); + // TODO: figure out how to tag with allTargetImageTags + index.addManifest(manifestDescriptor, imageReference.toStringWithQualifier()); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(index), "index.json", TAR_ENTRY_MODIFICATION_TIME); + + tarStreamBuilder.writeAsTarArchiveTo(out); + } + + private void dockerWriteTo(OutputStream out) throws IOException { + TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); + DockerManifestEntryTemplate manifestTemplate = new DockerManifestEntryTemplate(); + + // Adds all the layers to the tarball and manifest. + for (Layer layer : image.getLayers()) { + String layerName = layer.getBlobDescriptor().getDigest().getHash() + LAYER_FILE_EXTENSION; + + tarStreamBuilder.addBlobEntry( + layer.getBlob(), + layer.getBlobDescriptor().getSize(), + layerName, + TAR_ENTRY_MODIFICATION_TIME); + manifestTemplate.addLayerFile(layerName); + } + + // Adds the container configuration to the tarball. + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(image).getContainerConfiguration(); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(containerConfiguration), + CONTAINER_CONFIGURATION_JSON_FILE_NAME, + TAR_ENTRY_MODIFICATION_TIME); + + // Adds the manifest to tarball. + for (String tag : allTargetImageTags) { + manifestTemplate.addRepoTag(imageReference.withQualifier(tag).toStringWithQualifier()); + } + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(Collections.singletonList(manifestTemplate)), + MANIFEST_JSON_FILE_NAME, + TAR_ENTRY_MODIFICATION_TIME); + + tarStreamBuilder.writeAsTarArchiveTo(out); + } + + /** + * Returns the total size of the image's layers in bytes. + * + * @return the total size of the image's layers in bytes + */ + public long getTotalLayerSize() { + long size = 0; + for (Layer layer : image.getLayers()) { + size += layer.getBlobDescriptor().getSize(); + } + return size; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java index af4ae5d15c..41b7ce3894 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; @@ -51,18 +52,25 @@ public interface Layer { /** + * Returns this layer's contents. + * * @return the layer's content BLOB * @throws LayerPropertyNotFoundException if not available */ Blob getBlob() throws LayerPropertyNotFoundException; + // TODO: Remove this /** + * Returns this layer's content descriptor. + * * @return the layer's content {@link BlobDescriptor} * @throws LayerPropertyNotFoundException if not available */ BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException; /** + * Returns this layer's diff ID. + * * @return the layer's diff ID * @throws LayerPropertyNotFoundException if not available */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java index 33772fd7d1..a18b032f02 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java deleted file mode 100644 index 1c159b8286..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; - -/** - * Represents an entry in the layer. A layer consists of many entries that can be converted into tar - * archive entries. - */ -public class LayerEntry { - - /** - * The source files to build from. Source files that are directories will have all subfiles in the - * directory added (but not the directory itself). - * - *

    The source files are specified as a list instead of a set to define the order in which they - * are added. - */ - private final ImmutableList sourceFiles; - - /** The Unix-style path to add the source files to in the container image filesystem. */ - private final String extractionPath; - - public LayerEntry(ImmutableList sourceFiles, String extractionPath) { - this.sourceFiles = sourceFiles; - this.extractionPath = extractionPath; - } - - public ImmutableList getSourceFiles() { - return sourceFiles; - } - - public String getExtractionPath() { - return extractionPath; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java index a438eab78c..3424bbd741 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java index 410c8bb643..27d599e8f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java index 699933a93c..db8aef33d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 34de3fefca..ea7ea5a943 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,118 +16,181 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.tar.TarStreamBuilder; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; +import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** - * Builds a reproducible {@link UnwrittenLayer} from files. The reproducibility is implemented by - * strips out all non-reproducible elements (modification time, group ID, user ID, user name, and - * group name) from name-sorted tar archive entries. + * Builds a reproducible layer {@link Blob} from files. The reproducibility is implemented by strips + * out all non-reproducible elements (modification time, group ID, user ID, user name, and group + * name) from name-sorted tar archive entries. */ public class ReproducibleLayerBuilder { /** - * Builds the {@link TarArchiveEntry}s for adding this {@link LayerEntry} to a tarball archive. - * - * @return the list of {@link TarArchiveEntry} - * @throws IOException if walking a source file that is a directory failed + * Holds a list of {@link TarArchiveEntry}s with unique extraction paths. The list also includes + * all parent directories for each extraction path. */ - private static List buildAsTarArchiveEntries(LayerEntry layerEntry) - throws IOException { - List tarArchiveEntries = new ArrayList<>(); - - for (Path sourceFile : layerEntry.getSourceFiles()) { - if (Files.isDirectory(sourceFile)) { - new DirectoryWalker(sourceFile) - .filterRoot() - .walk( - path -> { - /* - * Builds the same file path as in the source file for extraction. The iteration - * is necessary because the path needs to be in Unix-style. - */ - StringBuilder subExtractionPath = - new StringBuilder(layerEntry.getExtractionPath()); - Path sourceFileRelativePath = sourceFile.getParent().relativize(path); - for (Path sourceFileRelativePathComponent : sourceFileRelativePath) { - subExtractionPath.append('/').append(sourceFileRelativePathComponent); - } - tarArchiveEntries.add( - new TarArchiveEntry(path.toFile(), subExtractionPath.toString())); - }); - - } else { - TarArchiveEntry tarArchiveEntry = - new TarArchiveEntry( - sourceFile.toFile(), - layerEntry.getExtractionPath() + "/" + sourceFile.getFileName()); - tarArchiveEntries.add(tarArchiveEntry); + private static class UniqueTarArchiveEntries { + + /** + * Uses the current directory to act as the file input to TarArchiveEntry (since all directories + * are treated the same in {@link TarArchiveEntry#TarArchiveEntry(File, String)}, except for + * modification time, UID, GID, etc., which are wiped away in {@link #build}). + */ + private static final Path DIRECTORY_FILE = Paths.get("."); + + private final List entries = new ArrayList<>(); + private final Set names = new HashSet<>(); + + /** + * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of + * the parent directories on the extraction path, if the parent does not exist. Parent will have + * modification time set to {@link FileEntriesLayer#DEFAULT_MODIFICATION_TIME}. + * + * @param tarArchiveEntry the {@link TarArchiveEntry} + * @throws IOException if an I/O error occurs + */ + private void add(TarArchiveEntry tarArchiveEntry) throws IOException { + if (names.contains(tarArchiveEntry.getName())) { + return; + } + + // Adds all directories along extraction paths to explicitly set permissions for those + // directories. + Path namePath = Paths.get(tarArchiveEntry.getName()); + if (namePath.getParent() != namePath.getRoot()) { + Path tarArchiveParentDir = Verify.verifyNotNull(namePath.getParent()); + TarArchiveEntry dir = new TarArchiveEntry(DIRECTORY_FILE, tarArchiveParentDir.toString()); + dir.setUserId(0); + dir.setGroupId(0); + dir.setUserName(""); + dir.setGroupName(""); + clearTimeHeaders(dir, FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + add(dir); } + + entries.add(tarArchiveEntry); + names.add(tarArchiveEntry.getName()); } - return tarArchiveEntries; + private List getSortedEntries() { + List sortedEntries = new ArrayList<>(entries); + sortedEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + return sortedEntries; + } } - private final ImmutableList.Builder layerEntries = ImmutableList.builder(); + private static void clearTimeHeaders(TarArchiveEntry entry, Instant modTime) { + entry.setModTime(modTime.toEpochMilli()); - public ReproducibleLayerBuilder() {} + String headerTime = Long.toString(modTime.getEpochSecond()); + final long nanos = modTime.getNano(); + if (nanos > 0) { + headerTime += "." + nanos; + } + entry.addPaxHeader("mtime", headerTime); + entry.addPaxHeader("atime", headerTime); + entry.addPaxHeader("ctime", headerTime); + entry.addPaxHeader("LIBARCHIVE.creationtime", headerTime); + } - /** - * Adds the {@code sourceFiles} to be extracted on the image at {@code extractionPath}. The order - * in which files are added matters. - * - * @param sourceFiles the source files to build from - * @param extractionPath the Unix-style path to add the source files to in the container image - * filesystem - * @return this - */ - public ReproducibleLayerBuilder addFiles(List sourceFiles, String extractionPath) { - this.layerEntries.add(new LayerEntry(ImmutableList.copyOf(sourceFiles), extractionPath)); - return this; + private static void setUserAndGroup(TarArchiveEntry entry, FileEntry layerEntry) { + entry.setUserId(0); + entry.setGroupId(0); + entry.setUserName(""); + entry.setGroupName(""); + + if (!layerEntry.getOwnership().isEmpty()) { + // Parse ":" string. + String user = layerEntry.getOwnership(); + String group = ""; + int colonIndex = user.indexOf(':'); + if (colonIndex != -1) { + group = user.substring(colonIndex + 1); + user = user.substring(0, colonIndex); + } + + if (!user.isEmpty()) { + // Check if it's a number, and set either UID or user name. + try { + entry.setUserId(Long.parseLong(user)); + } catch (NumberFormatException ignored) { + entry.setUserName(user); + } + } + if (!group.isEmpty()) { + // Check if it's a number, and set either GID or group name. + try { + entry.setGroupId(Long.parseLong(group)); + } catch (NumberFormatException ignored) { + entry.setGroupName(group); + } + } + } + } + + private final ImmutableList layerEntries; + + public ReproducibleLayerBuilder(ImmutableList layerEntries) { + this.layerEntries = layerEntries; } /** - * Builds and returns the layer. + * Builds and returns the layer {@link Blob}. * * @return the new layer - * @throws IOException if walking the source files fails + * @throws IOException if an I/O error occurs */ - public UnwrittenLayer build() throws IOException { - List filesystemEntries = new ArrayList<>(); + public Blob build() throws IOException { + UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. - for (LayerEntry layerEntry : layerEntries.build()) { - filesystemEntries.addAll(buildAsTarArchiveEntries(layerEntry)); + for (FileEntry layerEntry : layerEntries) { + // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and + // adds parent directories for each extraction path. + TarArchiveEntry entry = + new TarArchiveEntry( + layerEntry.getSourceFile(), layerEntry.getExtractionPath().toString()); + + // Sets the entry's permissions by masking out the permission bits from the entry's mode (the + // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. + entry.setMode((entry.getMode() & ~0777) | layerEntry.getPermissions().getPermissionBits()); + setUserAndGroup(entry, layerEntry); + clearTimeHeaders(entry, layerEntry.getModificationTime()); + + uniqueTarArchiveEntries.add(entry); } - // Sorts the entries by name. - filesystemEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + // Gets the entries sorted by extraction path. + List sortedFilesystemEntries = uniqueTarArchiveEntries.getSortedEntries(); + + Set names = new HashSet<>(); // Adds all the files to a tar stream. TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - for (TarArchiveEntry entry : filesystemEntries) { - // Strips out all non-reproducible elements from tar archive entries. - entry.setModTime(0); - entry.setGroupId(0); - entry.setUserId(0); - entry.setUserName(""); - entry.setGroupName(""); - - tarStreamBuilder.addEntry(entry); - } + for (TarArchiveEntry entry : sortedFilesystemEntries) { + Verify.verify(!names.contains(entry.getName())); + names.add(entry.getName()); - return new UnwrittenLayer(tarStreamBuilder.toBlob()); - } + tarStreamBuilder.addTarArchiveEntry(entry); + } - public ImmutableList getLayerEntries() { - return layerEntries.build(); + return Blobs.from(tarStreamBuilder::writeAsTarArchiveTo, false); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java deleted file mode 100644 index 40d46ff9fd..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; - -/** - * A layer that has not been written out and only has the unwritten content {@link Blob}. Once - * written, this layer becomes a {@link CachedLayer}. - */ -public class UnwrittenLayer implements Layer { - - private final Blob uncompressedBlob; - - /** - * Initializes with the uncompressed {@link Blob} of the layer content. - * - * @param uncompressedBlob the uncompressed {@link Blob} of the layer content - */ - public UnwrittenLayer(Blob uncompressedBlob) { - this.uncompressedBlob = uncompressedBlob; - } - - /** Gets the uncompressed layer content BLOB. */ - @Override - public Blob getBlob() { - return uncompressedBlob; - } - - @Override - public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException { - throw new LayerPropertyNotFoundException("Blob descriptor not available for unwritten layer"); - } - - @Override - public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { - throw new LayerPropertyNotFoundException("Diff ID not available for unwritten layer"); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java index 2b7c4df956..0497db95f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -23,4 +23,8 @@ public class BadContainerConfigurationFormatException extends Exception { BadContainerConfigurationFormatException(String message) { super(message); } + + BadContainerConfigurationFormatException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index bc6d2a4441..c61ff881df 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,17 +16,20 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; /** * Parent class for image manifest JSON templates that can be built. * - * @see V22ManifestTemplate for Docker V2.2 format - * @see OCIManifestTemplate for OCI format + * @see V22ManifestTemplate Docker V2.2 format + * @see OciManifestTemplate OCI format */ public interface BuildableManifestTemplate extends ManifestTemplate { @@ -37,12 +40,16 @@ public interface BuildableManifestTemplate extends ManifestTemplate { * @see OCI * Content Descriptors */ - @VisibleForTesting class ContentDescriptorTemplate implements JsonTemplate { - @Nullable private String mediaType; + @SuppressWarnings("unused") + @Nullable + private String mediaType; + @Nullable private DescriptorDigest digest; private long size; + @Nullable private List urls; + @Nullable private Map annotations; ContentDescriptorTemplate(String mediaType, long size, DescriptorDigest digest) { this.mediaType = mediaType; @@ -51,9 +58,9 @@ class ContentDescriptorTemplate implements JsonTemplate { } /** Necessary for Jackson to create from JSON. */ - private ContentDescriptorTemplate() {} + @SuppressWarnings("unused") + protected ContentDescriptorTemplate() {} - @VisibleForTesting public long getSize() { return size; } @@ -62,7 +69,6 @@ void setSize(long size) { this.size = size; } - @VisibleForTesting @Nullable public DescriptorDigest getDigest() { return digest; @@ -71,16 +77,49 @@ public DescriptorDigest getDigest() { void setDigest(DescriptorDigest digest) { this.digest = digest; } + + @VisibleForTesting + @Nullable + public List getUrls() { + return urls; + } + + void setUrls(List urls) { + this.urls = ImmutableList.copyOf(urls); + } + + @VisibleForTesting + @Nullable + public Map getAnnotations() { + return annotations; + } + + void setAnnotations(Map annotations) { + this.annotations = ImmutableMap.copyOf(annotations); + } } - /** @return the media type for this manifest, specific to the image format */ + /** + * Returns the media type for this manifest, specific to the image format. + * + * @return the media type for this manifest, specific to the image format + */ + @Override String getManifestMediaType(); - /** @return the content descriptor of the container configuration */ + /** + * Returns the content descriptor of the container configuration. + * + * @return the content descriptor of the container configuration + */ @Nullable ContentDescriptorTemplate getContainerConfiguration(); - /** @return an unmodifiable view of the layers */ + /** + * Returns an unmodifiable view of the layers. + * + * @return an unmodifiable view of the layers + */ List getLayers(); /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 32c612b2c2..45f6e221fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,9 +17,9 @@ package com.google.cloud.tools.jib.image.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,9 +38,32 @@ * "config": { * "Env": ["/usr/bin/java"], * "Entrypoint": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], - * "Cmd": ["arg1", "arg2"] - * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} } + * "Cmd": ["arg1", "arg2"], + * "Healthcheck": { + * "Test": ["CMD-SHELL", "/usr/bin/check-health localhost"], + * "Interval": 30000000000, + * "Timeout": 10000000000, + * "StartPeriod": 0, + * "Retries": 3 + * } + * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} }, + * "Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}}, + * "Labels": { "com.example.label": "value" }, + * "WorkingDir": "/home/user/workspace", + * "User": "me" * }, + * "history": [ + * { + * "author": "Jib", + * "created": "1970-01-01T00:00:00Z", + * "created_by": "jib" + * }, + * { + * "author": "Jib", + * "created": "1970-01-01T00:00:00Z", + * "created_by": "jib" + * } + * ] * "rootfs": { * "diff_ids": [ * "sha256:2aebd096e0e237b447781353379722157e6c2d434b9ec5a0d63f2a6f07cf90c2", @@ -57,14 +80,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class ContainerConfigurationTemplate implements JsonTemplate { - /** - * A combined date and time at which the image was created. Constant to maintain reproducibility - * and avoid Docker's weird "292 years old" bug. - * - * @see https://github.com/GoogleContainerTools/jib/issues/341 - */ - private String created = "1970-01-01T00:00:00Z"; + /** ISO-8601 formatted combined date and time at which the image was created. */ + @Nullable private String created; /** The CPU architecture to run the binaries in this container. */ private String architecture = "amd64"; @@ -75,6 +92,9 @@ public class ContainerConfigurationTemplate implements JsonTemplate { /** Execution parameters that should be used as a base when running the container. */ private final ConfigurationObjectTemplate config = new ConfigurationObjectTemplate(); + /** Describes the history of each layer. */ + private final List history = new ArrayList<>(); + /** Layer content digests that are used to build the container filesystem. */ private final RootFilesystemObjectTemplate rootfs = new RootFilesystemObjectTemplate(); @@ -91,8 +111,44 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { /** Arguments to pass into main. */ @Nullable private List Cmd; + /** Healthcheck. */ + @Nullable private HealthCheckObjectTemplate Healthcheck; + /** Network ports the container exposes. */ - @Nullable private Map> ExposedPorts; + @Nullable private Map> ExposedPorts; + + /** Labels. */ + @Nullable private Map Labels; + + /** Working directory. */ + @Nullable private String WorkingDir; + + /** User. */ + @Nullable private String User; + + /** Volumes. */ + @Nullable private Map> Volumes; + } + + /** Template for inner JSON object representing the healthcheck configuration. */ + private static class HealthCheckObjectTemplate implements JsonTemplate { + + /** The test to perform to check that the container is healthy. */ + @Nullable private List Test; + + /** Number of nanoseconds to wait between probe attempts. */ + @Nullable private Long Interval; + + /** Number of nanoseconds to wait before considering the check to have hung. */ + @Nullable private Long Timeout; + + /** + * Number of nanoseconds to wait for the container to initialize before starting health-retries. + */ + @Nullable private Long StartPeriod; + + /** The number of consecutive failures needed to consider the container as unhealthy. */ + @Nullable private Integer Retries; } /** @@ -102,6 +158,7 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { private static class RootFilesystemObjectTemplate implements JsonTemplate { /** The type must always be {@code "layers"}. */ + @SuppressWarnings("unused") private final String type = "layers"; /** @@ -111,30 +168,167 @@ private static class RootFilesystemObjectTemplate implements JsonTemplate { private final List diff_ids = new ArrayList<>(); } - public void setContainerEnvironment(List environment) { + public void setCreated(@Nullable String created) { + this.created = created; + } + + /** + * Sets the architecture for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @param architecture value for the {@code architecture} field + */ + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + /** + * Sets the operating system for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @param os value for the {@code os} field + */ + public void setOs(String os) { + this.os = os; + } + + public void setContainerEnvironment(@Nullable List environment) { config.Env = environment; } - public void setContainerEntrypoint(List command) { + public void setContainerEntrypoint(@Nullable List command) { config.Entrypoint = command; } - public void setContainerCmd(List cmd) { + public void setContainerCmd(@Nullable List cmd) { config.Cmd = cmd; } - public void setContainerExposedPorts(Map> exposedPorts) { + /** + * Sets test on HealthCheck, creates an empty HealthCheck object if necessary. + * + * @param test the list of tests to set + */ + public void setContainerHealthCheckTest(List test) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Test = test; + } + + /** + * Sets interval on HealthCheck, creates an empty HealthCheck object if necessary. + * + * @param interval the interval to set + */ + public void setContainerHealthCheckInterval(@Nullable Long interval) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Interval = interval; + } + + /** + * Sets timeout on HealthCheck, creates an empty HealthCheck object if necessary. + * + * @param timeout the timeout to configure + */ + public void setContainerHealthCheckTimeout(@Nullable Long timeout) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Timeout = timeout; + } + + /** + * Sets startPeriod on HealthCheck, creates an empty HealthCheck object if necessary. + * + * @param startPeriod the start period to configure + */ + public void setContainerHealthCheckStartPeriod(@Nullable Long startPeriod) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).StartPeriod = startPeriod; + } + + /** + * Sets retries on HealthCheck, creates an empty HealthCheck object if necessary. + * + * @param retries the number of retries to configure + */ + public void setContainerHealthCheckRetries(@Nullable Integer retries) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Retries = retries; + } + + public void setContainerExposedPorts(@Nullable Map> exposedPorts) { config.ExposedPorts = exposedPorts; } + public void setContainerLabels(@Nullable Map labels) { + config.Labels = labels; + } + + public void setContainerWorkingDir(@Nullable String workingDirectory) { + config.WorkingDir = workingDirectory; + } + + public void setContainerUser(@Nullable String user) { + config.User = user; + } + + public void setContainerVolumes(@Nullable Map> volumes) { + config.Volumes = volumes; + } + public void addLayerDiffId(DescriptorDigest diffId) { rootfs.diff_ids.add(diffId); } + public void addHistoryEntry(HistoryEntry historyEntry) { + history.add(historyEntry); + } + List getDiffIds() { return rootfs.diff_ids; } + List getHistory() { + return history; + } + + @Nullable + String getCreated() { + return created; + } + + /** + * Returns the architecture for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @return the {@code architecture} field + */ + public String getArchitecture() { + return architecture; + } + + /** + * Returns the operating system for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @return the {@code os} field + */ + public String getOs() { + return os; + } + @Nullable List getContainerEnvironment() { return config.Env; @@ -151,12 +345,60 @@ List getContainerCmd() { } @Nullable - Map> getContainerExposedPorts() { + List getContainerHealthTest() { + return config.Healthcheck == null ? null : config.Healthcheck.Test; + } + + @Nullable + Long getContainerHealthInterval() { + return config.Healthcheck == null ? null : config.Healthcheck.Interval; + } + + @Nullable + Long getContainerHealthTimeout() { + return config.Healthcheck == null ? null : config.Healthcheck.Timeout; + } + + @Nullable + Long getContainerHealthStartPeriod() { + return config.Healthcheck == null ? null : config.Healthcheck.StartPeriod; + } + + @Nullable + Integer getContainerHealthRetries() { + return config.Healthcheck == null ? null : config.Healthcheck.Retries; + } + + @Nullable + Map> getContainerExposedPorts() { return config.ExposedPorts; } - @VisibleForTesting - DescriptorDigest getLayerDiffId(int index) { + @Nullable + Map getContainerLabels() { + return config.Labels; + } + + @Nullable + String getContainerWorkingDir() { + return config.WorkingDir; + } + + @Nullable + String getContainerUser() { + return config.User; + } + + @Nullable + Map> getContainerVolumes() { + return config.Volumes; + } + + public DescriptorDigest getLayerDiffId(int index) { return rootfs.diff_ids.get(index); } + + public int getLayerCount() { + return rootfs.diff_ids.size(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java index 2cafda59da..bca89c2a33 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; import java.security.DigestException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java index 52c5746554..8308159123 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; /** Serializes a {@link DescriptorDigest} into JSON element. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java new file mode 100644 index 0000000000..959f39f57a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java @@ -0,0 +1,173 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.json.JsonTemplate; +import java.time.Instant; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Represents an item in the container configuration's {@code history} list. + * + * @see OCI + * image spec ({@code history} field) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HistoryEntry implements JsonTemplate { + + public static class Builder { + + @Nullable private Instant creationTimestamp; + @Nullable private String author; + @Nullable private String createdBy; + @Nullable private String comment; + @Nullable private Boolean emptyLayer; + + public Builder setCreationTimestamp(Instant creationTimestamp) { + this.creationTimestamp = creationTimestamp; + return this; + } + + public Builder setAuthor(String author) { + this.author = author; + return this; + } + + public Builder setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder setComment(String comment) { + this.comment = comment; + return this; + } + + public Builder setEmptyLayer(Boolean emptyLayer) { + this.emptyLayer = emptyLayer; + return this; + } + + /** + * Create a new history entry. + * + * @return an new {@link HistoryEntry} instance + */ + public HistoryEntry build() { + return new HistoryEntry( + creationTimestamp == null ? null : creationTimestamp.toString(), + author, + createdBy, + comment, + emptyLayer); + } + + private Builder() {} + } + + /** + * Creates a builder for a {@link HistoryEntry}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** The ISO-8601 formatted timestamp at which the image was created. */ + @JsonProperty("created") + @Nullable + private String creationTimestamp; + + /** The name of the author specified when committing the image. */ + @JsonProperty("author") + @Nullable + private String author; + + /** The command used to build the layer. */ + @JsonProperty("created_by") + @Nullable + private String createdBy; + + /** A custom message set when creating the layer. */ + @JsonProperty("comment") + @Nullable + private String comment; + + /** + * Whether or not the entry corresponds to a layer in the container ({@code @Nullable Boolean} to + * make field optional). + */ + @JsonProperty("empty_layer") + @Nullable + private Boolean emptyLayer; + + public HistoryEntry() {} + + private HistoryEntry( + @Nullable String creationTimestamp, + @Nullable String author, + @Nullable String createdBy, + @Nullable String comment, + @Nullable Boolean emptyLayer) { + this.author = author; + this.creationTimestamp = creationTimestamp; + this.createdBy = createdBy; + this.comment = comment; + this.emptyLayer = emptyLayer; + } + + /** + * Returns whether or not the history object corresponds to a layer in the container. + * + * @return {@code true} if the history object corresponds to a layer in the container + */ + @JsonIgnore + public boolean hasCorrespondingLayer() { + return emptyLayer == null ? false : emptyLayer; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof HistoryEntry) { + HistoryEntry otherHistory = (HistoryEntry) other; + return Objects.equals(otherHistory.creationTimestamp, creationTimestamp) + && Objects.equals(otherHistory.author, author) + && Objects.equals(otherHistory.createdBy, createdBy) + && Objects.equals(otherHistory.comment, comment) + && Objects.equals(otherHistory.emptyLayer, emptyLayer); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(author, creationTimestamp, createdBy, comment, emptyLayer); + } + + @Override + public String toString() { + return createdBy == null ? "" : createdBy; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageMetadataTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageMetadataTemplate.java new file mode 100644 index 0000000000..64ac7cfec5 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageMetadataTemplate.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.cloud.tools.jib.json.JsonTemplate; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +/** A bundle of an image manifest list, manifests, and container configurations. */ +public class ImageMetadataTemplate implements JsonTemplate { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "@class") + @JsonSubTypes({ + @JsonSubTypes.Type(value = OciIndexTemplate.class), + @JsonSubTypes.Type(value = V22ManifestListTemplate.class), + }) + @Nullable + private ManifestTemplate manifestList; + + private List manifestsAndConfigs = new ArrayList<>(); + + @SuppressWarnings("unused") + private ImageMetadataTemplate() {} + + public ImageMetadataTemplate( + @Nullable ManifestTemplate manifestList, + List manifestsAndConfigs) { + this.manifestList = manifestList; + this.manifestsAndConfigs = manifestsAndConfigs; + } + + @Nullable + public ManifestTemplate getManifestList() { + return manifestList; + } + + public List getManifestsAndConfigs() { + return manifestsAndConfigs; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index eb79141f62..581cb17031 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,97 +16,171 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import java.lang.reflect.InvocationTargetException; import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import javax.annotation.Nullable; -/** - * Translates an {@link Image} into a manifest or container configuration JSON BLOB. - * - *

    Example usage: - * - *

    {@code
    - * ImageToJsonTranslator translator = new ImageToJsonTranslator(image);
    - * Blob containerConfigurationBlob = translator.getContainerConfigurationBlob();
    - * BlobDescriptor containerConfigurationBlobDescriptor = blob.writeTo(outputStream);
    - * Blob manifestBlob = translator.getManifestBlob(containerConfigurationBlobDescriptor);
    - * }
    - */ +/** Translates an {@link Image} into a manifest or container configuration JSON. */ public class ImageToJsonTranslator { /** - * Converts a list of {@link Port}s to the corresponding container config format for exposed ports + * Converts a set of {@link Port}s to the corresponding container config format for exposed ports * (e.g. {@code Port(1000, Protocol.TCP)} -> {@code {"1000/tcp":{}}}). * - * @param exposedPorts the list of {@link Port}s to translate + * @param exposedPorts the set of {@link Port}s to translate, or {@code null} * @return a sorted map with the string representation of the ports as keys and empty maps as - * values + * values, or {@code null} if {@code exposedPorts} is {@code null} */ @VisibleForTesting - static Map> portListToMap(List exposedPorts) { - ImmutableSortedMap.Builder> result = - new ImmutableSortedMap.Builder<>(String::compareTo); - for (Port port : exposedPorts) { - result.put(port.getPort() + "/" + port.getProtocol(), Collections.emptyMap()); + @Nullable + static Map> portSetToMap(@Nullable Set exposedPorts) { + return setToMap(exposedPorts, port -> port.getPort() + "/" + port.getProtocol()); + } + + /** + * Converts a set of {@link AbsoluteUnixPath}s to the corresponding container config format for + * volumes (e.g. {@code AbsoluteUnixPath().get("/var/log/my-app-logs")} -> {@code + * {"/var/log/my-app-logs":{}}}). + * + * @param volumes the set of {@link AbsoluteUnixPath}s to translate, or {@code null} + * @return a sorted map with the string representation of the ports as keys and empty maps as + * values, or {@code null} if {@code exposedPorts} is {@code null} + */ + @VisibleForTesting + @Nullable + static Map> volumesSetToMap(@Nullable Set volumes) { + return setToMap(volumes, AbsoluteUnixPath::toString); + } + + /** + * Converts the map of environment variables to a list with items in the format "NAME=VALUE". + * + * @return the list + */ + @VisibleForTesting + @Nullable + static ImmutableList environmentMapToList(@Nullable Map environment) { + if (environment == null) { + return null; + } + Preconditions.checkArgument( + environment.keySet().stream().noneMatch(key -> key.contains("=")), + "Illegal environment variable: name cannot contain '='"); + return environment.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(ImmutableList.toImmutableList()); + } + + /** + * Turns a set into a sorted map where each element of the set is mapped to an entry composed by + * the key generated with {@code Function elementMapper} and an empty map as value. + * + *

    This method is needed because the volume object is a direct JSON serialization of the Go + * type map[string]struct{} and is represented in JSON as an object mapping its keys to an empty + * object. + * + *

    Further read at the image specs. + * + * @param set the set of elements to be transformed + * @param keyMapper the mapper function to generate keys to the map + * @param the type of the elements from the set + * @return an map + */ + @Nullable + private static Map> setToMap( + @Nullable Set set, Function keyMapper) { + if (set == null) { + return null; } - return result.build(); + + return set.stream() + .collect( + ImmutableSortedMap.toImmutableSortedMap( + String::compareTo, keyMapper, ignored -> Collections.emptyMap())); } - private final Image image; + private final Image image; /** * Instantiate with an {@link Image}. * - * @param image the image to translate. + * @param image the image to translate */ - public ImageToJsonTranslator(Image image) { + public ImageToJsonTranslator(Image image) { this.image = image; } /** - * Gets the container configuration as a {@link Blob}. + * Gets the container configuration. * - * @return the container configuration {@link Blob}. + * @return the container configuration */ - public Blob getContainerConfigurationBlob() { + public JsonTemplate getContainerConfiguration() { // Set up the JSON template. ContainerConfigurationTemplate template = new ContainerConfigurationTemplate(); // Adds the layer diff IDs. - for (CachedLayer layer : image.getLayers()) { + for (Layer layer : image.getLayers()) { template.addLayerDiffId(layer.getDiffId()); } - // Adds the environment variables. - template.setContainerEnvironment(image.getEnvironment()); + // Adds the history. + for (HistoryEntry historyObject : image.getHistory()) { + template.addHistoryEntry(historyObject); + } - // Sets the entrypoint. + template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); + template.setArchitecture(image.getArchitecture()); + template.setOs(image.getOs()); + template.setContainerEnvironment(environmentMapToList(image.getEnvironment())); template.setContainerEntrypoint(image.getEntrypoint()); + template.setContainerCmd(image.getProgramArguments()); + template.setContainerExposedPorts(portSetToMap(image.getExposedPorts())); + template.setContainerVolumes(volumesSetToMap(image.getVolumes())); + template.setContainerLabels(image.getLabels()); + template.setContainerWorkingDir(image.getWorkingDirectory()); + template.setContainerUser(image.getUser()); + + // Ignore healthcheck if not Docker/command is empty + DockerHealthCheck healthCheck = image.getHealthCheck(); + if (image.getImageFormat() == V22ManifestTemplate.class && healthCheck != null) { + template.setContainerHealthCheckTest(healthCheck.getCommand()); + healthCheck + .getInterval() + .ifPresent(interval -> template.setContainerHealthCheckInterval(interval.toNanos())); + healthCheck + .getTimeout() + .ifPresent(timeout -> template.setContainerHealthCheckTimeout(timeout.toNanos())); + healthCheck + .getStartPeriod() + .ifPresent( + startPeriod -> template.setContainerHealthCheckStartPeriod(startPeriod.toNanos())); + template.setContainerHealthCheckRetries(healthCheck.getRetries().orElse(null)); + } - // Sets the main method arguments. - template.setContainerCmd(image.getJavaArguments()); - - // Sets the exposed ports. - template.setContainerExposedPorts(portListToMap(image.getExposedPorts())); - - // Serializes into JSON. - return JsonTemplateMapper.toBlob(template); + return template; } /** * Gets the manifest as a JSON template. The {@code containerConfigurationBlobDescriptor} must be - * the [@link BlobDescriptor} obtained by writing out the container configuration {@link Blob} - * returned from {@link #getContainerConfigurationBlob()}. + * the {@link BlobDescriptor} obtained by writing out the container configuration JSON returned + * from {@link #getContainerConfiguration()}. * * @param child type of {@link BuildableManifestTemplate}. * @param manifestTemplateClass the JSON template to translate the image to. @@ -126,12 +200,11 @@ public T getManifestTemplate( template.setContainerConfiguration(containerConfigurationSize, containerConfigurationDigest); // Adds the layers. - for (CachedLayer layer : image.getLayers()) { + for (Layer layer : image.getLayers()) { template.addLayer( layer.getBlobDescriptor().getSize(), layer.getBlobDescriptor().getDigest()); } - // Serializes into JSON. return template; } catch (InstantiationException diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index da12d73c21..a56c2ba865 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,22 +16,28 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.image.DigestOnlyLayer; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.ReferenceLayer; import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -45,7 +51,17 @@ public class JsonToImageTranslator { * *

    Example matches: 100, 1000/tcp, 2000/udp */ - private static final Pattern portPattern = Pattern.compile("(\\d+)(?:/(tcp|udp))?"); + private static final Pattern PORT_PATTERN = + Pattern.compile("(?\\d+)(?:/(?tcp|udp))?"); + + /** + * Pattern used for parsing environment variables in the format {@code NAME=VALUE}. {@code NAME} + * should not contain an '='. + * + *

    Example matches: NAME=VALUE, A12345=$$$$$ + */ + @VisibleForTesting + static final Pattern ENVIRONMENT_PATTERN = Pattern.compile("(?[^=]+)=(?.*)"); /** * Translates {@link V21ManifestTemplate} to {@link Image}. @@ -53,15 +69,23 @@ public class JsonToImageTranslator { * @param manifestTemplate the template containing the image layers. * @return the translated {@link Image}. * @throws LayerPropertyNotFoundException if adding image layers fails. + * @throws BadContainerConfigurationFormatException if the container configuration is in a bad + * format */ - public static Image toImage(V21ManifestTemplate manifestTemplate) - throws LayerPropertyNotFoundException { - Image.Builder imageBuilder = Image.builder(); + public static Image toImage(V21ManifestTemplate manifestTemplate) + throws LayerPropertyNotFoundException, BadContainerConfigurationFormatException { + Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); - for (DescriptorDigest digest : manifestTemplate.getLayerDigests()) { + // V21 layers are in reverse order of V22. (The first layer is the latest one.) + for (DescriptorDigest digest : Lists.reverse(manifestTemplate.getLayerDigests())) { imageBuilder.addLayer(new DigestOnlyLayer(digest)); } + Optional configuration = + manifestTemplate.getContainerConfiguration(); + if (configuration.isPresent()) { + configureBuilderWithContainerConfiguration(imageBuilder, configuration.get()); + } return imageBuilder.build(); } @@ -79,7 +103,7 @@ public static Image toImage(V21ManifestTemplate manifestTemplate) * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - public static Image toImage( + public static Image toImage( BuildableManifestTemplate manifestTemplate, ContainerConfigurationTemplate containerConfigurationTemplate) throws LayerCountMismatchException, LayerPropertyNotFoundException, @@ -98,13 +122,12 @@ public static Image toImage( } List diffIds = containerConfigurationTemplate.getDiffIds(); - if (layers.size() != diffIds.size()) { throw new LayerCountMismatchException( "Mismatch between image manifest and container configuration"); } - Image.Builder imageBuilder = Image.builder(); + Image.Builder imageBuilder = Image.builder(manifestTemplate.getClass()); for (int layerIndex = 0; layerIndex < layers.size(); layerIndex++) { ReferenceNoDiffIdLayer noDiffIdLayer = layers.get(layerIndex); @@ -113,56 +136,144 @@ public static Image toImage( imageBuilder.addLayer(new ReferenceLayer(noDiffIdLayer.getBlobDescriptor(), diffId)); } - if (containerConfigurationTemplate.getContainerEntrypoint() != null) { - imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); + configureBuilderWithContainerConfiguration(imageBuilder, containerConfigurationTemplate); + return imageBuilder.build(); + } + + private static void configureBuilderWithContainerConfiguration( + Image.Builder imageBuilder, ContainerConfigurationTemplate containerConfigurationTemplate) + throws BadContainerConfigurationFormatException { + + containerConfigurationTemplate.getHistory().forEach(imageBuilder::addHistory); + + if (containerConfigurationTemplate.getCreated() != null) { + try { + imageBuilder.setCreated(Instant.parse(containerConfigurationTemplate.getCreated())); + } catch (DateTimeParseException ex) { + try { + // TODO: remove when using Java >= 12. + // See https://github.com/GoogleContainerTools/jib/issues/2428 + imageBuilder.setCreated( + DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( + containerConfigurationTemplate.getCreated(), Instant::from)); + } catch (DateTimeParseException ignored) { + throw new BadContainerConfigurationFormatException( + "Invalid image creation time: " + containerConfigurationTemplate.getCreated(), ex); + } + } } - if (containerConfigurationTemplate.getContainerCmd() != null) { - imageBuilder.setJavaArguments(containerConfigurationTemplate.getContainerCmd()); + if (containerConfigurationTemplate.getArchitecture() != null) { + imageBuilder.setArchitecture(containerConfigurationTemplate.getArchitecture()); + } + if (containerConfigurationTemplate.getOs() != null) { + imageBuilder.setOs(containerConfigurationTemplate.getOs()); + } + + imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); + imageBuilder.setProgramArguments(containerConfigurationTemplate.getContainerCmd()); + + List baseHealthCheckCommand = containerConfigurationTemplate.getContainerHealthTest(); + if (baseHealthCheckCommand != null) { + DockerHealthCheck.Builder builder = DockerHealthCheck.fromCommand(baseHealthCheckCommand); + if (containerConfigurationTemplate.getContainerHealthInterval() != null) { + builder.setInterval( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthInterval())); + } + if (containerConfigurationTemplate.getContainerHealthTimeout() != null) { + builder.setTimeout( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthTimeout())); + } + if (containerConfigurationTemplate.getContainerHealthStartPeriod() != null) { + builder.setStartPeriod( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthStartPeriod())); + } + if (containerConfigurationTemplate.getContainerHealthRetries() != null) { + builder.setRetries(containerConfigurationTemplate.getContainerHealthRetries()); + } + imageBuilder.setHealthCheck(builder.build()); } if (containerConfigurationTemplate.getContainerExposedPorts() != null) { - imageBuilder.setExposedPorts( - portMapToList(containerConfigurationTemplate.getContainerExposedPorts())); + imageBuilder.addExposedPorts( + portMapToSet(containerConfigurationTemplate.getContainerExposedPorts())); + } + + if (containerConfigurationTemplate.getContainerVolumes() != null) { + imageBuilder.addVolumes(volumeMapToSet(containerConfigurationTemplate.getContainerVolumes())); } if (containerConfigurationTemplate.getContainerEnvironment() != null) { for (String environmentVariable : containerConfigurationTemplate.getContainerEnvironment()) { - imageBuilder.addEnvironmentVariableDefinition(environmentVariable); + Matcher matcher = ENVIRONMENT_PATTERN.matcher(environmentVariable); + if (!matcher.matches()) { + throw new BadContainerConfigurationFormatException( + "Invalid environment variable definition: " + environmentVariable); + } + imageBuilder.addEnvironmentVariable(matcher.group("name"), matcher.group("value")); } } - return imageBuilder.build(); + imageBuilder.addLabels(containerConfigurationTemplate.getContainerLabels()); + imageBuilder.setWorkingDirectory(containerConfigurationTemplate.getContainerWorkingDir()); + imageBuilder.setUser(containerConfigurationTemplate.getContainerUser()); } /** - * Converts a map of exposed ports as strings to a list of {@link Port}s (e.g. {@code + * Converts a map of exposed ports as strings to a set of {@link Port}s (e.g. {@code * {"1000/tcp":{}}} -> {@code Port(1000, Protocol.TCP)}). * * @param portMap the map to convert - * @return a list of {@link Port}s + * @return a set of {@link Port}s */ @VisibleForTesting - static ImmutableList portMapToList(@Nullable Map> portMap) + static ImmutableSet portMapToSet(@Nullable Map> portMap) throws BadContainerConfigurationFormatException { if (portMap == null) { - return ImmutableList.of(); + return ImmutableSet.of(); } - ImmutableList.Builder ports = new ImmutableList.Builder<>(); - for (Map.Entry> entry : portMap.entrySet()) { + ImmutableSet.Builder ports = new ImmutableSet.Builder<>(); + for (Map.Entry> entry : portMap.entrySet()) { String port = entry.getKey(); - Matcher matcher = portPattern.matcher(port); + Matcher matcher = PORT_PATTERN.matcher(port); if (!matcher.matches()) { throw new BadContainerConfigurationFormatException( "Invalid port configuration: '" + port + "'."); } - int portNumber = Integer.parseInt(matcher.group(1)); - String protocol = matcher.group(2); - ports.add(new Port(portNumber, Protocol.getFromString(protocol))); + int portNumber = Integer.parseInt(matcher.group("portNum")); + String protocol = matcher.group("protocol"); + ports.add(Port.parseProtocol(portNumber, protocol)); } return ports.build(); } + /** + * Converts a map of volumes strings to a set of {@link AbsoluteUnixPath}s (e.g. {@code + * {"/var/log/my-app-logs":{}}} -> {@code AbsoluteUnixPath().get("/var/log/my-app-logs")}). + * + * @param volumeMap the map to convert + * @return a set of {@link AbsoluteUnixPath}s + */ + @VisibleForTesting + static ImmutableSet volumeMapToSet( + @Nullable Map> volumeMap) + throws BadContainerConfigurationFormatException { + if (volumeMap == null) { + return ImmutableSet.of(); + } + + ImmutableSet.Builder volumeList = ImmutableSet.builder(); + for (String volume : volumeMap.keySet()) { + try { + volumeList.add(AbsoluteUnixPath.get(volume)); + } catch (IllegalArgumentException exception) { + throw new BadContainerConfigurationFormatException("Invalid volume path: " + volume); + } + } + + return volumeList.build(); + } + private JsonToImageTranslator() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfigTemplate.java new file mode 100644 index 0000000000..2e2af58abd --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfigTemplate.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.cloud.tools.jib.json.JsonTemplate; +import javax.annotation.Nullable; + +/** Stores a manifest and container config. */ +public class ManifestAndConfigTemplate implements JsonTemplate { + + @Nullable private String manifestDigest; + + @JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "@class") + @JsonSubTypes({ + @JsonSubTypes.Type(value = OciManifestTemplate.class), + @JsonSubTypes.Type(value = V21ManifestTemplate.class), + @JsonSubTypes.Type(value = V22ManifestTemplate.class), + }) + @Nullable + private ManifestTemplate manifest; + + @Nullable private ContainerConfigurationTemplate config; + + @SuppressWarnings("unused") + private ManifestAndConfigTemplate() {} + + /** + * Creates an instance. + * + * @param manifest the image manifest + * @param config the container configuration + */ + public ManifestAndConfigTemplate( + // TODO: switch to BuildableManifestTemplate after we stop supporting V21 manifest. + ManifestTemplate manifest, + // TODO: remove @Nullable after we stop supporting V21 manifest. + @Nullable ContainerConfigurationTemplate config) { + this(manifest, config, null); + } + + /** + * Creates an instance. + * + * @param manifest the image manifest + * @param config the container configuration + * @param manifestDigest the digest of the manifest + */ + public ManifestAndConfigTemplate( + // TODO: switch to BuildableManifestTemplate after we stop supporting V21 manifest. + ManifestTemplate manifest, + // TODO: remove @Nullable after we stop supporting V21 manifest. + @Nullable ContainerConfigurationTemplate config, + @Nullable String manifestDigest) { + this.manifest = manifest; + this.config = config; + this.manifestDigest = manifestDigest; + } + + /** + * Gets the digest of the manifest. + * + * @return the digest + */ + @Nullable + public String getManifestDigest() { + return manifestDigest; + } + + /** + * Gets the manifest. + * + * @return the manifest + */ + @Nullable + public ManifestTemplate getManifest() { + return manifest; + } + + /** + * Gets the container configuration. + * + * @return the container configuration + */ + @Nullable + public ContainerConfigurationTemplate getConfig() { + return config; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListGenerator.java new file mode 100644 index 0000000000..1e3c80ab03 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListGenerator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.api.client.util.Preconditions; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import java.io.IOException; +import java.util.List; + +/** Generates a manifest list for {@link Image}s. */ +public class ManifestListGenerator { + + private final List images; + + public ManifestListGenerator(List images) { + this.images = images; + } + + /** + * Generates a manifest list JSON for the given {@link Image}s. + * + * @param child type of {@link BuildableManifestTemplate} + * @param manifestTemplateClass the JSON template to translate the image to + * @return a manifest list JSON + * @throws IOException if generating a manifest list fails due to an I/O error when computing + * digests + */ + public ManifestTemplate getManifestListTemplate( + Class manifestTemplateClass) throws IOException { + Preconditions.checkArgument( + manifestTemplateClass == V22ManifestTemplate.class, + "Build an OCI image index is not yet supported"); + Preconditions.checkState(!images.isEmpty(), "no images given"); + + V22ManifestListTemplate manifestList = new V22ManifestListTemplate(); + for (Image image : images) { + ImageToJsonTranslator imageTranslator = new ImageToJsonTranslator(image); + + BlobDescriptor configDescriptor = + Digests.computeDigest(imageTranslator.getContainerConfiguration()); + + BuildableManifestTemplate manifestTemplate = + imageTranslator.getManifestTemplate(manifestTemplateClass, configDescriptor); + BlobDescriptor manifestDescriptor = Digests.computeDigest(manifestTemplate); + + ManifestDescriptorTemplate manifest = new ManifestDescriptorTemplate(); + manifest.setMediaType(manifestTemplate.getManifestMediaType()); + manifest.setSize(manifestDescriptor.getSize()); + manifest.setDigest(manifestDescriptor.getDigest().toString()); + manifest.setPlatform(image.getArchitecture(), image.getOs()); + manifestList.addManifest(manifest); + } + return manifestList; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListTemplate.java new file mode 100644 index 0000000000..8ed4cfa520 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListTemplate.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import java.util.List; + +/** + * Parent class for manifest lists. + * + * @see V22ManifestListTemplate Docker V2.2 format + * @see OciIndexTemplate OCI format + */ +public interface ManifestListTemplate extends ManifestTemplate { + + /** + * Returns a list of digests for a specific platform found in the manifest list. see + * https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list + * + * @param architecture the architecture of the target platform + * @param os the os of the target platform + * @return a list of matching digests + */ + List getDigestsForPlatform(String architecture, String os); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java index 5a6d8717af..fa3c7bb6ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -19,9 +19,11 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; -/** Parent class for image manifest JSON templates. */ +/** Parent class for image manifest and manifest list JSON templates. */ @JsonIgnoreProperties(ignoreUnknown = true) public interface ManifestTemplate extends JsonTemplate { int getSchemaVersion(); + + String getManifestMediaType(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java new file mode 100644 index 0000000000..1ca6529143 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java @@ -0,0 +1,177 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * JSON template for OCI archive "index.json" file. + * + *

    Example manifest JSON: + * + *

    {@code
    + * {
    + *   "schemaVersion": 2,
    + *   "mediaType": "application/vnd.oci.image.index.v1+json",
    + *   "manifests": [
    + *     {
    + *       "mediaType": "application/vnd.oci.image.manifest.v1+json",
    + *       "digest": "sha256:e684b1dceef404268f17d4adf7f755fd9912b8ae64864b3954a83ebb8aa628b3",
    + *       "size": 1132,
    + *       "platform": {
    + *         "architecture": "ppc64le",
    + *         "os": "linux"
    + *       },
    + *       "annotations": {
    + *         "org.opencontainers.image.ref.name": "gcr.io/project/image:tag"
    + *       }
    + *     }
    + *   ]
    + * }
    + * }
    + * + * @see OCI Image + * Index Specification + */ +public class OciIndexTemplate implements ManifestListTemplate { + + /** The OCI Index media type. */ + public static final String MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"; + + private final int schemaVersion = 2; + private final String mediaType = MEDIA_TYPE; + + private final List manifests = new ArrayList<>(); + + @Override + public int getSchemaVersion() { + return schemaVersion; + } + + @Override + public String getManifestMediaType() { + return mediaType; + } + + /** + * Adds a manifest reference with the given {@link BlobDescriptor}. + * + * @param descriptor the manifest blob descriptor + * @param imageReferenceName the image reference name + */ + public void addManifest(BlobDescriptor descriptor, String imageReferenceName) { + ManifestDescriptorTemplate contentDescriptorTemplate = + new ManifestDescriptorTemplate( + OciManifestTemplate.MANIFEST_MEDIA_TYPE, descriptor.getSize(), descriptor.getDigest()); + contentDescriptorTemplate.setAnnotations( + ImmutableMap.of("org.opencontainers.image.ref.name", imageReferenceName)); + manifests.add(contentDescriptorTemplate); + } + + /** + * Adds a manifest. + * + * @param manifest a manifest descriptor + */ + public void addManifest(OciIndexTemplate.ManifestDescriptorTemplate manifest) { + manifests.add(manifest); + } + + @VisibleForTesting + public List getManifests() { + return manifests; + } + + @Override + public List getDigestsForPlatform(String architecture, String os) { + return getManifests().stream() + .filter( + manifest -> + manifest.platform != null + && os.equals(manifest.platform.os) + && architecture.equals(manifest.platform.architecture)) + .map(ManifestDescriptorTemplate::getDigest) + .filter(Objects::nonNull) + .map(DescriptorDigest::toString) + .collect(Collectors.toList()); + } + + /** + * Template for inner JSON object representing a single platform specific manifest. See OCI Image Index + * Specification + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ManifestDescriptorTemplate + extends BuildableManifestTemplate.ContentDescriptorTemplate { + + ManifestDescriptorTemplate(String mediaType, long size, DescriptorDigest digest) { + super(mediaType, size, digest); + } + + /** Necessary for Jackson to create from JSON. */ + @SuppressWarnings("unused") + private ManifestDescriptorTemplate() { + super(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Platform implements JsonTemplate { + @Nullable private String architecture; + @Nullable private String os; + + @Nullable + public String getArchitecture() { + return architecture; + } + + @Nullable + public String getOs() { + return os; + } + } + + @Nullable private OciIndexTemplate.ManifestDescriptorTemplate.Platform platform; + + /** + * Sets a platform. + * + * @param architecture the manifest architecture + * @param os the manifest os + */ + public void setPlatform(String architecture, String os) { + platform = new OciIndexTemplate.ManifestDescriptorTemplate.Platform(); + platform.architecture = architecture; + platform.os = os; + } + + @Nullable + public OciIndexTemplate.ManifestDescriptorTemplate.Platform getPlatform() { + return platform; + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java similarity index 93% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java index 806b24f295..3c2b457852 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,14 +16,14 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; /** - * JSON Template for OCI Manifest Schema + * JSON Template for OCI Manifest Schema. * *

    Example manifest JSON: * @@ -54,7 +54,7 @@ * @see OCI Image * Manifest Specification */ -public class OCIManifestTemplate implements BuildableManifestTemplate { +public class OciManifestTemplate implements BuildableManifestTemplate { /** The OCI manifest media type. */ public static final String MANIFEST_MEDIA_TYPE = "application/vnd.oci.image.manifest.v1+json"; @@ -67,6 +67,8 @@ public class OCIManifestTemplate implements BuildableManifestTemplate { private static final String LAYER_MEDIA_TYPE = "application/vnd.oci.image.layer.v1.tar+gzip"; private final int schemaVersion = 2; + + @SuppressWarnings("unused") private final String mediaType = MANIFEST_MEDIA_TYPE; /** The container configuration reference. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/PlatformNotFoundInBaseImageException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/PlatformNotFoundInBaseImageException.java new file mode 100644 index 0000000000..bafaac0a8b --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/PlatformNotFoundInBaseImageException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.api.RegistryException; + +/** Exception thrown when build target platforms are not found in the base image. */ +public class PlatformNotFoundInBaseImageException extends RegistryException { + + public PlatformNotFoundInBaseImageException(String message) { + super(message); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java index 37b936e052..860a232ffe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.registry.RegistryException; +import com.google.cloud.tools.jib.api.RegistryException; /** Exception thrown when trying to parse an unknown image manifest format. */ public class UnknownManifestFormatException extends RegistryException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnlistedPlatformInManifestListException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnlistedPlatformInManifestListException.java new file mode 100644 index 0000000000..328c41ba03 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnlistedPlatformInManifestListException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.api.RegistryException; + +/** Exception thrown when there is no matching platform in a manifest list. */ +public class UnlistedPlatformInManifestListException extends RegistryException { + + public UnlistedPlatformInManifestListException(String message) { + super(message); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java index 23aa89ebf2..b35a8ad5bc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,15 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** @@ -61,15 +64,17 @@ public class V21ManifestTemplate implements ManifestTemplate { public static final String MEDIA_TYPE = "application/vnd.docker.distribution.manifest.v1+json"; private final int schemaVersion = 1; + private final String mediaType = MEDIA_TYPE; /** The list of layer references. */ private final List fsLayers = new ArrayList<>(); - private final List history = new ArrayList<>(); + private final List history = new ArrayList<>(); /** * Template for inner JSON object representing a layer as part of the list of layer references. */ + @VisibleForTesting static class LayerObjectTemplate implements JsonTemplate { @Nullable private DescriptorDigest blobSum; @@ -80,13 +85,20 @@ DescriptorDigest getDigest() { } } - /** Template for inner JSON object representing the V1-compatible format for a layer. */ - private static class V1CompatibilityTemplate implements JsonTemplate { + /** Template for inner JSON object representing history for a layer. */ + private static class HistoryObjectTemplate implements JsonTemplate { - // TODO: Change to its own JSON template that can extract the layer diff ID. + // The value is basically free-form; they may be structured differently in practice, e.g., + // {"architecture": "amd64", "config": {"User": "1001", ...}, "parent": ...} + // {"id": ..., "container_config": {"Cmd":[""]}} @Nullable private String v1Compatibility; } + /** + * Returns a list of descriptor digests for the layers in the image. + * + * @return a list of descriptor digests for the layers in the image. + */ public List getLayerDigests() { List layerDigests = new ArrayList<>(); @@ -102,13 +114,37 @@ public int getSchemaVersion() { return schemaVersion; } + @Override + public String getManifestMediaType() { + return mediaType; + } + public List getFsLayers() { return Collections.unmodifiableList(fsLayers); } - @VisibleForTesting - @Nullable - String getV1Compatibility(int index) { - return history.get(index).v1Compatibility; + /** + * Attempts to parse the container configuration JSON (of format {@code + * application/vnd.docker.container.image.v1+json}) from the {@code v1Compatibility} value of the + * first {@code history} entry, which corresponds to the latest layer. + * + * @return container configuration if the first history string holds it; {@code null} otherwise + */ + public Optional getContainerConfiguration() { + try { + if (history.isEmpty()) { + return Optional.empty(); + } + String v1Compatibility = history.get(0).v1Compatibility; + if (v1Compatibility == null) { + return Optional.empty(); + } + + return Optional.of( + JsonTemplateMapper.readJson(v1Compatibility, ContainerConfigurationTemplate.class)); + } catch (IOException ex) { + // not a container configuration; ignore and continue + return Optional.empty(); + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java new file mode 100644 index 0000000000..e8e08def02 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java @@ -0,0 +1,183 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * JSON Template for Docker Manifest List Schema V2.2 + * + *

    Example manifest list JSON: + * + *

    {@code
    + * {
    + *   "schemaVersion": 2,
    + *   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
    + *   "manifests": [
    + *     {
    + *       "mediaType": "application/vnd.docker.image.manifest.v2+json",
    + *       "size": 7143,
    + *       "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
    + *       "platform": {
    + *         "architecture": "ppc64le",
    + *         "os": "linux",
    + *       }
    + *     },
    + *     {
    + *       "mediaType": "application/vnd.docker.image.manifest.v2+json",
    + *       "size": 7682,
    + *       "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
    + *       "platform": {
    + *         "architecture": "amd64",
    + *         "os": "linux",
    + *         "features": [
    + *           "sse4"
    + *         ]
    + *       }
    + *     }
    + *   ]
    + * }
    + * }
    + * + * @see Image Manifest + * Version 2, Schema 2: Manifest List + */ +public class V22ManifestListTemplate implements ManifestListTemplate { + + public static final String MANIFEST_MEDIA_TYPE = + "application/vnd.docker.distribution.manifest.list.v2+json"; + private static final int SCHEMA_VERSION = 2; + + private final int schemaVersion = SCHEMA_VERSION; + private final String mediaType = MANIFEST_MEDIA_TYPE; + + @Override + public int getSchemaVersion() { + return schemaVersion; + } + + @Override + public String getManifestMediaType() { + return mediaType; + } + + @Nullable private List manifests; + + /** + * Adds a manifest. + * + * @param manifest a manifest descriptor + */ + public void addManifest(ManifestDescriptorTemplate manifest) { + if (manifests == null) { + manifests = new ArrayList<>(); + } + manifests.add(manifest); + } + + @VisibleForTesting + public List getManifests() { + return Preconditions.checkNotNull(manifests); + } + + @Override + public List getDigestsForPlatform(String architecture, String os) { + return getManifests().stream() + .filter( + manifest -> + manifest.platform != null + && os.equals(manifest.platform.os) + && architecture.equals(manifest.platform.architecture)) + .map(ManifestDescriptorTemplate::getDigest) + .collect(Collectors.toList()); + } + + /** Template for inner JSON object representing a single platform specific manifest. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ManifestDescriptorTemplate implements JsonTemplate { + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Platform implements JsonTemplate { + @Nullable private String architecture; + @Nullable private String os; + + @Nullable + public String getArchitecture() { + return architecture; + } + + @Nullable + public String getOs() { + return os; + } + } + + @Nullable private String mediaType; + @Nullable private String digest; + + @SuppressWarnings("unused") + private long size; + + @Nullable private Platform platform; + + public void setSize(long size) { + this.size = size; + } + + public void setDigest(String digest) { + this.digest = digest; + } + + @Nullable + public String getDigest() { + return digest; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + @Nullable + public String getMediaType() { + return mediaType; + } + + /** + * Sets a platform. + * + * @param architecture the manifest architecture + * @param os the manifest os + */ + public void setPlatform(String architecture, String os) { + platform = new Platform(); + platform.architecture = architecture; + platform.os = os; + } + + @Nullable + public Platform getPlatform() { + return platform; + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java index fc6db4cd6d..ee1493ae95 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -69,6 +69,8 @@ public class V22ManifestTemplate implements BuildableManifestTemplate { "application/vnd.docker.image.rootfs.diff.tar.gzip"; private final int schemaVersion = 2; + + @SuppressWarnings("unused") private final String mediaType = MANIFEST_MEDIA_TYPE; /** The container configuration reference. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java index 612c3030c0..c634085611 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -28,6 +28,7 @@ @JsonAutoDetect( fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) public interface JsonTemplate {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index ad95a6824f..7698116f80 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,11 +17,18 @@ package com.google.cloud.tools.jib.json; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.List; // TODO: Add JsonFactory for HTTP response parsing. @@ -47,7 +54,8 @@ */ public class JsonTemplateMapper { - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectMapper objectMapper = + new ObjectMapper().registerModule(new JavaTimeModule()); /** * Deserializes a JSON file via a JSON object template. @@ -60,61 +68,125 @@ public class JsonTemplateMapper { */ public static T readJsonFromFile(Path jsonFile, Class templateClass) throws IOException { - return objectMapper.readValue(Files.newInputStream(jsonFile), templateClass); + try (InputStream fileIn = Files.newInputStream(jsonFile)) { + return objectMapper.readValue(fileIn, templateClass); + } } /** - * Deserializes a JSON object from a JSON string. + * Deserializes a JSON file via a JSON object template with a shared lock on the file. * * @param child type of {@link JsonTemplate} - * @param jsonString a JSON string + * @param jsonFile a file containing a JSON string + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonFile} + * @throws IOException if an error occurred during reading the file or parsing the JSON + */ + public static T readJsonFromFileWithLock( + Path jsonFile, Class templateClass) throws IOException { + // channel is closed by inputStream.close() + FileChannel channel = FileChannel.open(jsonFile, StandardOpenOption.READ); + channel.lock(0, Long.MAX_VALUE, true); // shared lock, released by channel close + try (InputStream inputStream = Channels.newInputStream(channel)) { + return objectMapper.readValue(inputStream, templateClass); + } + } + + /** + * Deserializes a JSON object from a JSON input stream. + * + * @param child type of {@link JsonTemplate} + * @param jsonStream input stream * @param templateClass the template to deserialize the string to * @return the template filled with the values parsed from {@code jsonString} * @throws IOException if an error occurred during parsing the JSON */ - public static T readJson(String jsonString, Class templateClass) + public static T readJson(InputStream jsonStream, Class templateClass) throws IOException { - return objectMapper.readValue(jsonString, templateClass); + return objectMapper.readValue(jsonStream, templateClass); } /** - * Deserializes a JSON object list from a JSON string. + * Deserializes a JSON object from a JSON string. * - * @param child type of {@link ListOfJsonTemplate} + * @param child type of {@link JsonTemplate} * @param jsonString a JSON string * @param templateClass the template to deserialize the string to - * @return the template filled with the values parsed from {@code jsonFile} + * @return the template filled with the values parsed from {@code jsonString} * @throws IOException if an error occurred during parsing the JSON */ - public static List readListOfJson( - String jsonString, Class templateClass) throws IOException { - return objectMapper.readValue( - jsonString, - objectMapper.getTypeFactory().constructCollectionType(List.class, templateClass)); + public static T readJson(String jsonString, Class templateClass) + throws IOException { + return objectMapper.readValue(jsonString, templateClass); } /** - * Convert a {@link JsonTemplate} to a {@link Blob} of the JSON string. + * Deserializes a JSON object from a JSON byte array. * - * @param template the JSON template to convert - * @return a {@link Blob} of the JSON string + * @param child type of {@link JsonTemplate} + * @param jsonBytes a JSON byte array + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonBytes} + * @throws IOException if an error occurred during parsing the JSON */ - public static Blob toBlob(JsonTemplate template) { - return toBlob((Object) template); + public static T readJson(byte[] jsonBytes, Class templateClass) + throws IOException { + return objectMapper.readValue(jsonBytes, templateClass); } /** - * Convert a {@link ListOfJsonTemplate} to a {@link Blob} of the JSON string. + * Deserializes a JSON object list from a JSON string. * - * @param template the list of JSON templates to convert - * @return a {@link Blob} of the JSON string + * @param child type of {@link JsonTemplate} + * @param jsonString a JSON string + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonString} + * @throws IOException if an error occurred during parsing the JSON */ - public static Blob toBlob(ListOfJsonTemplate template) { - return toBlob(template.getList()); + public static List readListOfJson( + String jsonString, Class templateClass) throws IOException { + CollectionType listType = + objectMapper.getTypeFactory().constructCollectionType(List.class, templateClass); + return objectMapper.readValue(jsonString, listType); + } + + public static String toUtf8String(JsonTemplate template) throws IOException { + return toUtf8String((Object) template); + } + + public static String toUtf8String(List templates) throws IOException { + return toUtf8String((Object) templates); + } + + public static byte[] toByteArray(JsonTemplate template) throws IOException { + return toByteArray((Object) template); + } + + public static byte[] toByteArray(List templates) throws IOException { + return toByteArray((Object) templates); + } + + public static void writeTo(JsonTemplate template, OutputStream out) throws IOException { + writeTo((Object) template, out); + } + + public static void writeTo(List templates, OutputStream out) + throws IOException { + writeTo((Object) templates, out); + } + + private static String toUtf8String(Object template) throws IOException { + return new String(toByteArray(template), StandardCharsets.UTF_8); + } + + private static byte[] toByteArray(Object template) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeTo(template, out); + return out.toByteArray(); } - private static Blob toBlob(Object template) { - return Blobs.from(outputStream -> objectMapper.writeValue(outputStream, template)); + private static void writeTo(Object template, OutputStream out) throws IOException { + objectMapper.writeValue(out, template); } private JsonTemplateMapper() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AbstractManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AbstractManifestPuller.java new file mode 100644 index 0000000000..1845b63bbf --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AbstractManifestPuller.java @@ -0,0 +1,196 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** Base class for manifest pullers. */ +abstract class AbstractManifestPuller + implements RegistryEndpointProvider { + + private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + private final String imageQualifier; + private final Class manifestTemplateClass; + + AbstractManifestPuller( + RegistryEndpointRequestProperties registryEndpointRequestProperties, + String imageQualifier, + Class manifestTemplateClass) { + this.registryEndpointRequestProperties = registryEndpointRequestProperties; + this.imageQualifier = imageQualifier; + this.manifestTemplateClass = manifestTemplateClass; + } + + @Nullable + @Override + public BlobHttpContent getContent() { + return null; + } + + @Override + public List getAccept() { + if (manifestTemplateClass.equals(V21ManifestTemplate.class)) { + return Collections.singletonList(V21ManifestTemplate.MEDIA_TYPE); + } + if (manifestTemplateClass.equals(V22ManifestTemplate.class)) { + return Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE); + } + if (manifestTemplateClass.equals(OciManifestTemplate.class)) { + return Collections.singletonList(OciManifestTemplate.MANIFEST_MEDIA_TYPE); + } + if (manifestTemplateClass.equals(V22ManifestListTemplate.class)) { + return Collections.singletonList(V22ManifestListTemplate.MANIFEST_MEDIA_TYPE); + } + if (manifestTemplateClass.equals(OciIndexTemplate.class)) { + return Collections.singletonList(OciIndexTemplate.MEDIA_TYPE); + } + + return Arrays.asList( + OciManifestTemplate.MANIFEST_MEDIA_TYPE, + V22ManifestTemplate.MANIFEST_MEDIA_TYPE, + V21ManifestTemplate.MEDIA_TYPE, + V22ManifestListTemplate.MANIFEST_MEDIA_TYPE, + OciIndexTemplate.MEDIA_TYPE); + } + + /** Parses the response body into a {@link ManifestAndDigest}. */ + @Override + public R handleResponse(Response response) throws IOException, UnknownManifestFormatException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DescriptorDigest digest = + Digests.computeDigest(response.getBody(), byteArrayOutputStream).getDigest(); + String jsonString = byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()); + T manifestTemplate = getManifestTemplateFromJson(jsonString); + return computeReturn(new ManifestAndDigest<>(manifestTemplate, digest)); + } + + abstract R computeReturn(ManifestAndDigest manifestAndDigest); + + @Override + public URL getApiRoute(String apiRouteBase) throws MalformedURLException { + return new URL( + apiRouteBase + + registryEndpointRequestProperties.getImageName() + + "/manifests/" + + imageQualifier); + } + + @Override + public String getHttpMethod() { + return HttpMethods.GET; + } + + @Override + public String getActionDescription() { + return "pull image manifest for " + + registryEndpointRequestProperties.getServerUrl() + + "/" + + registryEndpointRequestProperties.getImageName() + + ":" + + imageQualifier; + } + + /** + * Instantiates a {@link ManifestTemplate} from a JSON string. This checks the {@code + * schemaVersion} field of the JSON to determine which manifest version to use. + */ + private T getManifestTemplateFromJson(String jsonString) + throws IOException, UnknownManifestFormatException { + ObjectNode node = new ObjectMapper().readValue(jsonString, ObjectNode.class); + if (!node.has("schemaVersion")) { + throw new UnknownManifestFormatException("Cannot find field 'schemaVersion' in manifest"); + } + + int schemaVersion = node.get("schemaVersion").asInt(-1); + if (schemaVersion == -1) { + throw new UnknownManifestFormatException("'schemaVersion' field is not an integer"); + } + + if (schemaVersion == 1) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, V21ManifestTemplate.class)); + } + if (schemaVersion == 2) { + // 'schemaVersion' of 2 can be either Docker V2.2 or OCI. + JsonNode mediaTypeNode = node.get("mediaType"); + if (mediaTypeNode == null) { // not Docker, hence OCI + if (node.get("manifests") != null) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, OciIndexTemplate.class)); + } + if (node.get("config") != null) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, OciManifestTemplate.class)); + } + throw new UnknownManifestFormatException( + "'schemaVersion' is 2, but neither 'manifests' nor 'config' exists"); + } + + String mediaType = mediaTypeNode.asText(); + if (OciManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, OciManifestTemplate.class)); + } + if (V22ManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, V22ManifestTemplate.class)); + } + if (V22ManifestListTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, V22ManifestListTemplate.class)); + } + if (OciIndexTemplate.MEDIA_TYPE.equals(mediaType)) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, OciIndexTemplate.class)); + } + throw new UnknownManifestFormatException("Unknown mediaType: " + mediaType); + } + throw new UnknownManifestFormatException( + "Unknown schemaVersion: " + schemaVersion + " - only 1 and 2 are supported"); + } + + @Override + public R handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index b18f655bb2..20910e73c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,24 +17,34 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** Retrieves the {@code WWW-Authenticate} header from the registry API. */ -class AuthenticationMethodRetriever implements RegistryEndpointProvider { +class AuthenticationMethodRetriever + implements RegistryEndpointProvider> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + @Nullable private final String userAgent; + private final FailoverHttpClient httpClient; AuthenticationMethodRetriever( - RegistryEndpointRequestProperties registryEndpointRequestProperties) { + RegistryEndpointRequestProperties registryEndpointRequestProperties, + @Nullable String userAgent, + FailoverHttpClient httpClient) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; + this.userAgent = userAgent; + this.httpClient = httpClient; } @Nullable @@ -52,12 +62,11 @@ public List getAccept() { * The request did not error, meaning that the registry does not require authentication. * * @param response ignored - * @return {@code null} + * @return {@link Optional#empty()} */ @Override - @Nullable - public RegistryAuthenticator handleResponse(Response response) { - return null; + public Optional handleResponse(Response response) { + return Optional.empty(); } @Override @@ -76,19 +85,17 @@ public String getActionDescription() { } @Override - @Nullable - public RegistryAuthenticator handleHttpResponseException( - HttpResponseException httpResponseException) - throws HttpResponseException, RegistryErrorException { + public Optional handleHttpResponseException( + ResponseException responseException) throws ResponseException, RegistryErrorException { // Only valid for status code of '401 Unauthorized'. - if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { - throw httpResponseException; + if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { + throw responseException; } // Checks if the 'WWW-Authenticate' header is present. - String authenticationMethod = httpResponseException.getHeaders().getAuthenticate(); + String authenticationMethod = responseException.getHeaders().getAuthenticate(); if (authenticationMethod == null) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) + throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException) .addReason("'WWW-Authenticate' header not found") .build(); } @@ -96,7 +103,7 @@ public RegistryAuthenticator handleHttpResponseException( // Parses the header to retrieve the components. try { return RegistryAuthenticator.fromAuthenticationMethod( - authenticationMethod, registryEndpointRequestProperties.getImageName()); + authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient); } catch (RegistryAuthenticationFailedException ex) { throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index ed117fea53..470ddfadde 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,27 +17,24 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; -import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; -import java.io.IOException; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** * Checks if an image's BLOB exists on a registry, and retrieves its {@link BlobDescriptor} if it * exists. */ -class BlobChecker implements RegistryEndpointProvider { +class BlobChecker implements RegistryEndpointProvider> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final DescriptorDigest blobDigest; @@ -49,9 +46,9 @@ class BlobChecker implements RegistryEndpointProvider { this.blobDigest = blobDigest; } - /** @return the BLOB's content descriptor */ + /** Returns the BLOB's content descriptor. */ @Override - public BlobDescriptor handleResponse(Response response) throws RegistryErrorException { + public Optional handleResponse(Response response) throws RegistryErrorException { long contentLength = response.getContentLength(); if (contentLength < 0) { throw new RegistryErrorExceptionBuilder(getActionDescription()) @@ -59,50 +56,30 @@ public BlobDescriptor handleResponse(Response response) throws RegistryErrorExce .build(); } - return new BlobDescriptor(contentLength, blobDigest); + return Optional.of(new BlobDescriptor(contentLength, blobDigest)); } @Override - @Nullable - public BlobDescriptor handleHttpResponseException(HttpResponseException httpResponseException) - throws RegistryErrorException, HttpResponseException { - if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { - throw httpResponseException; + public Optional handleHttpResponseException(ResponseException responseException) + throws ResponseException { + if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { + throw responseException; } - // Finds a BLOB_UNKNOWN error response code. - String errorContent = httpResponseException.getContent(); - if (errorContent == null) { + if (responseException.getContent() == null) { // TODO: The Google HTTP client gives null content for HEAD requests. Make the content never // be null, even for HEAD requests. - return null; - - } else { - try { - ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); - List errors = errorResponse.getErrors(); - if (errors.size() == 1) { - String errorCodeString = errors.get(0).getCode(); - if (errorCodeString == null) { - // Did not get an error code back. - throw httpResponseException; - } - ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); - if (errorCode.equals(ErrorCodes.BLOB_UNKNOWN)) { - return null; - } - } - - } catch (IOException ex) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) - .addReason("Failed to parse registry error response body") - .build(); - } + return Optional.empty(); + } + + // Find a BLOB_UNKNOWN error response code. + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(responseException); + if (errorCode == ErrorCodes.BLOB_UNKNOWN) { + return Optional.empty(); } // BLOB_UNKNOWN was not found as a error response code. - throw httpResponseException; + throw responseException; } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index 842bf001eb..64bf0ff1d6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,16 +17,20 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.http.ResponseException; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; /** Pulls an image's BLOB (layer or container configuration). */ @@ -42,19 +46,30 @@ class BlobPuller implements RegistryEndpointProvider { */ private final OutputStream destinationOutputStream; + private final Consumer blobSizeListener; + private final Consumer writtenByteCountListener; + BlobPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, - OutputStream destinationOutputStream) { + OutputStream destinationOutputStream, + Consumer blobSizeListener, + Consumer writtenByteCountListener) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; this.destinationOutputStream = destinationOutputStream; + this.blobSizeListener = blobSizeListener; + this.writtenByteCountListener = writtenByteCountListener; } @Override public Void handleResponse(Response response) throws IOException, UnexpectedBlobDigestException { - try (OutputStream outputStream = destinationOutputStream) { - BlobDescriptor receivedBlobDescriptor = response.getBody().writeTo(outputStream); + blobSizeListener.accept(response.getContentLength()); + + try (OutputStream outputStream = + new NotifyingOutputStream(destinationOutputStream, writtenByteCountListener)) { + BlobDescriptor receivedBlobDescriptor = + Digests.computeDigest(response.getBody(), outputStream); if (!blobDigest.equals(receivedBlobDescriptor.getDigest())) { throw new UnexpectedBlobDigestException( @@ -100,4 +115,10 @@ public String getActionDescription() { + " with digest " + blobDigest; } + + @Override + public Void handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 8dc6bce671..f3419a3d2e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,17 +18,20 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.common.net.MediaType; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; import javax.annotation.Nullable; /** @@ -47,9 +50,10 @@ class BlobPusher { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final DescriptorDigest blobDigest; private final Blob blob; + @Nullable private final String sourceRepository; /** Initializes the BLOB upload. */ - private class Initializer implements RegistryEndpointProvider { + private class Initializer implements RegistryEndpointProvider> { @Nullable @Override @@ -63,19 +67,18 @@ public List getAccept() { } /** - * @return a URL to continue pushing the BLOB to, or {@code null} if the BLOB already exists on - * the registry + * Returns a URL to continue pushing the BLOB to, or {@link Optional#empty()} if the BLOB + * already exists on the registry. */ - @Nullable @Override - public URL handleResponse(Response response) throws RegistryErrorException { + public Optional handleResponse(Response response) throws RegistryErrorException { switch (response.getStatusCode()) { - case HttpStatusCodes.STATUS_CODE_CREATED: + case HttpURLConnection.HTTP_CREATED: // The BLOB exists in the registry. - return null; + return Optional.empty(); case HttpURLConnection.HTTP_ACCEPTED: - return getRedirectLocation(response); + return Optional.of(getRedirectLocation(response)); default: throw buildRegistryErrorException( @@ -85,11 +88,15 @@ public URL handleResponse(Response response) throws RegistryErrorException { @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { - return new URL( - apiRouteBase - + registryEndpointRequestProperties.getImageName() - + "/blobs/uploads/?mount=" - + blobDigest); + StringBuilder url = + new StringBuilder(apiRouteBase) + .append(registryEndpointRequestProperties.getImageName()) + .append("/blobs/uploads/"); + if (sourceRepository != null) { + url.append("?mount=").append(blobDigest).append("&from=").append(sourceRepository); + } + + return new URL(url.toString()); } @Override @@ -101,17 +108,24 @@ public String getHttpMethod() { public String getActionDescription() { return BlobPusher.this.getActionDescription(); } + + @Override + public Optional handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } /** Writes the BLOB content to the upload location. */ private class Writer implements RegistryEndpointProvider { private final URL location; + private final Consumer writtenByteCountListener; @Nullable @Override public BlobHttpContent getContent() { - return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString()); + return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), writtenByteCountListener); } @Override @@ -119,7 +133,7 @@ public List getAccept() { return Collections.emptyList(); } - /** @return a URL to continue pushing the BLOB to */ + /** Returns a URL to continue pushing the BLOB to. */ @Override public URL handleResponse(Response response) throws RegistryException { // TODO: Handle 204 No Content @@ -141,8 +155,15 @@ public String getActionDescription() { return BlobPusher.this.getActionDescription(); } - private Writer(URL location) { + private Writer(URL location, Consumer writtenByteCountListener) { this.location = location; + this.writtenByteCountListener = writtenByteCountListener; + } + + @Override + public URL handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; } } @@ -167,7 +188,7 @@ public Void handleResponse(Response response) { return null; } - /** @return {@code location} with query parameter 'digest' set to the BLOB's digest */ + /** Returns {@code location} with query parameter 'digest' set to the BLOB's digest. */ @Override public URL getApiRoute(String apiRouteBase) { return new GenericUrl(location).set("digest", blobDigest).toURL(); @@ -186,34 +207,47 @@ public String getActionDescription() { private Committer(URL location) { this.location = location; } + + @Override + public Void handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } BlobPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, - Blob blob) { + Blob blob, + @Nullable String sourceRepository) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; this.blob = blob; + this.sourceRepository = sourceRepository; } /** - * @return a {@link RegistryEndpointProvider} for initializing the BLOB upload with an existence - * check + * Returns a {@link RegistryEndpointProvider} for initializing the BLOB upload with an existence + * check. */ - RegistryEndpointProvider initializer() { + RegistryEndpointProvider> initializer() { return new Initializer(); } /** + * Returns a new Writer. + * * @param location the upload URL + * @param writtenByteCountListener the listener for {@link Blob} push progress (written bytes) * @return a {@link RegistryEndpointProvider} for writing the BLOB to an upload location */ - RegistryEndpointProvider writer(URL location) { - return new Writer(location); + RegistryEndpointProvider writer(URL location, Consumer writtenByteCountListener) { + return new Writer(location, writtenByteCountListener); } /** + * Returns a new Committer. + * * @param location the upload URL * @return a {@link RegistryEndpointProvider} for committing the written BLOB with its digest */ @@ -229,8 +263,8 @@ private RegistryErrorException buildRegistryErrorException(String reason) { } /** - * @return the common action description for {@link Initializer}, {@link Writer}, and {@link - * Committer} + * Returns the common action description for {@link Initializer}, {@link Writer}, and {@link + * Committer}. */ private String getActionDescription() { return "push BLOB for " diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java index 6b45b3953f..23515a75a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java new file mode 100644 index 0000000000..abd3479ca9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; +import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; +import java.io.IOException; +import java.util.List; + +/** Utility methods for parsing {@link ErrorResponseTemplate JSON-encoded error responses}. */ +public class ErrorResponseUtil { + + /** + * Extract an {@link ErrorCodes} response from the error object encoded in an {@link + * HttpResponseException}. + * + * @param responseException the response exception + * @return the parsed {@link ErrorCodes} if found + * @throws ResponseException rethrows the original exception if an error object could not be + * parsed, if there were multiple error objects, or if the error code is unknown. + */ + public static ErrorCodes getErrorCode(ResponseException responseException) + throws ResponseException { + // Obtain the error response code. + String errorContent = responseException.getContent(); + if (errorContent == null) { + throw responseException; + } + + try { + ErrorResponseTemplate errorResponse = + JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); + List errors = errorResponse.getErrors(); + // There may be multiple error objects + if (errors.size() == 1) { + String errorCodeString = errors.get(0).getCode(); + // May not get an error code back. + if (errorCodeString != null) { + // throws IllegalArgumentException if unknown error code + return ErrorCodes.valueOf(errorCodeString); + } + } + + } catch (IOException | IllegalArgumentException ex) { + // Parse exception: either isn't an error object or unknown error code + } + + // rethrow the original exception + throw responseException; + } + + // not intended to be instantiated + private ErrorResponseUtil() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java new file mode 100644 index 0000000000..a95bbd1dcf --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; + +/** Stores a manifest and digest. */ +public class ManifestAndDigest { + + private final T manifest; + private final DescriptorDigest digest; + + public ManifestAndDigest(T manifest, DescriptorDigest digest) { + this.manifest = manifest; + this.digest = digest; + } + + /** + * Gets the manifest. + * + * @return the manifest + */ + public T getManifest() { + return manifest; + } + + /** + * Gets the digest. + * + * @return the digest + */ + public DescriptorDigest getDigest() { + return digest; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestChecker.java new file mode 100644 index 0000000000..31ab274677 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestChecker.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import java.util.Optional; + +/** Checks an image's manifest. */ +class ManifestChecker + extends AbstractManifestPuller>> { + + ManifestChecker( + RegistryEndpointRequestProperties registryEndpointRequestProperties, + String imageQualifier, + Class manifestTemplateClass) { + super(registryEndpointRequestProperties, imageQualifier, manifestTemplateClass); + } + + @Override + public Optional> handleHttpResponseException( + ResponseException responseException) throws ResponseException { + if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { + throw responseException; + } + + if (responseException.getContent() == null) { + return Optional.empty(); + } + + // Find a MANIFEST_UNKNOWN error response code. + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(responseException); + if (errorCode == ErrorCodes.MANIFEST_UNKNOWN) { + return Optional.empty(); + } + + // MANIFEST_UNKNOWN was not found as a error response code. + throw responseException; + } + + @Override + Optional> computeReturn(ManifestAndDigest manifestAndDigest) { + return Optional.of(manifestAndDigest); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index 48114c893b..f6ddfc5ce0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,131 +16,21 @@ package com.google.cloud.tools.jib.registry; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.api.client.http.HttpMethods; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; /** Pulls an image's manifest. */ -class ManifestPuller implements RegistryEndpointProvider { - - private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final String imageTag; - private final Class manifestTemplateClass; +class ManifestPuller + extends AbstractManifestPuller> { ManifestPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, String imageTag, Class manifestTemplateClass) { - this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.imageTag = imageTag; - this.manifestTemplateClass = manifestTemplateClass; - } - - @Nullable - @Override - public BlobHttpContent getContent() { - return null; - } - - @Override - public List getAccept() { - if (manifestTemplateClass.equals(V21ManifestTemplate.class)) { - return Collections.singletonList(V21ManifestTemplate.MEDIA_TYPE); - } - if (manifestTemplateClass.equals(V22ManifestTemplate.class)) { - return Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE); - } - if (manifestTemplateClass.equals(OCIManifestTemplate.class)) { - return Collections.singletonList(OCIManifestTemplate.MANIFEST_MEDIA_TYPE); - } - - return Arrays.asList( - OCIManifestTemplate.MANIFEST_MEDIA_TYPE, - V22ManifestTemplate.MANIFEST_MEDIA_TYPE, - V21ManifestTemplate.MEDIA_TYPE); - } - - /** Parses the response body into a {@link ManifestTemplate}. */ - @Override - public T handleResponse(Response response) throws IOException, UnknownManifestFormatException { - return getManifestTemplateFromJson(Blobs.writeToString(response.getBody())); + super(registryEndpointRequestProperties, imageTag, manifestTemplateClass); } @Override - public URL getApiRoute(String apiRouteBase) throws MalformedURLException { - return new URL( - apiRouteBase + registryEndpointRequestProperties.getImageName() + "/manifests/" + imageTag); - } - - @Override - public String getHttpMethod() { - return HttpMethods.GET; - } - - @Override - public String getActionDescription() { - return "pull image manifest for " - + registryEndpointRequestProperties.getServerUrl() - + "/" - + registryEndpointRequestProperties.getImageName() - + ":" - + imageTag; - } - - /** - * Instantiates a {@link ManifestTemplate} from a JSON string. This checks the {@code - * schemaVersion} field of the JSON to determine which manifest version to use. - */ - private T getManifestTemplateFromJson(String jsonString) - throws IOException, UnknownManifestFormatException { - ObjectNode node = new ObjectMapper().readValue(jsonString, ObjectNode.class); - if (!node.has("schemaVersion")) { - throw new UnknownManifestFormatException("Cannot find field 'schemaVersion' in manifest"); - } - - if (!manifestTemplateClass.equals(ManifestTemplate.class)) { - return JsonTemplateMapper.readJson(jsonString, manifestTemplateClass); - } - - int schemaVersion = node.get("schemaVersion").asInt(-1); - if (schemaVersion == -1) { - throw new UnknownManifestFormatException("`schemaVersion` field is not an integer"); - } - - if (schemaVersion == 1) { - return manifestTemplateClass.cast( - JsonTemplateMapper.readJson(jsonString, V21ManifestTemplate.class)); - } - if (schemaVersion == 2) { - // 'schemaVersion' of 2 can be either Docker V2.2 or OCI. - String mediaType = node.get("mediaType").asText(); - if (V22ManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { - return manifestTemplateClass.cast( - JsonTemplateMapper.readJson(jsonString, V22ManifestTemplate.class)); - } - if (OCIManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { - return manifestTemplateClass.cast( - JsonTemplateMapper.readJson(jsonString, OCIManifestTemplate.class)); - } - throw new UnknownManifestFormatException("Unknown mediaType: " + mediaType); - } - throw new UnknownManifestFormatException( - "Unknown schemaVersion: " + schemaVersion + " - only 1 and 2 are supported"); + ManifestAndDigest computeReturn(ManifestAndDigest manifestAndDigest) { + return manifestAndDigest; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 856fcba5c5..7a86b689df 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,35 +17,73 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.security.DigestException; import java.util.Collections; import java.util.List; +import java.util.StringJoiner; +import org.apache.http.HttpStatus; /** Pushes an image's manifest. */ -class ManifestPusher implements RegistryEndpointProvider { +class ManifestPusher implements RegistryEndpointProvider { + + /** Response header containing digest of pushed image. */ + private static final String RESPONSE_DIGEST_HEADER = "Docker-Content-Digest"; + + /** + * Makes the warning for when the registry responds with an image digest that is not the expected + * digest of the image. + * + * @param expectedDigest the expected image digest + * @param receivedDigests the received image digests + * @return the warning message + */ + private static String makeUnexpectedImageDigestWarning( + DescriptorDigest expectedDigest, List receivedDigests) { + if (receivedDigests.isEmpty()) { + return "Expected image digest " + expectedDigest + ", but received none"; + } + + StringJoiner message = + new StringJoiner(", ", "Expected image digest " + expectedDigest + ", but received: ", ""); + for (String receivedDigest : receivedDigests) { + message.add(receivedDigest); + } + return message.toString(); + } private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final BuildableManifestTemplate manifestTemplate; + private final ManifestTemplate manifestTemplate; private final String imageTag; + private final EventHandlers eventHandlers; ManifestPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, - BuildableManifestTemplate manifestTemplate, - String imageTag) { + ManifestTemplate manifestTemplate, + String imageTag, + EventHandlers eventHandlers) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.manifestTemplate = manifestTemplate; this.imageTag = imageTag; + this.eventHandlers = eventHandlers; } @Override public BlobHttpContent getContent() { + // TODO: Consider giving progress on manifest push as well? return new BlobHttpContent( - JsonTemplateMapper.toBlob(manifestTemplate), manifestTemplate.getManifestMediaType()); + Blobs.from(manifestTemplate), manifestTemplate.getManifestMediaType()); } @Override @@ -54,8 +92,58 @@ public List getAccept() { } @Override - public Void handleResponse(Response response) { - return null; + public DescriptorDigest handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + // docker registry 2.0 and 2.1 returns: + // 400 Bad Request + // {"errors":[{"code":"TAG_INVALID","message":"manifest tag did not match URI"}]} + // docker registry:2.2 returns: + // 400 Bad Request + // {"errors":[{"code":"MANIFEST_INVALID","message":"manifest invalid","detail":{}}]} + // quay.io returns: + // 415 UNSUPPORTED MEDIA TYPE + // {"errors":[{"code":"MANIFEST_INVALID","detail": + // {"message":"manifest schema version not supported"},"message":"manifest invalid"}]} + + if (responseException.getStatusCode() != HttpStatus.SC_BAD_REQUEST + && responseException.getStatusCode() != HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE) { + throw responseException; + } + + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(responseException); + if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.TAG_INVALID) { + throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException) + .addReason( + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2") + .build(); + } + // rethrow: unhandled error response code. + throw responseException; + } + + @Override + public DescriptorDigest handleResponse(Response response) throws IOException { + // Checks if the image digest is as expected. + DescriptorDigest expectedDigest = Digests.computeJsonDigest(manifestTemplate); + + List receivedDigests = response.getHeader(RESPONSE_DIGEST_HEADER); + if (receivedDigests.size() == 1) { + try { + DescriptorDigest receivedDigest = DescriptorDigest.fromDigest(receivedDigests.get(0)); + if (expectedDigest.equals(receivedDigest)) { + return expectedDigest; + } + + } catch (DigestException ex) { + // Invalid digest. + } + } + + // The received digest is not as expected. Warns about this. + eventHandlers.dispatch( + LogEvent.warn(makeUnexpectedImageDigestWarning(expectedDigest, receivedDigests))); + return expectedDigest; } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java new file mode 100644 index 0000000000..de8accc33f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Provides known aliases and alternative hosts for a given registry. */ +public class RegistryAliasGroup { + + private RegistryAliasGroup() {} + + private static final ImmutableList> REGISTRY_ALIAS_GROUPS = + ImmutableList.of( + // Docker Hub alias group (https://github.com/moby/moby/pull/28100) + ImmutableSet.of( + "registry.hub.docker.com", "index.docker.io", "registry-1.docker.io", "docker.io")); + + /** Some registry names are symbolic. */ + private static final ImmutableMap REGISTRY_HOST_MAP = + ImmutableMap.of( + // https://github.com/docker/hub-feedback/issues/1767 + "docker.io", "registry-1.docker.io"); + + /** + * Returns the list of registry aliases for the given {@code registry}, including {@code registry} + * as the first element. + * + * @param registry the registry for which the alias group is requested + * @return non-empty list of registries where {@code registry} is the first element + */ + public static List getAliasesGroup(String registry) { + for (ImmutableSet aliasGroup : REGISTRY_ALIAS_GROUPS) { + if (aliasGroup.contains(registry)) { + // Found a group. Move the requested "registry" to the front before returning it. + Stream self = Stream.of(registry); + Stream withoutSelf = aliasGroup.stream().filter(alias -> !registry.equals(alias)); + return Stream.concat(self, withoutSelf).collect(Collectors.toList()); + } + } + + return Collections.singletonList(registry); + } + + /** + * Returns the server host name to use for the given registry. + * + * @param registry the name of the registry + * @return the registry host + */ + public static String getHost(String registry) { + return REGISTRY_HOST_MAP.getOrDefault(registry, registry); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index b038bf5cdf..0fc0a219da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,18 +17,30 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Map; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -41,113 +53,62 @@ */ public class RegistryAuthenticator { - /** Initializer for {@link RegistryAuthenticator}. */ - public static class Initializer { - - private final String serverUrl; - private final String repository; - private boolean allowHttp = false; - - /** - * Instantiates a new initializer for {@link RegistryAuthenticator}. - * - * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) - * @param repository the image/repository name (also known as, namespace) - */ - private Initializer(String serverUrl, String repository) { - this.serverUrl = serverUrl; - this.repository = repository; - } - - public Initializer setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; - return this; - } - - /** - * Gets a {@link RegistryAuthenticator} for a custom registry server and repository. - * - * @return the {@link RegistryAuthenticator} to authenticate pulls/pushes with the registry, or - * {@code null} if no token authentication is necessary - * @throws RegistryAuthenticationFailedException if failed to create the registry authenticator - * @throws IOException if communicating with the endpoint fails - * @throws RegistryException if communicating with the endpoint fails - */ - @Nullable - public RegistryAuthenticator initialize() - throws RegistryAuthenticationFailedException, IOException, RegistryException { - try { - return RegistryClient.factory(serverUrl, repository) - .setAllowHttp(allowHttp) - .newRegistryClient() - .getRegistryAuthenticator(); - - } catch (MalformedURLException ex) { - throw new RegistryAuthenticationFailedException(ex); - - } catch (InsecureRegistryException ex) { - // HTTP is not allowed, so just return null. - return null; - } - } - } - - /** - * Gets a new initializer for {@link RegistryAuthenticator}. - * - * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) - * @param repository the image/repository name (also known as, namespace) - * @return the new {@link Initializer} - */ - public static Initializer initializer(String serverUrl, String repository) { - return new Initializer(serverUrl, repository); - } - // TODO: Replace with a WWW-Authenticate header parser. /** * Instantiates from parsing a {@code WWW-Authenticate} header. * * @param authenticationMethod the {@code WWW-Authenticate} header value - * @param repository the repository/image name + * @param registryEndpointRequestProperties the registry request properties + * @param userAgent the {@code User-Agent} header value to use in later authentication calls + * @param httpClient HTTP client * @return a new {@link RegistryAuthenticator} for authenticating with the registry service * @throws RegistryAuthenticationFailedException if authentication fails * @see https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ - @Nullable - static RegistryAuthenticator fromAuthenticationMethod( - String authenticationMethod, String repository) throws RegistryAuthenticationFailedException { - // If the authentication method starts with 'basic ' (case insensitive), no registry + static Optional fromAuthenticationMethod( + String authenticationMethod, + RegistryEndpointRequestProperties registryEndpointRequestProperties, + @Nullable String userAgent, + FailoverHttpClient httpClient) + throws RegistryAuthenticationFailedException { + // If the authentication method starts with 'basic' (case insensitive), no registry // authentication is needed. - if (authenticationMethod.matches("^(?i)(basic) .*")) { - return null; + if (authenticationMethod.matches("^(?i)(basic).*")) { + return Optional.empty(); } + String registryUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); // Checks that the authentication method starts with 'bearer ' (case insensitive). if (!authenticationMethod.matches("^(?i)(bearer) .*")) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "Bearer"); + throw newRegistryAuthenticationFailedException( + registryUrl, imageName, authenticationMethod, "Bearer"); } Pattern realmPattern = Pattern.compile("realm=\"(.*?)\""); Matcher realmMatcher = realmPattern.matcher(authenticationMethod); if (!realmMatcher.find()) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "realm"); + throw newRegistryAuthenticationFailedException( + registryUrl, imageName, authenticationMethod, "realm"); } String realm = realmMatcher.group(1); Pattern servicePattern = Pattern.compile("service=\"(.*?)\""); Matcher serviceMatcher = servicePattern.matcher(authenticationMethod); - if (!serviceMatcher.find()) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "service"); - } - String service = serviceMatcher.group(1); + // use the provided registry location when missing service (e.g., for OpenShift) + String service = serviceMatcher.find() ? serviceMatcher.group(1) : registryUrl; - return new RegistryAuthenticator(realm, service, repository); + return Optional.of( + new RegistryAuthenticator( + realm, service, registryEndpointRequestProperties, userAgent, httpClient)); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( - String authenticationMethod, String authParam) { + String registry, String repository, String authenticationMethod, String authParam) { return new RegistryAuthenticationFailedException( + registry, + repository, "'" + authParam + "' was not found in the 'WWW-Authenticate' header, tried to parse: " @@ -155,8 +116,9 @@ private static RegistryAuthenticationFailedException newRegistryAuthenticationFa } /** Template for the authentication response JSON. */ + @VisibleForTesting @JsonIgnoreProperties(ignoreUnknown = true) - private static class AuthenticationResponseTemplate implements JsonTemplate { + static class AuthenticationResponseTemplate implements JsonTemplate { @Nullable private String token; @@ -166,93 +128,184 @@ private static class AuthenticationResponseTemplate implements JsonTemplate { * @see https://docs.docker.com/registry/spec/auth/token/#token-response-fields */ - @Nullable private String access_token; + @Nullable + @JsonProperty("access_token") + private String accessToken; - /** @return {@link #token} if not null, or {@link #access_token} */ + /** Returns {@link #token} if not null, or {@link #accessToken}. */ @Nullable - private String getToken() { + @VisibleForTesting + String getToken() { if (token != null) { return token; } - return access_token; + return accessToken; } } - private final String authenticationUrlBase; - @Nullable private Authorization authorization; + private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + private final String realm; + private final String service; + @Nullable private final String userAgent; + private final FailoverHttpClient httpClient; - RegistryAuthenticator(String realm, String service, String repository) { - authenticationUrlBase = realm + "?service=" + service + "&scope=repository:" + repository + ":"; - } - - /** - * Sets an {@code Authorization} header to authenticate with. - * - * @param authorization the authorization header - * @return this - */ - public RegistryAuthenticator setAuthorization(@Nullable Authorization authorization) { - this.authorization = authorization; - return this; + private RegistryAuthenticator( + String realm, + String service, + RegistryEndpointRequestProperties registryEndpointRequestProperties, + @Nullable String userAgent, + FailoverHttpClient httpClient) { + this.realm = realm; + this.service = service; + this.registryEndpointRequestProperties = registryEndpointRequestProperties; + this.userAgent = userAgent; + this.httpClient = httpClient; } /** * Authenticates permissions to pull. * + * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the pull * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ - public Authorization authenticatePull() throws RegistryAuthenticationFailedException { - return authenticate("pull"); + public Authorization authenticatePull(@Nullable Credential credential) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + return authenticate(credential, "pull"); } /** * Authenticates permission to pull and push. * + * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the push * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ - public Authorization authenticatePush() throws RegistryAuthenticationFailedException { - return authenticate("pull,push"); + public Authorization authenticatePush(@Nullable Credential credential) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + return authenticate(credential, "pull,push"); + } + + private String getServiceScopeRequestParameters(Map repositoryScopes) { + StringBuilder parameters = new StringBuilder("service=").append(service); + for (Map.Entry pair : repositoryScopes.entrySet()) { + parameters + .append("&scope=repository:") + .append(pair.getKey()) + .append(":") + .append(pair.getValue()); + } + return parameters.toString(); } @VisibleForTesting - URL getAuthenticationUrl(String scope) throws MalformedURLException { - return new URL(authenticationUrlBase + scope); + URL getAuthenticationUrl(@Nullable Credential credential, Map repositoryScopes) + throws MalformedURLException { + return isOAuth2Auth(credential) + ? new URL(realm) // Required parameters will be sent via POST . + : new URL(realm + "?" + getServiceScopeRequestParameters(repositoryScopes)); + } + + @VisibleForTesting + String getAuthRequestParameters( + @Nullable Credential credential, Map repositoryScopes) { + String serviceScope = getServiceScopeRequestParameters(repositoryScopes); + return isOAuth2Auth(credential) + ? serviceScope + // https://github.com/GoogleContainerTools/jib/pull/1545 + + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + + "&grant_type=refresh_token&refresh_token=" + // If OAuth2, credential.getPassword() is a refresh token. + + Verify.verifyNotNull(credential).getPassword() + : serviceScope; + } + + @VisibleForTesting + boolean isOAuth2Auth(@Nullable Credential credential) { + return credential != null && credential.isOAuth2RefreshToken(); } /** * Sends the authentication request and retrieves the Bearer authorization token. * + * @param credential the credential used to authenticate * @param scope the scope of permissions to authenticate for * @return the {@link Authorization} response * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were + * not sent over plain HTTP * @see https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ - private Authorization authenticate(String scope) throws RegistryAuthenticationFailedException { + private Authorization authenticate(@Nullable Credential credential, String scope) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + // try authorizing against both the main repository and the source repository too + // to enable cross-repository mounts on pushes + String sourceImageName = registryEndpointRequestProperties.getSourceImageName(); + String imageName = registryEndpointRequestProperties.getImageName(); + if (sourceImageName != null && !sourceImageName.equals(imageName)) { + try { + return authenticate(credential, ImmutableMap.of(imageName, scope, sourceImageName, "pull")); + } catch (RegistryAuthenticationFailedException ex) { + // Unable to obtain authorization with source image: fall through and try without + } + } + return authenticate(credential, ImmutableMap.of(imageName, scope)); + } + + private Authorization authenticate( + @Nullable Credential credential, Map repositoryScopes) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + String registryUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); try { - URL authenticationUrl = getAuthenticationUrl(scope); + URL url = getAuthenticationUrl(credential, repositoryScopes); - try (Connection connection = new Connection(authenticationUrl)) { - Request.Builder requestBuilder = Request.builder(); - if (authorization != null) { - requestBuilder.setAuthorization(authorization); - } - Response response = connection.get(requestBuilder.build()); - String responseString = Blobs.writeToString(response.getBody()); + Request.Builder requestBuilder = + Request.builder() + .setHttpTimeout(JibSystemProperties.getHttpTimeout()) + .setUserAgent(userAgent); + + if (isOAuth2Auth(credential)) { + String parameters = getAuthRequestParameters(credential, repositoryScopes); + requestBuilder.setBody( + new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString())); + } else if (credential != null) { + requestBuilder.setAuthorization( + Authorization.fromBasicCredentials(credential.getUsername(), credential.getPassword())); + } + + String httpMethod = isOAuth2Auth(credential) ? HttpMethods.POST : HttpMethods.GET; + try (Response response = httpClient.call(httpMethod, url, requestBuilder.build())) { AuthenticationResponseTemplate responseJson = - JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); + JsonTemplateMapper.readJson(response.getBody(), AuthenticationResponseTemplate.class); + if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( - "Did not get token in authentication response from " + authenticationUrl); + registryUrl, + imageName, + "Did not get token in authentication response from " + + getAuthenticationUrl(credential, repositoryScopes) + + "; parameters: " + + getAuthRequestParameters(credential, repositoryScopes)); } - return Authorizations.withBearerToken(responseJson.getToken()); + return Authorization.fromBearerToken(responseJson.getToken()); + } + + } catch (ResponseException ex) { + if (ex.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED + && ex.requestAuthorizationCleared()) { + throw new RegistryCredentialsNotSentException(registryUrl, imageName); } + throw new RegistryAuthenticationFailedException(registryUrl, imageName, ex); } catch (IOException ex) { - throw new RegistryAuthenticationFailedException(ex); + throw new RegistryAuthenticationFailedException(registryUrl, imageName, ex); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b54485d2c4..a2d97e35bb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,86 +16,84 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.Timer; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Multimap; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Stream; import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; -/** Interfaces with a registry. */ +/** Interfaces with a registry. Thread-safe. */ +@ThreadSafe public class RegistryClient { - // TODO: Remove - private Timer parentTimer = - new Timer( - new BuildLogger() { - @Override - public void debug(CharSequence message) {} - - @Override - public void info(CharSequence message) {} - - @Override - public void warn(CharSequence message) {} - - @Override - public void error(CharSequence message) {} - - @Override - public void lifecycle(CharSequence message) {} - }, - "NULL TIMER"); - - public RegistryClient setTimer(Timer parentTimer) { - this.parentTimer = parentTimer; - return this; - } - /** Factory for creating {@link RegistryClient}s. */ public static class Factory { + private final EventHandlers eventHandlers; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + private final FailoverHttpClient httpClient; - private boolean allowHttp = false; - @Nullable private Authorization authorization; + @Nullable private String userAgent; + @Nullable private Credential credential; - private Factory(RegistryEndpointRequestProperties registryEndpointRequestProperties) { + private Factory( + EventHandlers eventHandlers, + RegistryEndpointRequestProperties registryEndpointRequestProperties, + FailoverHttpClient httpClient) { + this.eventHandlers = eventHandlers; this.registryEndpointRequestProperties = registryEndpointRequestProperties; + this.httpClient = httpClient; } /** - * Sets whether or not {@code HTTP} should be allowed (credentials should not be sent when set - * to {@code true}). Defaults to {@code false}. + * Sets the authentication credentials to use to authenticate with the registry. * - * @param allowHttp if {@code true}, allows {@code HTTP} connections; otherwise, only {@code - * HTTPS} connections are allowed + * @param credential the {@link Credential} to access the registry/repository * @return this */ - public Factory setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; + public Factory setCredential(@Nullable Credential credential) { + this.credential = credential; return this; } /** - * Sets the authentication credentials to use to authenticate with the registry. + * Sets the value of {@code User-Agent} in headers for registry requests. * - * @param authorization the {@link Authorization} to access the registry/repository + * @param userAgent user agent string * @return this */ - public Factory setAuthorization(@Nullable Authorization authorization) { - this.authorization = authorization; + public Factory setUserAgent(@Nullable String userAgent) { + this.userAgent = userAgent; return this; } @@ -106,119 +104,339 @@ public Factory setAuthorization(@Nullable Authorization authorization) { */ public RegistryClient newRegistryClient() { return new RegistryClient( - authorization, registryEndpointRequestProperties, allowHttp, makeUserAgent()); + eventHandlers, credential, registryEndpointRequestProperties, userAgent, httpClient); } } - @Nullable private static String userAgentSuffix; + private static final int MAX_BEARER_TOKEN_REFRESH_TRIES = 5; /** + * Creates a new {@link Factory} for building a {@link RegistryClient}. + * + * @param eventHandlers the event handlers used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) + * @param httpClient HTTP client * @return the new {@link Factory} */ - public static Factory factory(String serverUrl, String imageName) { - return new Factory(new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory( + EventHandlers eventHandlers, + String serverUrl, + String imageName, + FailoverHttpClient httpClient) { + return new Factory( + eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName), httpClient); } - // TODO: Inject via a RegistryClient.Factory. /** - * Sets a suffix to append to {@code User-Agent} headers. + * Creates a new {@link Factory} for building a {@link RegistryClient}. * - * @param userAgentSuffix the suffix to append + * @param eventHandlers the event handlers used for dispatching log events + * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) + * @param imageName the image/repository name (also known as, namespace) + * @param sourceImageName additional source image to request pull permission from the registry + * @param httpClient HTTP client + * @return the new {@link Factory} */ - public static void setUserAgentSuffix(@Nullable String userAgentSuffix) { - RegistryClient.userAgentSuffix = userAgentSuffix; + public static Factory factory( + EventHandlers eventHandlers, + String serverUrl, + String imageName, + String sourceImageName, + FailoverHttpClient httpClient) { + return new Factory( + eventHandlers, + new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName), + httpClient); } /** - * The {@code User-Agent} is in the form of {@code jib }. For example: {@code jib - * 0.9.0 jib-maven-plugin}. + * A simple class representing the payload of a Docker Registry v2 Bearer Token + * which lists the set of access claims granted. * - * @return the {@code User-Agent} header to send. The {@code User-Agent} can be disabled by - * setting the system property variable {@code _JIB_DISABLE_USER_AGENT} to any non-empty - * string. + *
    +   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
    +   * 
    + * + * @see AccessClaim */ - @VisibleForTesting - static String makeUserAgent() { - if (!Strings.isNullOrEmpty(System.getProperty("_JIB_DISABLE_USER_AGENT"))) { - return ""; - } + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenPayloadTemplate implements JsonTemplate { + + @Nullable private List access; + } + + /** + * Represents an access claim for a repository in a Docker Registry Bearer Token payload. + * + *
    {"type": "repository","name": "library/openjdk","actions":["push","pull"]}
    + */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AccessClaim implements JsonTemplate { - String version = RegistryClient.class.getPackage().getImplementationVersion(); - StringBuilder userAgentBuilder = new StringBuilder(); - userAgentBuilder.append("jib"); - if (version != null) { - userAgentBuilder.append(" ").append(version); + @Nullable private String type; + @Nullable private String name; + @Nullable private List actions; + } + + /** + * Decode the Docker Registry v2 Bearer + * Token to list the granted repositories with their levels of access. + * + * @param token a Docker Registry Bearer Token + * @return a mapping of repository to granted access scopes, or {@code null} if the token is not a + * Docker Registry Bearer Token + */ + @VisibleForTesting + @Nullable + static Multimap decodeTokenRepositoryGrants(String token) { + // Docker Registry Bearer Tokens are based on JWT. A valid JWT is a set of 3 base64-encoded + // parts (header, payload, signature), collated with a ".". The header and payload are + // JSON objects. + String[] jwtParts = token.split("\\.", -1); + if (jwtParts.length != 3) { + return null; } - if (userAgentSuffix != null) { - userAgentBuilder.append(" ").append(userAgentSuffix); + byte[] payloadData = Base64.getDecoder().decode(jwtParts[1]); + + // The payload looks like: + // { + // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], + // "aud":"registry.docker.io", + // "iss":"auth.docker.io", + // "exp":999, + // "iat":999, + // "jti":"zzzz", + // "nbf":999, + // "sub":"e3ae001d-xxx" + // } + // + try { + TokenPayloadTemplate payload = + JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + if (payload.access == null) { + return null; + } + return payload.access.stream() + .filter(claim -> "repository".equals(claim.type)) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + claim -> claim.name, + claim -> claim.actions == null ? Stream.empty() : claim.actions.stream())); + } catch (IOException ex) { + return null; } - return userAgentBuilder.toString(); } - @Nullable private final Authorization authorization; + private final EventHandlers eventHandlers; + @Nullable private final Credential credential; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowHttp; - private final String userAgent; + @Nullable private final String userAgent; + private final FailoverHttpClient httpClient; + + // mutable + private final AtomicReference authorization = new AtomicReference<>(); + private boolean readOnlyBearerAuth; + private final AtomicReference initialBearerAuthenticator = + new AtomicReference<>(); /** * Instantiate with {@link #factory}. * - * @param authorization the {@link Authorization} to access the registry/repository + * @param eventHandlers the event handlers used for dispatching log events + * @param credential credential for registry/repository; will not be used unless {@link + * #configureBasicAuth} or {@link #doBearerAuth} is called * @param registryEndpointRequestProperties properties of registry endpoint requests - * @param allowHttp if {@code true}, allows redirects and fallbacks to HTTP; otherwise, only - * allows HTTPS + * @param userAgent {@code User-Agent} header to send with the request, can be {@code null} + * @param httpClient HTTP client */ private RegistryClient( - @Nullable Authorization authorization, + EventHandlers eventHandlers, + @Nullable Credential credential, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp, - String userAgent) { - this.authorization = authorization; + @Nullable String userAgent, + FailoverHttpClient httpClient) { + this.eventHandlers = eventHandlers; + this.credential = credential; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowHttp = allowHttp; this.userAgent = userAgent; + this.httpClient = httpClient; + } + + /** Configure basic authentication on this registry client. */ + public void configureBasicAuth() { + Preconditions.checkNotNull(credential); + Preconditions.checkState(!credential.isOAuth2RefreshToken()); + + authorization.set( + Authorization.fromBasicCredentials(credential.getUsername(), credential.getPassword())); + + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + eventHandlers.dispatch( + LogEvent.debug("configured basic auth for " + registry + "/" + repository)); } /** - * @return the {@link RegistryAuthenticator} to authenticate pulls/pushes with the registry, or - * {@code null} if no token authentication is necessary + * Attempts bearer authentication for pull. + * + * @return {@code true} if bearer authentication succeeded; {@code false} if the server expects + * basic authentication (and thus bearer authentication was not attempted) * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails + * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ - @Nullable - public RegistryAuthenticator getRegistryAuthenticator() throws IOException, RegistryException { - // Gets the WWW-Authenticate header (eg. 'WWW-Authenticate: Bearer - // realm="https://gcr.io/v2/token",service="gcr.io"') - return callRegistryEndpoint( - new AuthenticationMethodRetriever(registryEndpointRequestProperties)); + public boolean doPullBearerAuth() throws IOException, RegistryException { + return doBearerAuth(true); + } + + /** + * Attempts bearer authentication for pull and push. + * + * @return true if bearer authentication succeeded; false if the server expects basic + * authentication (and thus bearer authentication was not attempted) + * @throws IOException if communicating with the endpoint fails + * @throws RegistryException if communicating with the endpoint fails + * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP + */ + public boolean doPushBearerAuth() throws IOException, RegistryException { + return doBearerAuth(false); + } + + private boolean doBearerAuth(boolean readOnlyBearerAuth) throws IOException, RegistryException { + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + String image = registry + "/" + repository; + eventHandlers.dispatch(LogEvent.debug("attempting bearer auth for " + image + "...")); + + Optional authenticator = + callRegistryEndpoint( + new AuthenticationMethodRetriever( + registryEndpointRequestProperties, getUserAgent(), httpClient)); + if (!authenticator.isPresent()) { + eventHandlers.dispatch(LogEvent.debug("server requires basic auth for " + image)); + return false; // server returned "WWW-Authenticate: Basic ..." + } + + doBearerAuth(readOnlyBearerAuth, authenticator.get()); + return true; + } + + private void doBearerAuth(boolean readOnlyBearerAuth, RegistryAuthenticator authenticator) + throws RegistryException { + initialBearerAuthenticator.set(authenticator); + if (readOnlyBearerAuth) { + authorization.set(authenticator.authenticatePull(credential)); + } else { + authorization.set(authenticator.authenticatePush(credential)); + } + this.readOnlyBearerAuth = readOnlyBearerAuth; + + eventHandlers.dispatch( + LogEvent.debug( + "bearer auth succeeded for " + + registryEndpointRequestProperties.getServerUrl() + + "/" + + registryEndpointRequestProperties.getImageName())); + } + + private Authorization refreshBearerAuth(@Nullable String wwwAuthenticate) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + Preconditions.checkState(isBearerAuth(authorization.get())); + + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + eventHandlers.dispatch( + LogEvent.debug("refreshing bearer auth token for " + registry + "/" + repository + "...")); + + if (wwwAuthenticate != null) { + Optional authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + wwwAuthenticate, registryEndpointRequestProperties, getUserAgent(), httpClient); + if (authenticator.isPresent()) { + if (readOnlyBearerAuth) { + return authenticator.get().authenticatePull(credential); + } + return authenticator.get().authenticatePush(credential); + } + } + + eventHandlers.dispatch( + LogEvent.debug( + "server did not return 'WWW-Authenticate: Bearer' header. Actual: " + wwwAuthenticate)); + if (readOnlyBearerAuth) { + return Verify.verifyNotNull(initialBearerAuthenticator.get()).authenticatePull(credential); + } + return Verify.verifyNotNull(initialBearerAuthenticator.get()).authenticatePush(credential); + } + + /** + * Configure basic authentication or attempts bearer authentication for pulling based on the + * specified authentication method in a server response. + * + * @param wwwAuthenticate {@code WWW-Authenticate} HTTP header value from a server response + * specifying a required authentication method + * @throws RegistryException if communicating with the endpoint fails + * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + */ + public void authPullByWwwAuthenticate(String wwwAuthenticate) throws RegistryException { + Optional authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + wwwAuthenticate, registryEndpointRequestProperties, getUserAgent(), httpClient); + if (authenticator.isPresent()) { + doBearerAuth(true, authenticator.get()); + } else if (credential != null && !credential.isOAuth2RefreshToken()) { + configureBasicAuth(); + } + } + + /** + * Check if a manifest referred to by {@code imageQualifier} (tag or digest) exists on the + * registry. + * + * @param imageQualifier the tag or digest to check for + * @return the {@link ManifestAndDigest} referred to by {@code imageQualifier} if the manifest + * exists on the registry, or {@link Optional#empty()} otherwise + * @throws IOException if communicating with the endpoint fails + * @throws RegistryException if communicating with the endpoint fails + */ + public Optional> checkManifest(String imageQualifier) + throws IOException, RegistryException { + ManifestChecker manifestChecker = + new ManifestChecker<>( + registryEndpointRequestProperties, imageQualifier, ManifestTemplate.class); + return callRegistryEndpoint(manifestChecker); } /** - * Pulls the image manifest for a specific tag. + * Pulls the image manifest and digest for a specific tag. * * @param child type of ManifestTemplate - * @param imageTag the tag to pull on + * @param imageQualifier the tag or digest to pull on * @param manifestTemplateClass the specific version of manifest template to pull, or {@link - * ManifestTemplate} to pull either {@link V22ManifestTemplate} or {@link V21ManifestTemplate} - * @return the manifest template + * ManifestTemplate} to pull predefined subclasses; see: {@link + * ManifestPuller#handleResponse(Response)} + * @return the {@link ManifestAndDigest} * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public T pullManifest( - String imageTag, Class manifestTemplateClass) throws IOException, RegistryException { + public ManifestAndDigest pullManifest( + String imageQualifier, Class manifestTemplateClass) throws IOException, RegistryException { ManifestPuller manifestPuller = - new ManifestPuller<>(registryEndpointRequestProperties, imageTag, manifestTemplateClass); - T manifestTemplate = callRegistryEndpoint(manifestPuller); - if (manifestTemplate == null) { - throw new IllegalStateException("ManifestPuller#handleResponse does not return null"); - } - return manifestTemplate; + new ManifestPuller<>( + registryEndpointRequestProperties, imageQualifier, manifestTemplateClass); + return callRegistryEndpoint(manifestPuller); } - public ManifestTemplate pullManifest(String imageTag) throws IOException, RegistryException { - return pullManifest(imageTag, ManifestTemplate.class); + public ManifestAndDigest pullManifest(String imageQualifier) + throws IOException, RegistryException { + return pullManifest(imageQualifier, ManifestTemplate.class); } /** @@ -226,78 +444,121 @@ public ManifestTemplate pullManifest(String imageTag) throws IOException, Regist * * @param manifestTemplate the image manifest * @param imageTag the tag to push on + * @return the digest of the pushed image * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public void pushManifest(BuildableManifestTemplate manifestTemplate, String imageTag) + public DescriptorDigest pushManifest(ManifestTemplate manifestTemplate, String imageTag) throws IOException, RegistryException { - callRegistryEndpoint( - new ManifestPusher(registryEndpointRequestProperties, manifestTemplate, imageTag)); + if (isBearerAuth(authorization.get()) && readOnlyBearerAuth) { + throw new IllegalStateException("push may fail with pull-only bearer auth token"); + } + + return callRegistryEndpoint( + new ManifestPusher( + registryEndpointRequestProperties, manifestTemplate, imageTag, eventHandlers)); } /** + * Check if a blob is on the registry. + * * @param blobDigest the blob digest to check for - * @return the BLOB's {@link BlobDescriptor} if the BLOB exists on the registry, or {@code null} - * if it doesn't + * @return the BLOB's {@link BlobDescriptor} if the BLOB exists on the registry, or {@link + * Optional#empty()} if it doesn't * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - @Nullable - public BlobDescriptor checkBlob(DescriptorDigest blobDigest) + public Optional checkBlob(DescriptorDigest blobDigest) throws IOException, RegistryException { BlobChecker blobChecker = new BlobChecker(registryEndpointRequestProperties, blobDigest); return callRegistryEndpoint(blobChecker); } /** - * Downloads the BLOB to a file. + * Gets the BLOB referenced by {@code blobDigest}. Note that the BLOB is only pulled when it is + * written out. * * @param blobDigest the digest of the BLOB to download - * @param destinationOutputStream the {@link OutputStream} to write the BLOB to - * @return a {@link Blob} backed by the file at {@code destPath}. The file at {@code destPath} - * must exist for {@link Blob} to be valid. - * @throws IOException if communicating with the endpoint fails - * @throws RegistryException if communicating with the endpoint fails + * @param blobSizeListener callback to receive the total size of the BLOb to pull + * @param writtenByteCountListener listens on byte count written to an output stream during the + * pull + * @return a {@link Blob} */ - public Void pullBlob(DescriptorDigest blobDigest, OutputStream destinationOutputStream) - throws RegistryException, IOException { - BlobPuller blobPuller = - new BlobPuller(registryEndpointRequestProperties, blobDigest, destinationOutputStream); - return callRegistryEndpoint(blobPuller); + public Blob pullBlob( + DescriptorDigest blobDigest, + Consumer blobSizeListener, + Consumer writtenByteCountListener) { + return Blobs.from( + outputStream -> { + try { + callRegistryEndpoint( + new BlobPuller( + registryEndpointRequestProperties, + blobDigest, + outputStream, + blobSizeListener, + writtenByteCountListener)); + + } catch (RegistryException ex) { + throw new IOException(ex); + } + }, + false); } - // TODO: Add mount with 'from' parameter /** - * Pushes the BLOB, or skips if the BLOB already exists on the registry. + * Pushes the BLOB. If the {@code sourceRepository} is provided then the remote registry may skip + * if the BLOB already exists on the registry. * * @param blobDigest the digest of the BLOB, used for existence-check * @param blob the BLOB to push + * @param sourceRepository if pushing to the same registry then the source image, or {@code null} + * otherwise; used to optimize the BLOB push + * @param writtenByteCountListener listens on byte count written to the registry during the push * @return {@code true} if the BLOB already exists on the registry and pushing was skipped; false * if the BLOB was pushed * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public boolean pushBlob(DescriptorDigest blobDigest, Blob blob) + public boolean pushBlob( + DescriptorDigest blobDigest, + Blob blob, + @Nullable String sourceRepository, + Consumer writtenByteCountListener) throws IOException, RegistryException { - BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob); - - try (Timer t = parentTimer.subTimer("pushBlob")) { - try (Timer t2 = t.subTimer("pushBlob POST " + blobDigest)) { + if (isBearerAuth(authorization.get()) && readOnlyBearerAuth) { + throw new IllegalStateException("push may fail with pull-only bearer auth token"); + } - // POST /v2//blobs/uploads/?mount={blob.digest} - URL patchLocation = callRegistryEndpoint(blobPusher.initializer()); - if (patchLocation == null) { + if (sourceRepository != null + && !(JibSystemProperties.useCrossRepositoryBlobMounts() + && canAttemptBlobMount(authorization.get(), sourceRepository))) { + // don't bother requesting a cross-repository blob-mount if we don't have access + sourceRepository = null; + } + BlobPusher blobPusher = + new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); + + try (TimerEventDispatcher timerEventDispatcher = + new TimerEventDispatcher(eventHandlers, "pushBlob")) { + try (TimerEventDispatcher timerEventDispatcher2 = + timerEventDispatcher.subTimer("pushBlob POST " + blobDigest)) { + + // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} + // POST /v2//blobs/uploads/ + Optional patchLocation = callRegistryEndpoint(blobPusher.initializer()); + if (!patchLocation.isPresent()) { // The BLOB exists already. return true; } - t2.lap("pushBlob PATCH " + blobDigest); + timerEventDispatcher2.lap("pushBlob PATCH " + blobDigest); // PATCH with BLOB - URL putLocation = callRegistryEndpoint(blobPusher.writer(patchLocation)); - Preconditions.checkNotNull(putLocation); + URL putLocation = + callRegistryEndpoint(blobPusher.writer(patchLocation.get(), writtenByteCountListener)); - t2.lap("pushBlob PUT " + blobDigest); + timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); // PUT ?digest={blob.digest} callRegistryEndpoint(blobPusher.committer(putLocation)); @@ -307,12 +568,34 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob) } } - /** @return the registry endpoint's API root, without the protocol */ + /** + * Check if the authorization allows using the specified repository can be mounted by the remote + * registry as a source for blobs. More specifically, we can only check if the repository is not + * disallowed. + * + * @param repository repository in question + * @return {@code true} if the repository appears to be mountable + */ @VisibleForTesting - String getApiRouteBase() { - return registryEndpointRequestProperties.getServerUrl() + "/v2/"; + static boolean canAttemptBlobMount(@Nullable Authorization authorization, String repository) { + if (!isBearerAuth(authorization)) { + // Authorization methods other than the Docker Container Registry Token don't provide + // information as to which repositories are accessible. The caller should attempt the mount + // and rely on the registry fallback as required by the spec. + // https://docs.docker.com/registry/spec/api/#pushing-an-image + return true; + } + // if null then does not appear to be a DCRT + Multimap repositoryGrants = + decodeTokenRepositoryGrants(Verify.verifyNotNull(authorization).getToken()); + return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull"); + } + + private static boolean isBearerAuth(@Nullable Authorization authorization) { + return authorization != null && "bearer".equalsIgnoreCase(authorization.getScheme()); } + @Nullable @VisibleForTesting String getUserAgent() { return userAgent; @@ -325,16 +608,33 @@ String getUserAgent() { * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - @Nullable private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { - return new RegistryEndpointCaller<>( - userAgent, - getApiRouteBase(), - registryEndpointProvider, - authorization, - registryEndpointRequestProperties, - allowHttp) - .call(); + int bearerTokenRefreshes = 0; + while (true) { + try { + return new RegistryEndpointCaller<>( + eventHandlers, + getUserAgent(), + registryEndpointProvider, + authorization.get(), + registryEndpointRequestProperties, + httpClient) + .call(); + + } catch (RegistryUnauthorizedException ex) { + if (ex.getHttpResponseException().getStatusCode() + != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED + || !isBearerAuth(authorization.get()) + || ++bearerTokenRefreshes >= MAX_BEARER_TOKEN_REFRESH_TRIES) { + throw ex; + } + + // Because we successfully did bearer authentication initially, getting 401 here probably + // means the token was expired. + String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate(); + authorization.set(refreshBearerAuth(wwwAuthenticate)); + } + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java new file mode 100644 index 0000000000..22fe535d35 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.api.RegistryException; + +/** Thrown when registry request was unauthorized because credentials weren't sent. */ +public class RegistryCredentialsNotSentException extends RegistryException { + + /** + * Identifies the image registry and repository that denied access. + * + * @param registry the image registry + * @param repository the image repository + */ + RegistryCredentialsNotSentException(String registry, String repository) { + super( + "Required credentials for " + + registry + + "/" + + repository + + " were not sent because the connection was over HTTP"); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 2bd54ec75e..c13f74e191 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,25 +16,27 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; -import java.util.function.Function; +import java.util.Locale; import javax.annotation.Nullable; -import javax.net.ssl.SSLPeerUnverifiedException; -import org.apache.http.NoHttpResponseException; -import org.apache.http.conn.HttpHostConnectException; +import javax.net.ssl.SSLException; /** * Makes requests to a registry endpoint. @@ -44,87 +46,59 @@ class RegistryEndpointCaller { /** - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308 + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308. */ @VisibleForTesting static final int STATUS_CODE_PERMANENT_REDIRECT = 308; - private static final String DEFAULT_PROTOCOL = "https"; - - /** Maintains the state of a request. This is used to retry requests with different parameters. */ - private static class RequestState { - - @Nullable private final Authorization authorization; - private final URL url; + // https://github.com/GoogleContainerTools/jib/issues/1316 + @VisibleForTesting + static boolean isBrokenPipe(IOException original) { + Throwable exception = original; + while (exception != null) { + String message = exception.getMessage(); + if (message != null && message.toLowerCase(Locale.US).contains("broken pipe")) { + return true; + } - /** - * @param authorization authentication credentials - * @param url the endpoint URL to call - */ - private RequestState(@Nullable Authorization authorization, URL url) { - this.authorization = authorization; - this.url = url; + exception = exception.getCause(); + if (exception == original) { // just in case if there's a circular chain + return false; + } } + return false; } - /** Makes a {@link Connection} to the specified {@link URL}. */ - private final Function connectionFactory; - - private final RequestState initialRequestState; - private final String userAgent; + private final EventHandlers eventHandlers; + @Nullable private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; + @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowHttp; + private final FailoverHttpClient httpClient; /** * Constructs with parameters for making the request. * + * @param eventHandlers the event dispatcher used for dispatching log events * @param userAgent {@code User-Agent} header to send with the request - * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request - * @param allowHttp if {@code true}, allows redirects and fallbacks to HTTP; otherwise, only - * allows HTTPS - * @throws MalformedURLException if the URL generated for the endpoint is malformed + * @param httpClient HTTP client */ RegistryEndpointCaller( - String userAgent, - String apiRouteBase, + EventHandlers eventHandlers, + @Nullable String userAgent, RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp) - throws MalformedURLException { - this( - userAgent, - apiRouteBase, - registryEndpointProvider, - authorization, - registryEndpointRequestProperties, - allowHttp, - Connection::new); - } - - @VisibleForTesting - RegistryEndpointCaller( - String userAgent, - String apiRouteBase, - RegistryEndpointProvider registryEndpointProvider, - @Nullable Authorization authorization, - RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp, - Function connectionFactory) - throws MalformedURLException { - this.initialRequestState = - new RequestState( - authorization, - registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase)); + FailoverHttpClient httpClient) { + this.eventHandlers = eventHandlers; this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; + this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowHttp = allowHttp; - this.connectionFactory = connectionFactory; + this.httpClient = httpClient; } /** @@ -134,101 +108,121 @@ private RequestState(@Nullable Authorization authorization, URL url) { * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ - @Nullable T call() throws IOException, RegistryException { - return call(initialRequestState); + String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; + URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); + return call(initialRequestUrl); } /** - * Calls the registry endpoint with a certain {@link RequestState}. + * Calls the registry endpoint with a certain {@link URL}. * - * @param requestState the state of the request - determines how to make the request and how to - * process the response - * @return an object representing the response, or {@code null} + * @param url the endpoint URL to call + * @return an object representing the response * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ - @Nullable - private T call(RequestState requestState) throws IOException, RegistryException { - boolean isHttpProtocol = "http".equals(requestState.url.getProtocol()); - if (!allowHttp && isHttpProtocol) { - throw new InsecureRegistryException(requestState.url); - } + private T call(URL url) throws IOException, RegistryException { + String serverUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); - try (Connection connection = connectionFactory.apply(requestState.url)) { - Request.Builder requestBuilder = - Request.builder() - .setUserAgent(userAgent) - .setAccept(registryEndpointProvider.getAccept()) - .setBody(registryEndpointProvider.getContent()); - // Only sends authorization if using HTTPS. - if (!isHttpProtocol) { - requestBuilder.setAuthorization(requestState.authorization); - } - Response response = - connection.send(registryEndpointProvider.getHttpMethod(), requestBuilder.build()); + Request.Builder requestBuilder = + Request.builder() + .setUserAgent(userAgent) + .setHttpTimeout(JibSystemProperties.getHttpTimeout()) + .setAccept(registryEndpointProvider.getAccept()) + .setBody(registryEndpointProvider.getContent()) + .setAuthorization(authorization); + + try (Response response = + httpClient.call(registryEndpointProvider.getHttpMethod(), url, requestBuilder.build())) { return registryEndpointProvider.handleResponse(response); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { // First, see if the endpoint provider handles an exception as an expected response. try { return registryEndpointProvider.handleHttpResponseException(ex); - } catch (HttpResponseException httpResponseException) { - if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST - || httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND - || httpResponseException.getStatusCode() + } catch (ResponseException responseException) { + if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST + || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND + || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_METHOD_NOT_ALLOWED) { // The name or reference was invalid. - ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson( - httpResponseException.getContent(), ErrorResponseTemplate.class); - RegistryErrorExceptionBuilder registryErrorExceptionBuilder = - new RegistryErrorExceptionBuilder( - registryEndpointProvider.getActionDescription(), httpResponseException); - for (ErrorEntryTemplate errorEntry : errorResponse.getErrors()) { - registryErrorExceptionBuilder.addReason(errorEntry); - } + throw newRegistryErrorException(responseException); + + } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); - throw registryErrorExceptionBuilder.build(); - - } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED - || httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { - throw new RegistryUnauthorizedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - httpResponseException); - - } else if (httpResponseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT - || httpResponseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY - || httpResponseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { - // 'Location' header can be relative or absolute. - URL redirectLocation = - new URL(requestState.url, httpResponseException.getHeaders().getLocation()); - return call(new RequestState(requestState.authorization, redirectLocation)); + } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { + if (responseException.requestAuthorizationCleared()) { + throw new RegistryCredentialsNotSentException(serverUrl, imageName); + } else { + // Credentials are either missing or wrong. + throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); + } } else { // Unknown - throw httpResponseException; + throw responseException; } } - } catch (HttpHostConnectException | SSLPeerUnverifiedException ex) { - // Tries to call with HTTP protocol if HTTPS failed to connect. - // Note that this will not succeed if 'allowHttp' is false. - if ("https".equals(requestState.url.getProtocol())) { - GenericUrl httpUrl = new GenericUrl(requestState.url); - httpUrl.setScheme("http"); - return call(new RequestState(requestState.authorization, httpUrl.toURL())); - } + } catch (IOException ex) { + logError("I/O error for image [" + serverUrl + "/" + imageName + "]:"); + logError(" " + ex.getClass().getName()); + logError(" " + (ex.getMessage() == null ? "(null exception message)" : ex.getMessage())); + logErrorIfBrokenPipe(ex); + if (ex instanceof SSLException) { + throw new InsecureRegistryException(url, ex); + } throw ex; + } + } + + @VisibleForTesting + RegistryErrorException newRegistryErrorException(ResponseException responseException) { + RegistryErrorExceptionBuilder registryErrorExceptionBuilder = + new RegistryErrorExceptionBuilder( + registryEndpointProvider.getActionDescription(), responseException); + if (responseException.getContent() != null) { + try { + ErrorResponseTemplate errorResponse = + JsonTemplateMapper.readJson( + responseException.getContent(), ErrorResponseTemplate.class); + for (ErrorEntryTemplate errorEntry : errorResponse.getErrors()) { + registryErrorExceptionBuilder.addReason(errorEntry); + } + } catch (IOException ex) { + registryErrorExceptionBuilder.addReason( + "registry returned error code " + + responseException.getStatusCode() + + "; possible causes include invalid or wrong reference. Actual error output follows:\n" + + responseException.getContent() + + "\n"); + } + } else { + registryErrorExceptionBuilder.addReason( + "registry returned error code " + + responseException.getStatusCode() + + " but did not return any details; possible causes include invalid or wrong reference, or proxy/firewall/VPN interfering \n"); + } + return registryErrorExceptionBuilder.build(); + } + + /** Logs error message in red. */ + private void logError(String message) { + eventHandlers.dispatch(LogEvent.error("\u001B[31;1m" + message + "\u001B[0m")); + } - } catch (NoHttpResponseException ex) { - throw new RegistryNoResponseException(ex); + private void logErrorIfBrokenPipe(IOException ex) { + if (isBrokenPipe(ex)) { + logError( + "broken pipe: the server shut down the connection. Check the server log if possible. " + + "This could also be a proxy issue. For example, a proxy may prevent sending " + + "packets that are too large."); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index bcf7f99001..c66813bee8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,9 +16,10 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -32,43 +33,42 @@ */ interface RegistryEndpointProvider { - /** @return the HTTP method to send the request with */ + /** Returns the HTTP method to send the request with. */ String getHttpMethod(); /** + * Returns the registry endpoint URL. + * * @param apiRouteBase the registry's base URL (for example, {@code https://gcr.io/v2/}) * @return the registry endpoint URL */ URL getApiRoute(String apiRouteBase) throws MalformedURLException; - /** @return the {@link BlobHttpContent} to send as the request body */ + /** Returns the {@link BlobHttpContent} to send as the request body. */ @Nullable BlobHttpContent getContent(); - /** @return a list of MIME types to pass as an HTTP {@code Accept} header */ + /** Returns a list of MIME types to pass as an HTTP {@code Accept} header. */ List getAccept(); /** Handles the response specific to the registry action. */ - @Nullable T handleResponse(Response response) throws IOException, RegistryException; /** - * Handles an {@link HttpResponseException} that occurs. + * Handles an {@link ResponseException} that occurs. Implementation must re-throw the given + * exception if it did not conclusively handled the response exception. * - * @param httpResponseException the {@link HttpResponseException} to handle - * @throws HttpResponseException {@code httpResponseException} if {@code httpResponseException} - * could not be handled + * @param responseException the {@link ResponseException} to handle + * @throws ResponseException {@code responseException} if {@code responseException} could not be + * handled * @throws RegistryErrorException if there is an error with a remote registry */ - @Nullable - default T handleHttpResponseException(HttpResponseException httpResponseException) - throws HttpResponseException, RegistryErrorException { - throw httpResponseException; - } + T handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException; /** - * @return a description of the registry action performed, used in error messages to describe the - * action that failed + * Returns description of the registry action performed, used in error messages to describe the + * action that failed. */ String getActionDescription(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index c7b59d6c6a..05b71aeaff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,19 +16,37 @@ package com.google.cloud.tools.jib.registry; +import javax.annotation.Nullable; + /** Properties of registry endpoint requests. */ class RegistryEndpointRequestProperties { private final String serverUrl; private final String imageName; + @Nullable private final String sourceImageName; /** + * New properties. + * * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) */ RegistryEndpointRequestProperties(String serverUrl, String imageName) { + this(serverUrl, imageName, null); + } + + /** + * New properties. + * + * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) + * @param imageName the image/repository name (also known as, namespace) + * @param sourceImageName additional source image to request pull permission from the registry + */ + RegistryEndpointRequestProperties( + String serverUrl, String imageName, @Nullable String sourceImageName) { this.serverUrl = serverUrl; this.imageName = imageName; + this.sourceImageName = sourceImageName; } String getServerUrl() { @@ -38,4 +56,9 @@ String getServerUrl() { String getImageName() { return imageName; } + + @Nullable + String getSourceImageName() { + return sourceImageName; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java index 78bdfa50e2..f20209a581 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,13 +16,14 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; import javax.annotation.Nullable; /** * Thrown when an HTTP request to a registry endpoint failed with errors as defined in {@link * ErrorCodes}. */ -public class RegistryErrorException extends RegistryException { +class RegistryErrorException extends RegistryException { RegistryErrorException(String message, @Nullable Throwable cause) { super(message, cause); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java index af2ee867fb..3939b49512 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import javax.annotation.Nullable; @@ -39,33 +38,30 @@ private static String getReason(@Nullable String errorCodeString, @Nullable Stri if (message == null) { message = "no details"; } + if (errorCodeString == null) { + return "unknown: " + message; + } try { - if (errorCodeString == null) { - throw new IllegalArgumentException(); + switch (ErrorCodes.valueOf(errorCodeString)) { + case MANIFEST_INVALID: + case BLOB_UNKNOWN: + return message + " (something went wrong)"; + + case MANIFEST_UNKNOWN: + case TAG_INVALID: + case MANIFEST_UNVERIFIED: + return message; + + default: + return "other: " + message; } - - ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); - - if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.BLOB_UNKNOWN) { - return message + " (something went wrong)"; - - } else if (errorCode == ErrorCodes.MANIFEST_UNKNOWN - || errorCode == ErrorCodes.TAG_INVALID - || errorCode == ErrorCodes.MANIFEST_UNVERIFIED) { - return message; - - } else { - return "other: " + message; - } - } catch (IllegalArgumentException ex) { - // Unknown errorCodeString - return "unknown: " + message; + return "unknown error code: " + errorCodeString + " (" + message + ")"; } } - /** @param method the registry method that errored */ + /** Creates a new builder with information about the method that errored. */ RegistryErrorExceptionBuilder(String method, @Nullable Throwable cause) { this.cause = cause; @@ -74,7 +70,7 @@ private static String getReason(@Nullable String errorCodeString, @Nullable Stri errorMessageBuilder.append(" but failed because: "); } - /** @param method the registry method that errored */ + /** Creates a new builder with information about the method that errored. */ RegistryErrorExceptionBuilder(String method) { this(method, null); } @@ -102,9 +98,6 @@ RegistryErrorExceptionBuilder addReason(String reason) { } RegistryErrorException build() { - // Provides a feedback channel. - errorMessageBuilder.append( - " | If this is a bug, please file an issue at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); return new RegistryErrorException(errorMessageBuilder.toString(), cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java index cba9273eb6..ad90506c48 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; + /** Thrown when a pulled BLOB did not have the same digest as requested. */ -public class UnexpectedBlobDigestException extends RegistryException { +class UnexpectedBlobDigestException extends RegistryException { UnexpectedBlobDigestException(String message) { super(message); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java similarity index 68% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java index 0f15a483eb..859aff2902 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,10 +16,12 @@ package com.google.cloud.tools.jib.registry.credentials; +import java.nio.file.Path; + /** Thrown because the requested credential helper CLI does not exist. */ -public class NonexistentDockerCredentialHelperException extends Exception { +public class CredentialHelperNotFoundException extends CredentialRetrievalException { - NonexistentDockerCredentialHelperException(String credentialHelperSuffix, Throwable cause) { - super("The system does not have docker-credential-" + credentialHelperSuffix + " CLI", cause); + CredentialHelperNotFoundException(Path credentialHelper, Throwable cause) { + super("The system does not have " + credentialHelper + " CLI", cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java similarity index 77% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java index aee3a4e2f5..f009a37888 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,11 +16,13 @@ package com.google.cloud.tools.jib.registry.credentials; +import java.nio.file.Path; + /** Thrown because the credential helper does not have credentials for the specified server URL. */ -public class NonexistentServerUrlDockerCredentialHelperException extends Exception { +public class CredentialHelperUnhandledServerUrlException extends CredentialRetrievalException { - NonexistentServerUrlDockerCredentialHelperException( - String credentialHelper, String serverUrl, String credentialHelperOutput) { + CredentialHelperUnhandledServerUrlException( + Path credentialHelper, String serverUrl, String credentialHelperOutput) { super( "The credential helper (" + credentialHelper diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java new file mode 100644 index 0000000000..ee482a7853 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.api.CredentialRetriever; + +/** Thrown if something went wrong during {@link CredentialRetriever#retrieve}. */ +public class CredentialRetrievalException extends Exception { + + CredentialRetrievalException(String message, Throwable cause) { + super(message, cause); + } + + CredentialRetrievalException(String message) { + super(message); + } + + public CredentialRetrievalException(Throwable cause) { + super(cause); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java new file mode 100644 index 0000000000..f4d0af3f05 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -0,0 +1,138 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import javax.annotation.Nullable; + +/** Handles getting useful information from a {@link DockerConfigTemplate}. */ +class DockerConfig { + + /** + * Returns the first entry matching the given key predicates (short-circuiting in the order of + * predicates). + */ + @Nullable + private static Map.Entry findFirstInMapByKey( + Map map, List> keyMatches) { + return keyMatches.stream() + .map(keyMatch -> findFirstInMapByKey(map, keyMatch)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** Returns the first entry matching the given key predicate. */ + @Nullable + private static Map.Entry findFirstInMapByKey(Map map, Predicate keyMatch) { + return map.entrySet().stream() + .filter(entry -> keyMatch.test(entry.getKey())) + .findFirst() + .orElse(null); + } + + private final DockerConfigTemplate dockerConfigTemplate; + + DockerConfig(DockerConfigTemplate dockerConfigTemplate) { + this.dockerConfigTemplate = dockerConfigTemplate; + } + + /** + * Returns the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if + * none exists. The order of lookup preference: + * + *
      + *
    1. Exact registry name + *
    2. https:// + registry name + *
    3. registry name + / + arbitrary suffix + *
    4. https:// + registry name + / arbitrary suffix + *
    + * + * @param registry the registry to get the authorization for + * @return the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if + * none exists + */ + @Nullable + AuthTemplate getAuthFor(String registry) { + Map.Entry authEntry = + findFirstInMapByKey(dockerConfigTemplate.getAuths(), getRegistryMatchersFor(registry)); + return authEntry != null ? authEntry.getValue() : null; + } + + /** + * Determines a {@link DockerCredentialHelper} to use for {@code registry}. + * + *

    If there exists a matching registry entry (or its aliases) in {@code credHelpers}, returns + * the corresponding credential helper. Otherwise, returns the credential helper defined by {@code + * credStore}. + * + *

    See {@link #getRegistryMatchersFor} for the alias lookup order. + * + * @param registry the registry to get the credential helpers for + * @return the {@link DockerCredentialHelper} or {@code null} if none is found for the given + * registry + */ + @Nullable + DockerCredentialHelper getCredentialHelperFor(String registry) { + List> registryMatchers = getRegistryMatchersFor(registry); + + Map.Entry firstCredHelperMatch = + findFirstInMapByKey(dockerConfigTemplate.getCredHelpers(), registryMatchers); + if (firstCredHelperMatch != null) { + return new DockerCredentialHelper( + firstCredHelperMatch.getKey(), + Paths.get("docker-credential-" + firstCredHelperMatch.getValue())); + } + + if (dockerConfigTemplate.getCredsStore() != null) { + return new DockerCredentialHelper( + registry, Paths.get("docker-credential-" + dockerConfigTemplate.getCredsStore())); + } + + return null; + } + + /** + * Gets registry matchers for a registry. + * + *

    Matches are determined in the following order: + * + *

      + *
    1. Exact registry name + *
    2. https:// + registry name + *
    3. registry name + / + arbitrary suffix + *
    4. https:// + registry name + / + arbitrary suffix + *
    + * + * @param registry the registry to get matchers for + * @return the list of predicates to match possible aliases + */ + private List> getRegistryMatchersFor(String registry) { + Predicate exactMatch = registry::equals; + Predicate withHttps = ("https://" + registry)::equals; + Predicate withSuffix = name -> name.startsWith(registry + "/"); + Predicate withHttpsAndSuffix = name -> name.startsWith("https://" + registry + "/"); + return Arrays.asList(exactMatch, withHttps, withSuffix, withHttpsAndSuffix); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 4670214931..2aa5ec3f32 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,16 +16,25 @@ package com.google.cloud.tools.jib.registry.credentials; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import javax.annotation.Nullable; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; /** * Retrieves registry credentials from the Docker config. @@ -33,9 +42,9 @@ *

    The credentials are searched in the following order (stopping when credentials are found): * *

      + *
    1. The credential helper from {@code credHelpers} defined for a registry, if available. + *
    2. The {@code credsStore} credential helper, if available. *
    3. If there is an {@code auth} defined for a registry. - *
    4. Using the {@code credsStore} credential helper, if available. - *
    5. Using the credential helper from {@code credHelpers}, if available. *
    * * @see https://docs.docker.com/engine/reference/commandline/login/#privileged-user-requirement - */ - private static final Path DOCKER_CONFIG_FILE = - Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("config.json"); - private final String registry; private final Path dockerConfigFile; - private final DockerCredentialHelperFactory dockerCredentialHelperFactory; + private final boolean legacyConfigFormat; - public DockerConfigCredentialRetriever(String registry) { - this(registry, DOCKER_CONFIG_FILE); + public static DockerConfigCredentialRetriever create(String registry, Path dockerConfigFile) { + return new DockerConfigCredentialRetriever(registry, dockerConfigFile, false); } - @VisibleForTesting - DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { - this.registry = registry; - this.dockerConfigFile = dockerConfigFile; - this.dockerCredentialHelperFactory = new DockerCredentialHelperFactory(registry); + public static DockerConfigCredentialRetriever createForLegacyFormat( + String registry, Path dockerConfigFile) { + return new DockerConfigCredentialRetriever(registry, dockerConfigFile, true); } - @VisibleForTesting - DockerConfigCredentialRetriever( - String registry, - Path dockerConfigFile, - DockerCredentialHelperFactory dockerCredentialHelperFactory) { + private DockerConfigCredentialRetriever( + String registry, Path dockerConfigFile, boolean legacyConfigFormat) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; - this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; + this.legacyConfigFormat = legacyConfigFormat; + } + + public Path getDockerConfigFile() { + return dockerConfigFile; } /** - * @return {@link Authorization} found for {@code registry}, or {@code null} if not found + * Retrieves credentials for a registry. Tries all possible known aliases. + * + * @param logger a consumer for handling log events + * @return {@link Credential} found for {@code registry}, or {@link Optional#empty} if not found * @throws IOException if failed to parse the config JSON */ - @Nullable - public Authorization retrieve() throws IOException { - DockerConfigTemplate dockerConfigTemplate = loadDockerConfigTemplate(); - if (dockerConfigTemplate == null) { - return null; - } - - // First, tries to find defined auth. - String auth = dockerConfigTemplate.getAuthFor(registry); - if (auth != null) { - return Authorizations.withBasicToken(auth); + public Optional retrieve(Consumer logger) throws IOException { + if (!Files.exists(dockerConfigFile)) { + return Optional.empty(); } - // Then, tries to use a defined credHelpers credential helper. - String credentialHelperSuffix = dockerConfigTemplate.getCredentialHelperFor(registry); - if (credentialHelperSuffix != null) { - try { - return dockerCredentialHelperFactory - .withCredentialHelperSuffix(credentialHelperSuffix) - .retrieve(); - - } catch (IOException - | NonexistentServerUrlDockerCredentialHelperException - | NonexistentDockerCredentialHelperException ex) { - // Ignores credential helper retrieval exceptions. + ObjectMapper objectMapper = + JsonMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .build(); + try (InputStream fileIn = Files.newInputStream(dockerConfigFile)) { + if (legacyConfigFormat) { + // legacy config format is the value of the "auths":{ } block of the new config (i.e., + // the of string -> DockerConfigTemplate.AuthTemplate). + Map auths = + objectMapper.readValue(fileIn, new TypeReference>() {}); + DockerConfig dockerConfig = new DockerConfig(new DockerConfigTemplate(auths)); + return retrieve(dockerConfig, logger); } - } - return null; + DockerConfig dockerConfig = + new DockerConfig(objectMapper.readValue(fileIn, DockerConfigTemplate.class)); + return retrieve(dockerConfig, logger); + } } /** - * Loads the Docker config JSON and caches it. + * Retrieves credentials for a registry alias from a {@link DockerConfig}. * - * @throws IOException if failed to parse the config JSON + * @param dockerConfig the {@link DockerConfig} to retrieve from + * @param logger a consumer for handling log events + * @return the retrieved credentials, or {@code Optional#empty} if none are found */ - @Nullable - private DockerConfigTemplate loadDockerConfigTemplate() throws IOException { - // Loads the Docker config. - if (!Files.exists(dockerConfigFile)) { - return null; - } + @VisibleForTesting + Optional retrieve(DockerConfig dockerConfig, Consumer logger) { + for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { + // First, find a credential helper from "credentialHelpers" and "credsStore" in order. + DockerCredentialHelper dockerCredentialHelper = + dockerConfig.getCredentialHelperFor(registryAlias); + if (dockerCredentialHelper != null) { + try { + Path helperPath = dockerCredentialHelper.getCredentialHelper(); + logger.accept(LogEvent.info("trying " + helperPath + " for " + registryAlias)); + // Tries with the given registry alias (may be the original registry). + return Optional.of(dockerCredentialHelper.retrieve()); + + } catch (IOException + | CredentialHelperUnhandledServerUrlException + | CredentialHelperNotFoundException ex) { + // Warns the user that the specified credential helper cannot be used. + if (ex.getMessage() != null) { + logger.accept(LogEvent.warn(ex.getMessage())); + if (ex.getCause() != null && ex.getCause().getMessage() != null) { + logger.accept(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); + } + } + } + } - return JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class); + // Lastly, find defined auth. + AuthTemplate auth = dockerConfig.getAuthFor(registryAlias); + if (auth != null) { + if (auth.getAuth() != null) { + // 'auth' is a basic authentication token that should be parsed back into credentials + String usernameColonPassword = + new String(Base64.getDecoder().decode(auth.getAuth()), StandardCharsets.UTF_8); + String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); + String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); + logger.accept( + LogEvent.info( + "Docker config auths section defines credentials for " + registryAlias)); + if (auth.getIdentityToken() != null + // These username and password checks may be unnecessary, but doing so to restrict the + // scope only to the Azure behavior to maintain maximum backward-compatibilty. + && username.equals("00000000-0000-0000-0000-000000000000") + && password.isEmpty()) { + logger.accept( + LogEvent.info("Using 'identityToken' in Docker config auth for " + registryAlias)); + return Optional.of( + Credential.from(Credential.OAUTH2_TOKEN_USER_NAME, auth.getIdentityToken())); + } + return Optional.of(Credential.from(username, password)); + } else if (auth.getUsername() != null && auth.getPassword() != null) { + logger.accept( + LogEvent.info( + "Docker config auths section defines username and password for " + + registryAlias)); + return Optional.of(Credential.from(auth.getUsername(), auth.getPassword())); + } + } + } + return Optional.empty(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 88ea117257..e7cd50f8e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,16 +17,26 @@ package com.google.cloud.tools.jib.registry.credentials; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.function.Function; import javax.annotation.Nullable; /** @@ -38,46 +48,115 @@ public class DockerCredentialHelper { private final String serverUrl; - private final String credentialHelperSuffix; + private final Path credentialHelper; + private final Properties systemProperties; + private final Function, ProcessBuilder> processBuilderFactory; + private final Map environment; /** Template for a Docker credential helper output. */ + @VisibleForTesting @JsonIgnoreProperties(ignoreUnknown = true) - private static class DockerCredentialsTemplate implements JsonTemplate { + static class DockerCredentialsTemplate implements JsonTemplate { - @Nullable private String Username; - @Nullable private String Secret; + @Nullable + @VisibleForTesting + @JsonProperty("Username") + String username; + + @Nullable + @VisibleForTesting + @JsonProperty("Secret") + String secret; + } + + /** + * Constructs a new {@link DockerCredentialHelper}. + * + * @param serverUrl the server URL to pass into the credential helper + * @param credentialHelper the path to the credential helper executable + */ + public DockerCredentialHelper(String serverUrl, Path credentialHelper) { + this( + serverUrl, + credentialHelper, + System.getProperties(), + ProcessBuilder::new, + Collections.emptyMap()); } /** - * Construct with {@link DockerCredentialHelperFactory}. + * Constructs a new {@link DockerCredentialHelper}. * * @param serverUrl the server URL to pass into the credential helper - * @param credentialHelperSuffix the credential helper CLI suffix + * @param credentialHelper the path to the credential helper executable + * @param environment environment variables used in configuring the credential helper */ - DockerCredentialHelper(String serverUrl, String credentialHelperSuffix) { + public DockerCredentialHelper( + String serverUrl, Path credentialHelper, Map environment) { + this(serverUrl, credentialHelper, System.getProperties(), ProcessBuilder::new, environment); + } + + @VisibleForTesting + DockerCredentialHelper( + String serverUrl, + Path credentialHelper, + Properties systemProperties, + Function, ProcessBuilder> processBuilderFactory, + Map environment) { this.serverUrl = serverUrl; - this.credentialHelperSuffix = credentialHelperSuffix; + this.credentialHelper = credentialHelper; + this.systemProperties = systemProperties; + this.processBuilderFactory = processBuilderFactory; + this.environment = environment; } /** - * @return the Docker credentials by calling the corresponding CLI. - *

    The credential helper CLI is called in the form: - *

    {@code
    +   * Calls the credential helper CLI.
    +   *
    +   * 

    Calls occur in the form: + * + *

    {@code
        * echo -n  | docker-credential- get
        * }
    * - * @throws IOException if writing/reading process input/output fails. - * @throws NonexistentServerUrlDockerCredentialHelperException if credentials are not found. - * @throws NonexistentDockerCredentialHelperException if the credential helper CLI doesn't exist. + * @return the Docker credentials by calling the corresponding CLI + * @throws IOException if writing/reading process input/output fails + * @throws CredentialHelperUnhandledServerUrlException if no credentials could be found for the + * corresponding server + * @throws CredentialHelperNotFoundException if the credential helper CLI doesn't exist */ - public Authorization retrieve() - throws IOException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException { + public Credential retrieve() + throws IOException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException { + boolean isWindows = + systemProperties.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); + String lowerCaseHelper = credentialHelper.toString().toLowerCase(Locale.ENGLISH); + if (!isWindows || lowerCaseHelper.endsWith(".cmd") || lowerCaseHelper.endsWith(".exe")) { + return retrieve(Arrays.asList(credentialHelper.toString(), "get")); + } + + // We are on Windows with undefined/unknown file extension. + for (String suffix : Arrays.asList(".cmd", ".exe")) { + try { + return retrieve(Arrays.asList(credentialHelper.toString() + suffix, "get")); + } catch (CredentialHelperNotFoundException ignored) { + // ignored + } + } + // On Windows, launching a process from Java without a file extension should normally fail + // (https://github.com/GoogleContainerTools/jib/issues/2399#issuecomment-612972912), but + // running Jib on Linux-like environment (e.g., Cygwin) might succeed? + return retrieve(Arrays.asList(credentialHelper.toString(), "get")); + } + + private Credential retrieve(List credentialHelperCommand) + throws IOException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException { try { - String credentialHelper = "docker-credential-" + credentialHelperSuffix; - String[] credentialHelperCommand = {credentialHelper, "get"}; + ProcessBuilder processBuilder = processBuilderFactory.apply(credentialHelperCommand); + processBuilder.environment().putAll(environment); + Process process = processBuilder.start(); - Process process = new ProcessBuilder(credentialHelperCommand).start(); try (OutputStream processStdin = process.getOutputStream()) { processStdin.write(serverUrl.getBytes(StandardCharsets.UTF_8)); } @@ -88,14 +167,14 @@ public Authorization retrieve() // Throws an exception if the credential store does not have credentials for serverUrl. if (output.contains("credentials not found in native keychain")) { - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } if (output.isEmpty()) { try (InputStreamReader processStderrReader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { String errorOutput = CharStreams.toString(processStderrReader); - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, errorOutput); } } @@ -103,16 +182,16 @@ public Authorization retrieve() try { DockerCredentialsTemplate dockerCredentials = JsonTemplateMapper.readJson(output, DockerCredentialsTemplate.class); - if (dockerCredentials.Username == null || dockerCredentials.Secret == null) { - throw new NonexistentServerUrlDockerCredentialHelperException( + if (Strings.isNullOrEmpty(dockerCredentials.username) + || Strings.isNullOrEmpty(dockerCredentials.secret)) { + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } - return Authorizations.withBasicCredentials( - dockerCredentials.Username, dockerCredentials.Secret); + return Credential.from(dockerCredentials.username, dockerCredentials.secret); } catch (JsonProcessingException ex) { - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } } @@ -124,11 +203,16 @@ public Authorization retrieve() // Checks if the failure is due to a nonexistent credential helper CLI. if (ex.getMessage().contains("No such file or directory") - || ex.getMessage().contains("cannot find the file")) { - throw new NonexistentDockerCredentialHelperException(credentialHelperSuffix, ex); + || ex.getMessage().contains("cannot find the file") + || ex.getMessage().contains("error=2")) /* errno=2 (ENOENT) */ { + throw new CredentialHelperNotFoundException(credentialHelper, ex); } throw ex; } } + + Path getCredentialHelper() { + return credentialHelper; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java deleted file mode 100644 index 7cef1f5bc1..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry.credentials; - -/** Factory class for constructing {@link DockerCredentialHelper}. */ -public class DockerCredentialHelperFactory { - - private final String registry; - - public DockerCredentialHelperFactory(String registry) { - this.registry = registry; - } - - /** - * @param credentialHelperSuffix the suffix of the docker-credential-[suffix] command to be run. - * @return a {@link DockerCredentialHelper} retrieved from the command. - */ - public DockerCredentialHelper withCredentialHelperSuffix(String credentialHelperSuffix) { - return new DockerCredentialHelper(registry, credentialHelperSuffix); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java deleted file mode 100644 index 750ccf3010..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry.credentials; - -import com.google.cloud.tools.jib.http.Authorization; - -/** - * Stores retrieved registry credentials and their source. - * - *

    The credentials are referred to by the registry they are used for. - */ -public class RegistryCredentials { - - private final Authorization authorization; - - /** - * A string representation of where the credentials were retrieved from. This is useful for - * letting the user know which credentials were used. - */ - private final String credentialSource; - - public RegistryCredentials(String credentialSource, Authorization authorization) { - this.authorization = authorization; - this.credentialSource = credentialSource; - } - - public Authorization getAuthorization() { - return authorization; - } - - public String getCredentialSource() { - return credentialSource; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index b05e53e065..99120e3ea4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -32,12 +32,15 @@ * { * "auths": { * "registry": { - * "auth": "username:password in base64" + * "auth": "username:password string in base64", + * "username": "..." + * "password": "..." + * "identityToken": "..." * }, * "anotherregistry": {}, * ... * }, - * "credsStore": "credential helper name", + * "credsStore": "legacy credential helper config acting as a \"default\" helper", * "credHelpers": { * "registry": "credential helper name", * "anotherregistry": "another credential helper name", @@ -46,90 +49,82 @@ * } * }

    * - * If an {@code auth} is defined for a registry, that is a valid {@code Basic} authorization to use - * for that registry. - * - *

    If {@code credsStore} is defined, is a credential helper that stores authorizations for all - * registries listed under {@code auths}. - * *

    Each entry in {@code credHelpers} is a mapping from a registry to a credential helper that - * stores the authorization for that registry. + * stores the authorization for that registry. This takes precedence over {@code credsStore} if + * there exists a match. + * + *

    {@code credsStore} is a legacy config that acts to provide a "default" credential helper if + * there is no match in {@code credHelpers}. * - * @see https://www.projectatomic.io/blog/2016/03/docker-credentials-store/ + *

    If an {@code auth} is defined for a registry, that is a valid {@code Basic} authorization to + * use for that registry. */ @JsonIgnoreProperties(ignoreUnknown = true) public class DockerConfigTemplate implements JsonTemplate { /** Template for an {@code auth} defined for a registry under {@code auths}. */ @JsonIgnoreProperties(ignoreUnknown = true) - private static class AuthTemplate implements JsonTemplate { + public static class AuthTemplate implements JsonTemplate { @Nullable private String auth; + + @Nullable private String username; + + @Nullable private String password; + + // Both "identitytoken" and "identityToken" have been observed. For example, + // https://github.com/GoogleContainerTools/jib/issues/2488 + // https://github.com/spotify/docker-client/issues/580 + @Nullable private String identityToken; + + @Nullable + public String getAuth() { + return auth; + } + + @Nullable + public String getUsername() { + return username; + } + + @Nullable + public String getPassword() { + return password; + } + + @Nullable + public String getIdentityToken() { + return identityToken; + } } /** Maps from registry to its {@link AuthTemplate}. */ - private final Map auths = new HashMap<>(); + private final Map auths; @Nullable private String credsStore; /** Maps from registry to credential helper name. */ private final Map credHelpers = new HashMap<>(); - /** - * @param registry the registry to get the authorization for - * @return the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if - * none exists - */ - @Nullable - public String getAuthFor(String registry) { - AuthTemplate registryAuth = auths.get(registry); - if (registryAuth == null) { - // The registry could be prefixed with the HTTPS protocol. - registryAuth = auths.get("https://" + registry); - } - if (registryAuth != null) { - return registryAuth.auth; - } - return null; + public DockerConfigTemplate(Map auths) { + this.auths = ImmutableMap.copyOf(auths); } - /** - * @param registry the registry to get the credential helpers for - * @return {@code credsStore} if {@code registry} is present in {@code auths}; otherwise, searches - * {@code credHelpers}; otherwise, {@code null} if not found - */ - @Nullable - public String getCredentialHelperFor(String registry) { - if (credsStore != null) { - // The registry could be prefixed with the HTTPS protocol. - if (auths.containsKey(registry) || auths.containsKey("https://" + registry)) { - return credsStore; - } - } - if (credHelpers.containsKey(registry)) { - return credHelpers.get(registry); - } - return null; + @SuppressWarnings("unused") + private DockerConfigTemplate() { + auths = new HashMap<>(); } - @VisibleForTesting - DockerConfigTemplate addAuth(String registry, @Nullable String auth) { - AuthTemplate authTemplate = new AuthTemplate(); - authTemplate.auth = auth; - auths.put(registry, authTemplate); - return this; + public Map getAuths() { + return auths; } - @VisibleForTesting - DockerConfigTemplate setCredsStore(String credsStore) { - this.credsStore = credsStore; - return this; + @Nullable + public String getCredsStore() { + return credsStore; } - @VisibleForTesting - DockerConfigTemplate addCredHelper(String registry, String credHelper) { - credHelpers.put(registry, credHelper); - return this; + public Map getCredHelpers() { + return credHelpers; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java index 5d26bdc695..d8653d6a6d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -32,6 +32,8 @@ public ErrorEntryTemplate(String code, String message) { this.message = message; } + /** Necessary for Jackson to create from JSON. */ + @SuppressWarnings("unused") private ErrorEntryTemplate() {} @Nullable diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java index 8dd07a5ffa..20663bfc23 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java new file mode 100644 index 0000000000..373daad091 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.tar; + +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.io.ByteStreams; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; + +/** Extracts a tarball. */ +public class TarExtractor { + + private TarExtractor() {} + + /** + * Extracts a tarball to the specified destination. + * + * @param source the tarball to extract + * @param destination the output directory + * @throws IOException if extraction fails + */ + public static void extract(Path source, Path destination) throws IOException { + extract(source, destination, false); + } + + /** + * Extracts a tarball to the specified destination. + * + * @param source the tarball to extract + * @param destination the output directory + * @param enableReproducibleTimestamps whether or not reproducible timestamps should be used + * @throws IOException if extraction fails + * @throws IllegalStateException when reproducible timestamps are enabled but the target root used + * for extracting the tar contents is not empty + */ + public static void extract(Path source, Path destination, boolean enableReproducibleTimestamps) + throws IOException { + if (enableReproducibleTimestamps + && Files.isDirectory(destination) + && destination.toFile().list().length != 0) { + throw new IllegalStateException( + "Cannot enable reproducible timestamps. They can only be enabled when the target root doesn't exist or is an empty directory"); + } + String canonicalDestination = destination.toFile().getCanonicalPath(); + List entries = new ArrayList<>(); + try (InputStream in = new BufferedInputStream(Files.newInputStream(source)); + TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { + for (TarArchiveEntry entry = tarArchiveInputStream.getNextEntry(); + entry != null; + entry = tarArchiveInputStream.getNextEntry()) { + entries.add(entry); + Path entryPath = destination.resolve(entry.getName()); + + String canonicalTarget = entryPath.toFile().getCanonicalPath(); + if (!canonicalTarget.startsWith(canonicalDestination + File.separator)) { + String offender = entry.getName() + " from " + source; + throw new IOException("Blocked unzipping files outside destination: " + offender); + } + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + if (entryPath.getParent() != null) { + Files.createDirectories(entryPath.getParent()); + } + + if (entry.isSymbolicLink()) { + Files.createSymbolicLink(entryPath, Paths.get(entry.getLinkName())); + } else { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { + ByteStreams.copy(tarArchiveInputStream, out); + } + } + } + } + } + preserveModificationTimes(destination, entries, enableReproducibleTimestamps); + } + + /** + * Preserve modification timestamps of files and directories in a tar file. If a directory is not + * an entry in the tar file and reproducible timestamps are enabled then its modification + * timestamp is set to a constant value. Note that the modification timestamps of symbolic links + * are not preserved even with reproducible timestamps enabled. + * + * @param destination target root for unzipping + * @param entries list of entries in tar file + * @param enableReproducibleTimestamps whether or not reproducible timestamps should be used + * @throws IOException when I/O error occurs + */ + private static void preserveModificationTimes( + Path destination, List entries, boolean enableReproducibleTimestamps) + throws IOException { + if (enableReproducibleTimestamps) { + FileTime epochPlusOne = FileTime.fromMillis(1000L); + new DirectoryWalker(destination) + .filter(Files::isDirectory) + .walk(path -> Files.setLastModifiedTime(path, epochPlusOne)); + } + for (TarArchiveEntry entry : entries) { + + // Setting the symbolic link's modification timestamp will cause the modification timestamp of + // the target to change + if (!entry.isSymbolicLink()) { + Files.setLastModifiedTime( + destination.resolve(entry.getName()), FileTime.from(entry.getModTime().toInstant())); + } + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 318f9f9253..880e837d26 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,13 +18,10 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.common.io.ByteStreams; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; @@ -33,74 +30,70 @@ /** Builds a tarball archive. */ public class TarStreamBuilder { - @FunctionalInterface - private interface TarArchiveOutputStreamConsumer { - void accept(TarArchiveOutputStream tarArchiveOutputStream) throws IOException; - } - /** - * Maps from {@link TarArchiveEntry} to function that outputs the entry onto a {@link - * TarArchiveOutputStream}. The order of the entries is the order they belong in the tarball. + * Maps from {@link TarArchiveEntry} to a {@link Blob}. The order of the entries is the order they + * belong in the tarball. */ - private final LinkedHashMap archiveMap = - new LinkedHashMap<>(); + private final LinkedHashMap archiveMap = new LinkedHashMap<>(); /** * Writes each entry in the filesystem to the tarball archive stream. * - * @param tarByteStream the stream to write to. + * @param out the stream to write to. * @throws IOException if building the tarball fails. */ - private void writeEntriesAsTarArchive(OutputStream tarByteStream) throws IOException { + public void writeAsTarArchiveTo(OutputStream out) throws IOException { try (TarArchiveOutputStream tarArchiveOutputStream = - new TarArchiveOutputStream(tarByteStream)) { + new TarArchiveOutputStream(out, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - for (Map.Entry entry : - archiveMap.entrySet()) { + tarArchiveOutputStream.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + for (Map.Entry entry : archiveMap.entrySet()) { tarArchiveOutputStream.putArchiveEntry(entry.getKey()); - entry.getValue().accept(tarArchiveOutputStream); + entry.getValue().writeTo(tarArchiveOutputStream); tarArchiveOutputStream.closeArchiveEntry(); } } } /** - * Adds an entry to the archive. + * Adds a {@link TarArchiveEntry} to the archive. * - * @param entry the entry to add. + * @param entry the {@link TarArchiveEntry} */ - public void addEntry(TarArchiveEntry entry) { + public void addTarArchiveEntry(TarArchiveEntry entry) { archiveMap.put( - entry, - tarArchiveOutputStream -> { - // Note that this will skip files that don't exist. - if (entry.isFile()) { - try (InputStream contentStream = - new BufferedInputStream(Files.newInputStream(entry.getFile().toPath()))) { - ByteStreams.copy(contentStream, tarArchiveOutputStream); - } - } - }); + entry, entry.isFile() ? Blobs.from(entry.getPath()) : Blobs.from(ignored -> {}, true)); } /** - * Adds a blob to the archive. + * Adds a blob to the archive. Note that this should be used with raw bytes and not file contents; + * for adding files to the archive, use {@link #addTarArchiveEntry}. * - * @param contents the blob contents to add to the tarball. - * @param name the name of the entry (i.e. filename). + * @param contents the bytes to add to the tarball + * @param name the name of the entry (i.e. filename) + * @param modificationTime the modification time of the entry */ - public void addEntry(String contents, String name) { + public void addByteEntry(byte[] contents, String name, Instant modificationTime) { TarArchiveEntry entry = new TarArchiveEntry(name); - entry.setSize(contents.length()); - archiveMap.put( - entry, - tarArchiveOutputStream -> - tarArchiveOutputStream.write(contents.getBytes(StandardCharsets.UTF_8))); + entry.setSize(contents.length); + entry.setModTime(modificationTime.toEpochMilli()); + archiveMap.put(entry, Blobs.from(outputStream -> outputStream.write(contents), true)); } - /** @return a new {@link Blob} that can stream the uncompressed tarball archive BLOB. */ - public Blob toBlob() { - return Blobs.from(this::writeEntriesAsTarArchive); + /** + * Adds a blob to the archive. Note that this should be used with non-file {@link Blob}s; for + * adding files to the archive, use {@link #addTarArchiveEntry}. + * + * @param blob the {@link Blob} to add to the tarball + * @param size the size (in bytes) of {@code blob} + * @param name the name of the entry (i.e. filename) + * @param modificationTime the modification time of the entry + */ + public void addBlobEntry(Blob blob, long size, String name, Instant modificationTime) { + TarArchiveEntry entry = new TarArchiveEntry(name); + entry.setSize(size); + entry.setModTime(modificationTime.toEpochMilli()); + archiveMap.put(entry, blob); } } diff --git a/jib-core/src/main/resources/commons-logging.properties b/jib-core/src/main/resources/commons-logging.properties new file mode 100644 index 0000000000..463615463a --- /dev/null +++ b/jib-core/src/main/resources/commons-logging.properties @@ -0,0 +1 @@ +org.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java new file mode 100644 index 0000000000..658aee81ef --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.Assert; + +/** Testing infrastructure for running code across multiple threads. */ +public class MultithreadedExecutor implements Closeable { + + private static final Duration MULTITHREADED_TEST_TIMEOUT = Duration.ofSeconds(3); + private static final int THREAD_COUNT = 20; + + private final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT); + + public E invoke(Callable callable) throws ExecutionException, InterruptedException { + List returnValue = invokeAll(Collections.singletonList(callable)); + return returnValue.get(0); + } + + public List invokeAll(List> callables) + throws InterruptedException, ExecutionException { + List> futures = + executorService.invokeAll( + callables, MULTITHREADED_TEST_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + List returnValues = new ArrayList<>(); + for (Future future : futures) { + Assert.assertTrue(future.isDone()); + returnValues.add(future.get()); + } + + return returnValues; + } + + @Override + public void close() throws IOException { + executorService.shutdown(); + try { + executorService.awaitTermination(MULTITHREADED_TEST_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + } catch (InterruptedException ex) { + throw new IOException(ex); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java new file mode 100644 index 0000000000..6d1b34db69 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.AnotherDockerClient; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** Tests for {@link Containerizer}. */ +public class ContainerizerTest { + + @Test + public void testTo() throws CacheDirectoryCreationException, InvalidImageReferenceException { + RegistryImage registryImage = RegistryImage.named("registry/image"); + DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named("daemon/image"); + TarImage tarImage = TarImage.at(Paths.get("ignored")).named("tar/image"); + DockerClient dockerClient = new AnotherDockerClient(); + + verifyTo(Containerizer.to(registryImage)); + verifyTo(Containerizer.to(dockerDaemonImage)); + verifyTo(Containerizer.to(tarImage)); + verifyTo(Containerizer.to(dockerClient, dockerDaemonImage)); + } + + private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreationException { + Assert.assertTrue(containerizer.getAdditionalTags().isEmpty()); + Assert.assertFalse(containerizer.getExecutorService().isPresent()); + Assert.assertEquals( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, + containerizer.getBaseImageLayersCacheDirectory()); + Assert.assertNotEquals( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, + containerizer.getApplicationLayersCacheDirectory()); + Assert.assertFalse(containerizer.getAllowInsecureRegistries()); + Assert.assertEquals("jib-core", containerizer.getToolName()); + + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); + containerizer + .withAdditionalTag("tag1") + .withAdditionalTag("tag2") + .setExecutorService(executorService) + .setBaseImageLayersCache(Paths.get("base/image/layers")) + .setApplicationLayersCache(Paths.get("application/layers")) + .setAllowInsecureRegistries(true) + .setToolName("tool"); + + Assert.assertEquals(ImmutableSet.of("tag1", "tag2"), containerizer.getAdditionalTags()); + Assert.assertTrue(containerizer.getExecutorService().isPresent()); + Assert.assertSame(executorService, containerizer.getExecutorService().get()); + Assert.assertEquals( + Paths.get("base/image/layers"), containerizer.getBaseImageLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("application/layers"), containerizer.getApplicationLayersCacheDirectory()); + Assert.assertTrue(containerizer.getAllowInsecureRegistries()); + Assert.assertEquals("tool", containerizer.getToolName()); + } + + @Test + public void testWithAdditionalTag() throws InvalidImageReferenceException { + Containerizer containerizer = Containerizer.to(DockerDaemonImage.named("image")); + + containerizer.withAdditionalTag("tag"); + try { + containerizer.withAdditionalTag("+invalid+"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("invalid tag '+invalid+'", ex.getMessage()); + } + } + + @Test + public void testGetImageConfiguration_registryImage() throws InvalidImageReferenceException { + CredentialRetriever credentialRetriever = Mockito.mock(CredentialRetriever.class); + Containerizer containerizer = + Containerizer.to( + RegistryImage.named("registry/image").addCredentialRetriever(credentialRetriever)); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("registry/image", imageConfiguration.getImage().toString()); + Assert.assertEquals( + Arrays.asList(credentialRetriever), imageConfiguration.getCredentialRetrievers()); + } + + @Test + public void testGetImageConfiguration_dockerDaemonImage() throws InvalidImageReferenceException { + Containerizer containerizer = Containerizer.to(DockerDaemonImage.named("docker/daemon/image")); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("docker/daemon/image", imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + } + + @Test + public void testGetImageConfiguration_tarImage() throws InvalidImageReferenceException { + Containerizer containerizer = + Containerizer.to(TarImage.at(Paths.get("output/file")).named("tar/image")); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("tar/image", imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + } + + @Test + public void testGetApplicationLayersCacheDirectory_defaults() + throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { + Containerizer containerizer = Containerizer.to(RegistryImage.named("registry/image")); + Path applicationLayersCache = containerizer.getApplicationLayersCacheDirectory(); + Path expectedCacheDir = + Paths.get(System.getProperty("java.io.tmpdir")) + .resolve("jib-core-application-layers-cache"); + Assert.assertTrue(Files.isSameFile(expectedCacheDir, applicationLayersCache)); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java new file mode 100644 index 0000000000..a5444441f3 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link com.google.cloud.tools.jib.api.Credential}. */ +public class CredentialTest { + + @Test + public void testCredentialsHash() { + Credential credentialA1 = Credential.from("username", "password"); + Credential credentialA2 = Credential.from("username", "password"); + Credential credentialB1 = Credential.from("", ""); + Credential credentialB2 = Credential.from("", ""); + + Assert.assertEquals(credentialA1, credentialA2); + Assert.assertEquals(credentialB1, credentialB2); + Assert.assertNotEquals(credentialA1, credentialB1); + Assert.assertNotEquals(credentialA1, credentialB2); + + Set credentialSet = + new HashSet<>(Arrays.asList(credentialA1, credentialA2, credentialB1, credentialB2)); + Assert.assertEquals(new HashSet<>(Arrays.asList(credentialA2, credentialB1)), credentialSet); + } + + @Test + public void testCredentialsOAuth2RefreshToken() { + Credential oauth2Credential = Credential.from("", "eyJhbGciOi...3gw"); + Assert.assertTrue( + "Credential should be an auth2 token when username is ", + oauth2Credential.isOAuth2RefreshToken()); + Assert.assertEquals( + "OAuth2 token credential should take password as refresh token", + "eyJhbGciOi...3gw", + oauth2Credential.getPassword()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java index 4341bd1101..ac55ceccc3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import java.security.DigestException; import java.util.HashMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerClientResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerClientResolverTest.java new file mode 100644 index 0000000000..17c6dc6ad0 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerClientResolverTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.docker.AnotherDockerClient; +import com.google.cloud.tools.jib.docker.DockerClientResolver; +import java.util.Collections; +import java.util.Optional; +import org.junit.Test; + +/** Tests for {@link DockerClientResolver}. */ +public class DockerClientResolverTest { + + @Test + public void testDockerClientIsReturned() { + Optional dockerClient = + DockerClientResolver.resolve(Collections.singletonMap("test", "true")); + assertThat(dockerClient.get()).isInstanceOf(AnotherDockerClient.class); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java new file mode 100644 index 0000000000..0ffd4dd078 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.docker.CliDockerClient; +import com.google.common.collect.ImmutableMap; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerDaemonImage}. */ +public class DockerDaemonImageTest { + + @Test + public void testGetters_default() throws InvalidImageReferenceException { + DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named("docker/daemon/image"); + + Assert.assertEquals("docker/daemon/image", dockerDaemonImage.getImageReference().toString()); + Assert.assertEquals( + CliDockerClient.DEFAULT_DOCKER_CLIENT, dockerDaemonImage.getDockerExecutable()); + Assert.assertEquals(0, dockerDaemonImage.getDockerEnvironment().size()); + } + + @Test + public void testGetters() throws InvalidImageReferenceException { + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named("docker/daemon/image") + .setDockerExecutable(Paths.get("docker/binary")) + .setDockerEnvironment(ImmutableMap.of("key", "value")); + + Assert.assertEquals(Paths.get("docker/binary"), dockerDaemonImage.getDockerExecutable()); + Assert.assertEquals(ImmutableMap.of("key", "value"), dockerDaemonImage.getDockerEnvironment()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java similarity index 50% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java index d07008e262..6a70245861 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,12 +14,13 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import com.google.common.base.Strings; import java.util.Arrays; import java.util.List; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; @@ -52,10 +53,9 @@ public void testParse_pass() throws InvalidImageReferenceException { for (String goodRegistry : goodRegistries) { for (String goodRepository : goodRepositories) { for (String goodTag : goodTags) { - verifyParse(goodRegistry, goodRepository, ":", goodTag); - } - for (String goodDigest : goodDigests) { - verifyParse(goodRegistry, goodRepository, "@", goodDigest); + for (String goodDigest : goodDigests) { + verifyParse(goodRegistry, goodRepository, goodTag, goodDigest); + } } } } @@ -66,9 +66,9 @@ public void testParse_dockerHub_official() throws InvalidImageReferenceException String imageReferenceString = "busybox"; ImageReference imageReference = ImageReference.parse(imageReferenceString); - Assert.assertEquals("registry.hub.docker.com", imageReference.getRegistry()); + Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("library/busybox", imageReference.getRepository()); - Assert.assertEquals("latest", imageReference.getTag()); + Assert.assertEquals("latest", imageReference.getTag().orElse(null)); } @Test @@ -76,9 +76,9 @@ public void testParse_dockerHub_user() throws InvalidImageReferenceException { String imageReferenceString = "someuser/someimage"; ImageReference imageReference = ImageReference.parse(imageReferenceString); - Assert.assertEquals("registry.hub.docker.com", imageReference.getRegistry()); + Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("someuser/someimage", imageReference.getRepository()); - Assert.assertEquals("latest", imageReference.getTag()); + Assert.assertEquals("latest", imageReference.getTag().orElse(null)); } @Test @@ -89,7 +89,7 @@ public void testParse_invalid() { Assert.fail(badImageReference + " should not be a valid image reference"); } catch (InvalidImageReferenceException ex) { - Assert.assertThat(ex.getMessage(), CoreMatchers.containsString(badImageReference)); + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString(badImageReference)); } } } @@ -107,21 +107,24 @@ public void testOf_smoke() { expectedRepository, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getRepository()); Assert.assertEquals( - expectedTag, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getTag()); + expectedTag, + ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getTag().orElse(null)); Assert.assertEquals( - "registry.hub.docker.com", + "registry-1.docker.io", ImageReference.of(null, expectedRepository, expectedTag).getRegistry()); Assert.assertEquals( - "registry.hub.docker.com", ImageReference.of(null, expectedRepository, null).getRegistry()); + "registry-1.docker.io", ImageReference.of(null, expectedRepository, null).getRegistry()); + Assert.assertEquals( + "latest", + ImageReference.of(expectedRegistry, expectedRepository, null).getTag().orElse(null)); Assert.assertEquals( - "latest", ImageReference.of(expectedRegistry, expectedRepository, null).getTag()); - Assert.assertEquals("latest", ImageReference.of(null, expectedRepository, null).getTag()); + "latest", ImageReference.of(null, expectedRepository, null).getTag().orElse(null)); Assert.assertEquals( expectedRepository, ImageReference.of(null, expectedRepository, null).getRepository()); } @Test - public void testToString() { + public void testToString() throws InvalidImageReferenceException { Assert.assertEquals("someimage", ImageReference.of(null, "someimage", null).toString()); Assert.assertEquals("someimage", ImageReference.of("", "someimage", "").toString()); Assert.assertEquals( @@ -132,40 +135,140 @@ public void testToString() { Assert.assertEquals( "anotherregistry/anotherimage:sometag", ImageReference.of("anotherregistry", "anotherimage", "sometag").toString()); + + Assert.assertEquals( + "someimage@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ImageReference.of( + null, + "someimage", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .toString()); + Assert.assertEquals( + "gcr.io/distroless/java@sha256:b430543bea1d8326e767058bdab3a2482ea45f59d7af5c5c61334cd29ede88a1", + ImageReference.parse( + "gcr.io/distroless/java@sha256:b430543bea1d8326e767058bdab3a2482ea45f59d7af5c5c61334cd29ede88a1") + .toString()); } @Test - public void testToStringWithTag() { + public void testToStringWithQualifier() { Assert.assertEquals( - "someimage:latest", ImageReference.of(null, "someimage", null).toStringWithTag()); + "someimage:latest", ImageReference.of(null, "someimage", null).toStringWithQualifier()); Assert.assertEquals( - "someimage:latest", ImageReference.of("", "someimage", "").toStringWithTag()); + "someimage:latest", ImageReference.of("", "someimage", "").toStringWithQualifier()); Assert.assertEquals( "someotherimage:latest", - ImageReference.of(null, "library/someotherimage", null).toStringWithTag()); + ImageReference.of(null, "library/someotherimage", null).toStringWithQualifier()); Assert.assertEquals( "someregistry/someotherimage:latest", - ImageReference.of("someregistry", "someotherimage", null).toStringWithTag()); + ImageReference.of("someregistry", "someotherimage", null).toStringWithQualifier()); Assert.assertEquals( "anotherregistry/anotherimage:sometag", - ImageReference.of("anotherregistry", "anotherimage", "sometag").toStringWithTag()); + ImageReference.of("anotherregistry", "anotherimage", "sometag").toStringWithQualifier()); + Assert.assertEquals( + "anotherregistry/anotherimage@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ImageReference.of( + "anotherregistry", + "anotherimage", + null, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .toStringWithQualifier()); + Assert.assertEquals( + "anotherregistry/anotherimage@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ImageReference.of( + "anotherregistry", + "anotherimage", + "sometag", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .toStringWithQualifier()); + } + + @Test + public void testIsScratch() throws InvalidImageReferenceException { + Assert.assertTrue(ImageReference.parse("scratch").isScratch()); + Assert.assertTrue(ImageReference.scratch().isScratch()); + Assert.assertFalse(ImageReference.of("", "scratch", "").isScratch()); + Assert.assertFalse(ImageReference.of(null, "scratch", null).isScratch()); + } + + @Test + public void testToString_scratch() { + Assert.assertEquals("scratch", ImageReference.scratch().toString()); + } + + @Test + public void testGetRegistry() { + Assert.assertEquals( + "registry-1.docker.io", ImageReference.of(null, "someimage", null).getRegistry()); + Assert.assertEquals( + "registry-1.docker.io", ImageReference.of("docker.io", "someimage", null).getRegistry()); + Assert.assertEquals( + "index.docker.io", ImageReference.of("index.docker.io", "someimage", null).getRegistry()); + Assert.assertEquals( + "registry.hub.docker.com", + ImageReference.of("registry.hub.docker.com", "someimage", null).getRegistry()); + Assert.assertEquals("gcr.io", ImageReference.of("gcr.io", "someimage", null).getRegistry()); } - private void verifyParse(String registry, String repository, String tagSeparator, String tag) + @Test + public void testEquality() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("gcr.io/project/image:tag"); + + Assert.assertEquals(image1, image2); + Assert.assertEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentRegistry() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("registry-1.docker.io/project/image:tag"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentRepository() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("gcr.io/project2/image:tag"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentTag() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag1"); + ImageReference image2 = ImageReference.parse("gcr.io/project/image:tag2"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + + private void verifyParse(String registry, String repository, String tag, String digest) throws InvalidImageReferenceException { // Gets the expected parsed components. String expectedRegistry = registry; if (Strings.isNullOrEmpty(expectedRegistry)) { - expectedRegistry = "registry.hub.docker.com"; + expectedRegistry = "registry-1.docker.io"; } String expectedRepository = repository; - if ("registry.hub.docker.com".equals(expectedRegistry) && repository.indexOf('/') < 0) { + if ("registry-1.docker.io".equals(expectedRegistry) && repository.indexOf('/') < 0) { expectedRepository = "library/" + expectedRepository; } String expectedTag = tag; - if (Strings.isNullOrEmpty(expectedTag)) { + if (Strings.isNullOrEmpty(expectedTag) && Strings.isNullOrEmpty(digest)) { expectedTag = "latest"; } + if (Strings.isNullOrEmpty(expectedTag)) { + expectedTag = null; + } + + String expectedDigest = digest; + if (Strings.isNullOrEmpty(digest)) { + expectedDigest = null; + } // Builds the image reference to parse. StringBuilder imageReferenceBuilder = new StringBuilder(); @@ -174,13 +277,17 @@ private void verifyParse(String registry, String repository, String tagSeparator } imageReferenceBuilder.append(repository); if (!Strings.isNullOrEmpty(tag)) { - imageReferenceBuilder.append(tagSeparator).append(tag); + imageReferenceBuilder.append(':').append(tag); + } + if (!Strings.isNullOrEmpty(digest)) { + imageReferenceBuilder.append('@').append(digest); } ImageReference imageReference = ImageReference.parse(imageReferenceBuilder.toString()); Assert.assertEquals(expectedRegistry, imageReference.getRegistry()); Assert.assertEquals(expectedRepository, imageReference.getRepository()); - Assert.assertEquals(expectedTag, imageReference.getTag()); + Assert.assertEquals(expectedTag, imageReference.getTag().orElse(null)); + Assert.assertEquals(expectedDigest, imageReference.getDigest().orElse(null)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java new file mode 100644 index 0000000000..a75b6209ef --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -0,0 +1,279 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.RelativeUnixPath; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link JavaContainerBuilder}. */ +public class JavaContainerBuilderTest { + + /** Gets a resource file as a {@link Path}. */ + private static Path getResource(String directory) throws URISyntaxException { + return Paths.get(Resources.getResource(directory).toURI()); + } + + /** Gets the extraction paths in the specified layer of a give {@link BuildContext}. */ + private static List getExtractionPaths( + BuildContext buildContext, String layerName) { + return buildContext.getLayerConfigurations().stream() + .filter(layerConfiguration -> layerConfiguration.getName().equals(layerName)) + .findFirst() + .map( + layerConfiguration -> + layerConfiguration.getEntries().stream() + .map(FileEntry::getExtractionPath) + .collect(Collectors.toList())) + .orElse(ImmutableList.of()); + } + + @Test + public void testToJibContainerBuilder_all() + throws InvalidImageReferenceException, URISyntaxException, IOException, + CacheDirectoryCreationException { + BuildContext buildContext = + JavaContainerBuilder.from("scratch") + .setAppRoot("/hello") + .addResources(getResource("core/application/resources")) + .addClasses(getResource("core/application/classes")) + .addDependencies( + getResource("core/application/dependencies/dependency-1.0.0.jar"), + getResource("core/application/dependencies/more/dependency-1.0.0.jar")) + .addSnapshotDependencies( + getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addProjectDependencies( + getResource("core/application/dependencies/libraryA.jar"), + getResource("core/application/dependencies/libraryB.jar")) + .addToClasspath(getResource("core/fileA"), getResource("core/fileB")) + .setClassesDestination(RelativeUnixPath.get("different-classes")) + .setResourcesDestination(RelativeUnixPath.get("different-resources")) + .setDependenciesDestination(RelativeUnixPath.get("different-libs")) + .setOthersDestination(RelativeUnixPath.get("different-classpath")) + .addJvmFlags("-xflag1", "-xflag2") + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); + + // Check entrypoint + Assert.assertEquals( + ImmutableList.of( + "java", + "-xflag1", + "-xflag2", + "-cp", + "/hello/different-resources:/hello/different-classes:/hello/different-libs/*:/hello/different-classpath", + "HelloWorld"), + buildContext.getContainerConfiguration().getEntrypoint()); + + // Check dependencies + List expectedDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-770.jar"), + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); + + // Check snapshots + List expectedSnapshotDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-SNAPSHOT.jar")); + Assert.assertEquals( + expectedSnapshotDependencies, getExtractionPaths(buildContext, "snapshot dependencies")); + + List expectedProjectDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-libs/libraryA.jar"), + AbsoluteUnixPath.get("/hello/different-libs/libraryB.jar")); + Assert.assertEquals( + expectedProjectDependencies, getExtractionPaths(buildContext, "project dependencies")); + + // Check resources + List expectedResources = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-resources/resourceA"), + AbsoluteUnixPath.get("/hello/different-resources/resourceB"), + AbsoluteUnixPath.get("/hello/different-resources/world")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildContext, "resources")); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-classes/HelloWorld.class"), + AbsoluteUnixPath.get("/hello/different-classes/some.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); + + // Check additional classpath files + List expectedOthers = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-classpath/fileA"), + AbsoluteUnixPath.get("/hello/different-classpath/fileB")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildContext, "extra files")); + } + + @Test + public void testToJibContainerBuilder_missingAndMultipleAdds() + throws InvalidImageReferenceException, URISyntaxException, IOException, + CacheDirectoryCreationException { + BuildContext buildContext = + JavaContainerBuilder.from("scratch") + .addDependencies(getResource("core/application/dependencies/libraryA.jar")) + .addDependencies(getResource("core/application/dependencies/libraryB.jar")) + .addSnapshotDependencies( + getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addClasses(getResource("core/application/classes/")) + .addClasses(getResource("core/class-finder-tests/extension")) + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); + + // Check entrypoint + Assert.assertEquals( + ImmutableList.of("java", "-cp", "/app/libs/*:/app/classes", "HelloWorld"), + buildContext.getContainerConfiguration().getEntrypoint()); + + // Check dependencies + List expectedDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/app/libs/libraryA.jar"), + AbsoluteUnixPath.get("/app/libs/libraryB.jar")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); + + // Check snapshots + List expectedSnapshotDependencies = + ImmutableList.of(AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-SNAPSHOT.jar")); + Assert.assertEquals( + expectedSnapshotDependencies, getExtractionPaths(buildContext, "snapshot dependencies")); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/app/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/some.class"), + AbsoluteUnixPath.get("/app/classes/main/"), + AbsoluteUnixPath.get("/app/classes/main/MainClass.class"), + AbsoluteUnixPath.get("/app/classes/pack/"), + AbsoluteUnixPath.get("/app/classes/pack/Apple.class"), + AbsoluteUnixPath.get("/app/classes/pack/Orange.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); + + // Check empty layers + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildContext, "resources")); + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildContext, "extra files")); + } + + @Test + public void testToJibContainerBuilder_setAppRootLate() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + BuildContext buildContext = + JavaContainerBuilder.from("scratch") + .addClasses(getResource("core/application/classes")) + .addResources(getResource("core/application/resources")) + .addDependencies(getResource("core/application/dependencies/libraryA.jar")) + .addToClasspath(getResource("core/fileA")) + .setAppRoot("/different") + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); + + // Check entrypoint + Assert.assertEquals( + ImmutableList.of( + "java", + "-cp", + "/different/classes:/different/resources:/different/libs/*:/different/classpath", + "HelloWorld"), + buildContext.getContainerConfiguration().getEntrypoint()); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/different/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/different/classes/some.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); + + // Check resources + List expectedResources = + ImmutableList.of( + AbsoluteUnixPath.get("/different/resources/resourceA"), + AbsoluteUnixPath.get("/different/resources/resourceB"), + AbsoluteUnixPath.get("/different/resources/world")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildContext, "resources")); + + // Check dependencies + List expectedDependencies = + ImmutableList.of(AbsoluteUnixPath.get("/different/libs/libraryA.jar")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); + + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); + + // Check additional classpath files + List expectedOthers = + ImmutableList.of(AbsoluteUnixPath.get("/different/classpath/fileA")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildContext, "extra files")); + } + + @Test + public void testToJibContainerBuilder_mainClassNull() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, + URISyntaxException { + BuildContext buildContext = + JavaContainerBuilder.from("scratch") + .addClasses(getResource("core/application/classes/")) + .toContainerBuilder() + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); + Assert.assertNull(buildContext.getContainerConfiguration().getEntrypoint()); + + try { + JavaContainerBuilder.from("scratch").addJvmFlags("-flag1", "-flag2").toContainerBuilder(); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "Failed to construct entrypoint on JavaContainerBuilder; jvmFlags were set, but " + + "mainClass is null. Specify the main class using " + + "JavaContainerBuilder#setMainClass(String), or consider using MainClassFinder to " + + "infer the main class.", + ex.getMessage()); + } + } + + @Test + public void testToJibContainerBuilder_classpathEmpty() + throws IOException, InvalidImageReferenceException { + try { + JavaContainerBuilder.from("scratch").setMainClass("Hello").toContainerBuilder(); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "Failed to construct entrypoint because no files were added to the JavaContainerBuilder", + ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java new file mode 100644 index 0000000000..74ed36bca1 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -0,0 +1,365 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link JibContainerBuilder}. */ +@RunWith(MockitoJUnitRunner.class) +public class JibContainerBuilderTest { + + @Spy private BuildContext.Builder spyBuildContextBuilder; + @Mock private FileEntriesLayer mockLayerConfiguration1; + @Mock private FileEntriesLayer mockLayerConfiguration2; + @Mock private CredentialRetriever mockCredentialRetriever; + @Mock private Consumer mockJibEventConsumer; + @Mock private JibEvent mockJibEvent; + + @Test + public void testToBuildContext_containerConfigurationSet() + throws InvalidImageReferenceException, CacheDirectoryCreationException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); + JibContainerBuilder jibContainerBuilder = + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) + .setPlatforms(ImmutableSet.of(new Platform("testArchitecture", "testOS"))) + .setEntrypoint(Arrays.asList("entry", "point")) + .setEnvironment(ImmutableMap.of("name", "value")) + .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) + .setLabels(ImmutableMap.of("key", "value")) + .setProgramArguments(Arrays.asList("program", "arguments")) + .setCreationTime(Instant.ofEpochMilli(1000)) + .setUser("user") + .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); + + BuildContext buildContext = + jibContainerBuilder.toBuildContext(Containerizer.to(RegistryImage.named("target/image"))); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); + Assert.assertEquals( + ImmutableSet.of(new Platform("testArchitecture", "testOS")), + containerConfiguration.getPlatforms()); + Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); + Assert.assertEquals( + ImmutableMap.of("name", "value"), containerConfiguration.getEnvironmentMap()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1234), Port.udp(5678)), containerConfiguration.getExposedPorts()); + Assert.assertEquals(ImmutableMap.of("key", "value"), containerConfiguration.getLabels()); + Assert.assertEquals( + Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + Assert.assertEquals(Instant.ofEpochMilli(1000), containerConfiguration.getCreationTime()); + Assert.assertEquals("user", containerConfiguration.getUser()); + Assert.assertEquals( + AbsoluteUnixPath.get("/working/directory"), containerConfiguration.getWorkingDirectory()); + } + + @Test + public void testToBuildContext_containerConfigurationAdd() + throws InvalidImageReferenceException, CacheDirectoryCreationException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); + JibContainerBuilder jibContainerBuilder = + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) + .addPlatform("testArchitecture", "testOS") + .setEntrypoint("entry", "point") + .setEnvironment(ImmutableMap.of("name", "value")) + .addEnvironmentVariable("environment", "variable") + .setExposedPorts(Port.tcp(1234), Port.udp(5678)) + .addExposedPort(Port.tcp(1337)) + .setLabels(ImmutableMap.of("key", "value")) + .addLabel("added", "label") + .setProgramArguments("program", "arguments"); + + BuildContext buildContext = + jibContainerBuilder.toBuildContext(Containerizer.to(RegistryImage.named("target/image"))); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); + Assert.assertEquals( + ImmutableSet.of(new Platform("testArchitecture", "testOS"), new Platform("amd64", "linux")), + containerConfiguration.getPlatforms()); + Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); + Assert.assertEquals( + ImmutableMap.of("name", "value", "environment", "variable"), + containerConfiguration.getEnvironmentMap()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1234), Port.udp(5678), Port.tcp(1337)), + containerConfiguration.getExposedPorts()); + Assert.assertEquals( + ImmutableMap.of("key", "value", "added", "label"), containerConfiguration.getLabels()); + Assert.assertEquals( + Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + Assert.assertEquals(Instant.EPOCH, containerConfiguration.getCreationTime()); + } + + @Test + public void testToBuildContext() + throws InvalidImageReferenceException, CredentialRetrievalException, + CacheDirectoryCreationException { + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); + RegistryImage targetImage = + RegistryImage.named(ImageReference.of("gcr.io", "my-project/my-app", null)) + .addCredential("username", "password"); + Containerizer containerizer = + Containerizer.to(targetImage) + .setBaseImageLayersCache(Paths.get("base/image/layers")) + .setApplicationLayersCache(Paths.get("application/layers")) + .setExecutorService(executorService) + .addEventHandler(mockJibEventConsumer) + .setAlwaysCacheBaseImage(false); + + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")) + .setCredentialRetrievers(Collections.singletonList(mockCredentialRetriever)) + .build(); + JibContainerBuilder jibContainerBuilder = + new JibContainerBuilder(baseImageConfiguration, spyBuildContextBuilder) + .setFileEntriesLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); + BuildContext buildContext = jibContainerBuilder.toBuildContext(containerizer); + + Assert.assertEquals( + spyBuildContextBuilder.build().getContainerConfiguration(), + buildContext.getContainerConfiguration()); + + Assert.assertEquals( + "base/image", buildContext.getBaseImageConfiguration().getImage().toString()); + Assert.assertEquals( + Arrays.asList(mockCredentialRetriever), + buildContext.getBaseImageConfiguration().getCredentialRetrievers()); + + Assert.assertEquals( + "gcr.io/my-project/my-app", + buildContext.getTargetImageConfiguration().getImage().toString()); + Assert.assertEquals( + 1, buildContext.getTargetImageConfiguration().getCredentialRetrievers().size()); + Assert.assertEquals( + Credential.from("username", "password"), + buildContext + .getTargetImageConfiguration() + .getCredentialRetrievers() + .get(0) + .retrieve() + .orElseThrow(AssertionError::new)); + + Assert.assertEquals(ImmutableSet.of("latest"), buildContext.getAllTargetImageTags()); + + Mockito.verify(spyBuildContextBuilder) + .setBaseImageLayersCacheDirectory(Paths.get("base/image/layers")); + Mockito.verify(spyBuildContextBuilder) + .setApplicationLayersCacheDirectory(Paths.get("application/layers")); + + Assert.assertEquals( + Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2), + buildContext.getLayerConfigurations()); + + Assert.assertSame(executorService, buildContext.getExecutorService()); + + buildContext.getEventHandlers().dispatch(mockJibEvent); + Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); + + Assert.assertEquals("jib-core", buildContext.getToolName()); + + Assert.assertSame(V22ManifestTemplate.class, buildContext.getTargetFormat()); + + Assert.assertEquals("jib-core", buildContext.getToolName()); + + // Changes jibContainerBuilder. + buildContext = + jibContainerBuilder + .setFormat(ImageFormat.OCI) + .toBuildContext( + containerizer + .withAdditionalTag("tag1") + .withAdditionalTag("tag2") + .setToolName("toolName")); + Assert.assertSame(OciManifestTemplate.class, buildContext.getTargetFormat()); + Assert.assertEquals( + ImmutableSet.of("latest", "tag1", "tag2"), buildContext.getAllTargetImageTags()); + Assert.assertEquals("toolName", buildContext.getToolName()); + Assert.assertFalse(buildContext.getAlwaysCacheBaseImage()); + } + + @Test + public void testToContainerBuildPlan_default() throws InvalidImageReferenceException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); + JibContainerBuilder containerBuilder = + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder); + + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + Assert.assertEquals("base/image", buildPlan.getBaseImage()); + Assert.assertEquals(ImmutableSet.of(new Platform("amd64", "linux")), buildPlan.getPlatforms()); + Assert.assertEquals(Instant.EPOCH, buildPlan.getCreationTime()); + Assert.assertEquals(ImageFormat.Docker, buildPlan.getFormat()); + Assert.assertEquals(Collections.emptyMap(), buildPlan.getEnvironment()); + Assert.assertEquals(Collections.emptyMap(), buildPlan.getLabels()); + Assert.assertEquals(Collections.emptySet(), buildPlan.getVolumes()); + Assert.assertEquals(Collections.emptySet(), buildPlan.getExposedPorts()); + Assert.assertNull(buildPlan.getUser()); + Assert.assertNull(buildPlan.getWorkingDirectory()); + Assert.assertNull(buildPlan.getEntrypoint()); + Assert.assertNull(buildPlan.getCmd()); + Assert.assertEquals(Collections.emptyList(), buildPlan.getLayers()); + } + + @Test + public void testToContainerBuildPlan() throws InvalidImageReferenceException, IOException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); + JibContainerBuilder containerBuilder = + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) + .setPlatforms(ImmutableSet.of(new Platform("testArchitecture", "testOS"))) + .setCreationTime(Instant.ofEpochMilli(1000)) + .setFormat(ImageFormat.OCI) + .setEnvironment(ImmutableMap.of("env", "var")) + .setLabels(ImmutableMap.of("com.example.label", "value")) + .setVolumes(AbsoluteUnixPath.get("/mnt/vol"), AbsoluteUnixPath.get("/media/data")) + .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) + .setUser("user") + .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")) + .setEntrypoint(Arrays.asList("entry", "point")) + .setProgramArguments(Arrays.asList("program", "arguments")) + .addLayer(Arrays.asList(Paths.get("/non/existing/foo")), "/into/this"); + + ContainerBuildPlan buildPlan = containerBuilder.toContainerBuildPlan(); + Assert.assertEquals("base/image", buildPlan.getBaseImage()); + Assert.assertEquals( + ImmutableSet.of(new Platform("testArchitecture", "testOS")), buildPlan.getPlatforms()); + Assert.assertEquals(Instant.ofEpochMilli(1000), buildPlan.getCreationTime()); + Assert.assertEquals(ImageFormat.OCI, buildPlan.getFormat()); + Assert.assertEquals(ImmutableMap.of("env", "var"), buildPlan.getEnvironment()); + Assert.assertEquals(ImmutableMap.of("com.example.label", "value"), buildPlan.getLabels()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/mnt/vol"), AbsoluteUnixPath.get("/media/data")), + buildPlan.getVolumes()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1234), Port.udp(5678)), buildPlan.getExposedPorts()); + Assert.assertEquals("user", buildPlan.getUser()); + Assert.assertEquals( + AbsoluteUnixPath.get("/working/directory"), buildPlan.getWorkingDirectory()); + Assert.assertEquals(Arrays.asList("entry", "point"), buildPlan.getEntrypoint()); + Assert.assertEquals(Arrays.asList("program", "arguments"), buildPlan.getCmd()); + + Assert.assertEquals(1, buildPlan.getLayers().size()); + MatcherAssert.assertThat( + buildPlan.getLayers().get(0), CoreMatchers.instanceOf(FileEntriesLayer.class)); + Assert.assertEquals( + Arrays.asList( + new FileEntry( + Paths.get("/non/existing/foo"), + AbsoluteUnixPath.get("/into/this/foo"), + FilePermissions.fromOctalString("644"), + Instant.ofEpochSecond(1))), + ((FileEntriesLayer) buildPlan.getLayers().get(0)).getEntries()); + } + + @Test + public void setApplyContainerBuildPlan() + throws InvalidImageReferenceException, CacheDirectoryCreationException { + FileEntriesLayer layer = + FileEntriesLayer.builder() + .addEntry(Paths.get("/src/file/foo"), AbsoluteUnixPath.get("/path/in/container")) + .build(); + ContainerBuildPlan buildPlan = + ContainerBuildPlan.builder() + .setBaseImage("some/base") + .setPlatforms(ImmutableSet.of(new Platform("testArchitecture", "testOS"))) + .setFormat(ImageFormat.OCI) + .setCreationTime(Instant.ofEpochMilli(30)) + .setEnvironment(ImmutableMap.of("env", "var")) + .setVolumes( + ImmutableSet.of(AbsoluteUnixPath.get("/mnt/foo"), AbsoluteUnixPath.get("/bar"))) + .setLabels(ImmutableMap.of("com.example.label", "cool")) + .setExposedPorts(ImmutableSet.of(Port.tcp(443))) + .setLayers(Arrays.asList(layer)) + .setUser(":") + .setWorkingDirectory(AbsoluteUnixPath.get("/workspace")) + .setEntrypoint(Arrays.asList("foo", "entrypoint")) + .setCmd(Arrays.asList("bar", "cmd")) + .build(); + + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("initial/base")).build(); + JibContainerBuilder containerBuilder = + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) + .applyContainerBuildPlan(buildPlan); + + BuildContext buildContext = + containerBuilder.toBuildContext(Containerizer.to(RegistryImage.named("target/image"))); + Assert.assertEquals( + "some/base", buildContext.getBaseImageConfiguration().getImage().toString()); + Assert.assertEquals(OciManifestTemplate.class, buildContext.getTargetFormat()); + Assert.assertEquals(1, buildContext.getLayerConfigurations().size()); + Assert.assertEquals(1, buildContext.getLayerConfigurations().get(0).getEntries().size()); + Assert.assertEquals( + Arrays.asList( + new FileEntry( + Paths.get("/src/file/foo"), + AbsoluteUnixPath.get("/path/in/container"), + FilePermissions.fromOctalString("644"), + Instant.ofEpochSecond(1))), + buildContext.getLayerConfigurations().get(0).getEntries()); + + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); + Assert.assertEquals(Instant.ofEpochMilli(30), containerConfiguration.getCreationTime()); + Assert.assertEquals(ImmutableMap.of("env", "var"), containerConfiguration.getEnvironmentMap()); + Assert.assertEquals( + ImmutableMap.of("com.example.label", "cool"), containerConfiguration.getLabels()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/mnt/foo"), AbsoluteUnixPath.get("/bar")), + containerConfiguration.getVolumes()); + Assert.assertEquals(ImmutableSet.of(Port.tcp(443)), containerConfiguration.getExposedPorts()); + Assert.assertEquals(":", containerConfiguration.getUser()); + Assert.assertEquals( + AbsoluteUnixPath.get("/workspace"), containerConfiguration.getWorkingDirectory()); + Assert.assertEquals(Arrays.asList("foo", "entrypoint"), containerConfiguration.getEntrypoint()); + Assert.assertEquals(Arrays.asList("bar", "cmd"), containerConfiguration.getProgramArguments()); + + ContainerBuildPlan convertedPlan = containerBuilder.toContainerBuildPlan(); + Assert.assertEquals( + ImmutableSet.of(new Platform("testArchitecture", "testOS")), convertedPlan.getPlatforms()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java new file mode 100644 index 0000000000..cb0e4ec3a8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.common.collect.ImmutableSet; +import java.security.DigestException; +import java.util.Set; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +/** Tests for {@link JibContainer}. */ +public class JibContainerTest { + + @Rule public TemporaryFolder temporaryDirectory = new TemporaryFolder(); + + private ImageReference targetImage1; + private ImageReference targetImage2; + private DescriptorDigest digest1; + private DescriptorDigest digest2; + private Set tags1; + private Set tags2; + + @Before + public void setUp() throws DigestException, InvalidImageReferenceException { + targetImage1 = ImageReference.parse("gcr.io/project/image:tag"); + targetImage2 = ImageReference.parse("gcr.io/project/image:tag2"); + digest1 = + DescriptorDigest.fromDigest( + "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"); + digest2 = + DescriptorDigest.fromDigest( + "sha256:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); + tags1 = ImmutableSet.of("latest", "custom-tag"); + tags2 = ImmutableSet.of("latest"); + } + + @Test + public void testCreation() { + JibContainer container = new JibContainer(targetImage1, digest1, digest2, tags1, true); + + Assert.assertEquals(targetImage1, container.getTargetImage()); + Assert.assertEquals(digest1, container.getDigest()); + Assert.assertEquals(digest2, container.getImageId()); + Assert.assertEquals(tags1, container.getTags()); + Assert.assertTrue(container.isImagePushed()); + } + + @Test + public void testEquality() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1, true); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest2, tags1, true); + + Assert.assertEquals(container1, container2); + Assert.assertEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentTargetImage() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1, true); + JibContainer container2 = new JibContainer(targetImage2, digest1, digest2, tags1, true); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentImageDigest() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1, true); + JibContainer container2 = new JibContainer(targetImage1, digest2, digest2, tags1, true); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentImageId() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest1, tags1, true); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest2, tags1, true); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentTags() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest1, tags1, true); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest1, tags2, true); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentImagePushed() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest1, tags1, true); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest1, tags1, false); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testCreation_withBuildContextAndBuildResult() { + BuildResult buildResult = Mockito.mock(BuildResult.class); + BuildContext buildContext = Mockito.mock(BuildContext.class); + ImageConfiguration mockTargetConfiguration = Mockito.mock(ImageConfiguration.class); + + when(buildResult.getImageDigest()).thenReturn(digest1); + when(buildResult.getImageId()).thenReturn(digest1); + when(buildResult.isImagePushed()).thenReturn(true); + when(mockTargetConfiguration.getImage()).thenReturn(targetImage1); + when(buildContext.getTargetImageConfiguration()).thenReturn(mockTargetConfiguration); + when(buildContext.getAllTargetImageTags()).thenReturn(ImmutableSet.copyOf(tags1)); + + JibContainer container = JibContainer.from(buildContext, buildResult); + Assert.assertEquals(targetImage1, container.getTargetImage()); + Assert.assertEquals(digest1, container.getDigest()); + Assert.assertEquals(digest1, container.getImageId()); + Assert.assertEquals(tags1, container.getTags()); + Assert.assertTrue(container.isImagePushed()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java new file mode 100644 index 0000000000..f61cfaf09d --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.api.MainClassFinder.Result; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Consumer; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link MainClassFinder}. */ +@RunWith(MockitoJUnitRunner.class) +public class MainClassFinderTest { + + @Mock private Consumer logEventConsumer; + + @Test + public void testFindMainClass_simple() throws URISyntaxException, IOException { + Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/simple").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); + } + + @Test + public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/subdirectories").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("multi.layered.HelloWorld")); + } + + @Test + public void testFindMainClass_noClass() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/no-main").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertEquals(Result.Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); + } + + @Test + public void testFindMainClass_multiple() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertEquals(Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); + Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); + Assert.assertTrue( + mainClassFinderResult.getFoundMainClasses().contains("multi.layered.HelloMoon")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("HelloWorld")); + } + + @Test + public void testFindMainClass_extension() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/extension").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); + } + + @Test + public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/imported-methods").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); + } + + @Test + public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/external-classes").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); + } + + @Test + public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/inner-classes").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("HelloWorld$InnerClass")); + } + + @Test + public void testMainClass_varargs() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/varargs").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame(Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); + } + + @Test + public void testMainClass_synthetic() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/synthetic").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorldKt")); + } + + @Test + public void testMainClass_java25StaticNoArgs() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("StaticMainNoArgs.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("StaticMainNoArgs")); + } + + @Test + public void testMainClass_java25InstanceWithArgs() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("InstanceMainWithArgs.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("InstanceMainWithArgs")); + } + + @Test + public void testMainClass_java25InstanceNoArgs() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("InstanceMainNoArgs.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("InstanceMainNoArgs")); + } + + @Test + public void testMainClass_java25ProtectedMain() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("ProtectedMain.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("ProtectedMain")); + } + + @Test + public void testMainClass_java25PackagePrivateMain() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("PackagePrivateMain.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + MatcherAssert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("PackagePrivateMain")); + } + + @Test + public void testMainClass_java25MultipleFlexibleMains() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); + Assert.assertEquals(Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); + Assert.assertEquals(5, mainClassFinderResult.getFoundMainClasses().size()); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("StaticMainNoArgs")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("InstanceMainWithArgs")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("InstanceMainNoArgs")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("ProtectedMain")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("PackagePrivateMain")); + } + + @Test + public void testMainClass_java25PrivateMainNotAllowed() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI()); + Path classFile = rootDirectory.resolve("PrivateMain.class"); + MainClassFinder.Result mainClassFinderResult = + MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer); + Assert.assertSame( + MainClassFinder.Result.Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java similarity index 58% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java index eb31b03b26..725ab27fb9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,53 +14,46 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link ExposedPortsParser}. */ -@RunWith(MockitoJUnitRunner.class) -public class ExposedPortsParserTest { - - @Mock private BuildLogger mockLogger; +/** Tests for {@link Ports}. */ +public class PortsTest { @Test public void testParse() { List goodInputs = Arrays.asList("1000", "2000-2003", "3000-3000", "4000/tcp", "5000/udp", "6000-6002/udp"); - ImmutableList expected = - new ImmutableList.Builder() + ImmutableSet expected = + new ImmutableSet.Builder() .add( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(2001, Protocol.TCP), - new Port(2002, Protocol.TCP), - new Port(2003, Protocol.TCP), - new Port(3000, Protocol.TCP), - new Port(4000, Protocol.TCP), - new Port(5000, Protocol.UDP), - new Port(6000, Protocol.UDP), - new Port(6001, Protocol.UDP), - new Port(6002, Protocol.UDP)) + Port.tcp(1000), + Port.tcp(2000), + Port.tcp(2001), + Port.tcp(2002), + Port.tcp(2003), + Port.tcp(3000), + Port.tcp(4000), + Port.udp(5000), + Port.udp(6000), + Port.udp(6001), + Port.udp(6002)) .build(); - ImmutableList result = ExposedPortsParser.parse(goodInputs); + Set result = Ports.parse(goodInputs); Assert.assertEquals(expected, result); List badInputs = Arrays.asList("abc", "/udp", "1000/abc", "a100/tcp", "20/udpabc"); for (String input : badInputs) { try { - ExposedPortsParser.parse(Collections.singletonList(input)); + Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( @@ -74,7 +67,7 @@ public void testParse() { } try { - ExposedPortsParser.parse(Collections.singletonList("4002-4000")); + Ports.parse(Collections.singletonList("4002-4000")); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( @@ -84,7 +77,7 @@ public void testParse() { badInputs = Arrays.asList("0", "70000", "0-400", "1-70000"); for (String input : badInputs) { try { - ExposedPortsParser.parse(Collections.singletonList(input)); + Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java new file mode 100644 index 0000000000..98c894c2d2 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link RegistryImage}. */ +@RunWith(MockitoJUnitRunner.class) +public class RegistryImageTest { + + @Mock private CredentialRetriever mockCredentialRetriever; + + @Test + public void testGetters_default() throws InvalidImageReferenceException { + RegistryImage image = RegistryImage.named("registry/image"); + + Assert.assertEquals("registry/image", image.getImageReference().toString()); + Assert.assertEquals(0, image.getCredentialRetrievers().size()); + } + + @Test + public void testGetters() + throws InvalidImageReferenceException, AssertionError, CredentialRetrievalException { + RegistryImage image = + RegistryImage.named("registry/image") + .addCredentialRetriever(mockCredentialRetriever) + .addCredential("username", "password"); + + Assert.assertEquals(2, image.getCredentialRetrievers().size()); + Assert.assertSame(mockCredentialRetriever, image.getCredentialRetrievers().get(0)); + Assert.assertEquals( + Credential.from("username", "password"), + image.getCredentialRetrievers().get(1).retrieve().get()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java new file mode 100644 index 0000000000..834ee60316 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link TarImage}. */ +public class TarImageTest { + + @Test + public void testGetters_bothSet() throws InvalidImageReferenceException { + TarImage tarImage = TarImage.at(Paths.get("output/file")).named("tar/image"); + Assert.assertEquals("tar/image", tarImage.getImageReference().get().toString()); + Assert.assertEquals(Paths.get("output/file"), tarImage.getPath()); + } + + @Test + public void testGetters_nameMissing() { + TarImage tarImage = TarImage.at(Paths.get("output/file")); + Assert.assertFalse(tarImage.getImageReference().isPresent()); + Assert.assertEquals(Paths.get("output/file"), tarImage.getPath()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index 8023446b22..5098214fe9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.hash.WritableContents; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -31,7 +32,6 @@ import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; /** Tests for {@link Blob}. */ public class BlobTest { @@ -45,7 +45,7 @@ public void testFromInputStream() throws IOException { @Test public void testFromFile() throws IOException, URISyntaxException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); + Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); String expected = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); verifyBlobWriteTo(expected, Blobs.from(fileA)); } @@ -57,13 +57,13 @@ public void testFromString() throws IOException { } @Test - public void testFromBlobWriter() throws IOException { + public void testFromWritableContents() throws IOException { String expected = "crepecake"; - BlobWriter writer = + WritableContents writableContents = outputStream -> outputStream.write(expected.getBytes(StandardCharsets.UTF_8)); - verifyBlobWriteTo(expected, Blobs.from(writer)); + verifyBlobWriteTo(expected, Blobs.from(writableContents, false)); } /** Checks that the {@link Blob} streams the expected string. */ @@ -77,10 +77,8 @@ private void verifyBlobWriteTo(String expected, Blob blob) throws IOException { byte[] expectedBytes = expected.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(expectedBytes.length, blobDescriptor.getSize()); - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(Mockito.mock(OutputStream.class)); - countingDigestOutputStream.write(expectedBytes); - DescriptorDigest expectedDigest = countingDigestOutputStream.toBlobDescriptor().getDigest(); + DescriptorDigest expectedDigest = + Digests.computeDigest(new ByteArrayInputStream(expectedBytes)).getDigest(); Assert.assertEquals(expectedDigest, blobDescriptor.getDigest()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java deleted file mode 100644 index cd463a845a..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -public class BuildConfigurationTest { - - @Test - public void testBuilder() { - String expectedBaseImageServerUrl = "someserver"; - String expectedBaseImageName = "baseimage"; - String expectedBaseImageTag = "baseimagetag"; - String expectedBaseImageCredentialHelperName = "credentialhelper"; - RegistryCredentials expectedKnownBaseRegistryCredentials = - Mockito.mock(RegistryCredentials.class); - String expectedTargetServerUrl = "someotherserver"; - String expectedTargetImageName = "targetimage"; - String expectedTargetTag = "targettag"; - String expectedTargetImageCredentialHelperName = "anotherCredentialHelper"; - RegistryCredentials expectedKnownTargetRegistryCredentials = - Mockito.mock(RegistryCredentials.class); - String expectedMainClass = "mainclass"; - List expectedJavaArguments = Arrays.asList("arg1", "arg2"); - List expectedJvmFlags = Arrays.asList("some", "jvm", "flags"); - Map expectedEnvironment = ImmutableMap.of("key", "value"); - ImmutableList expectedExposedPorts = - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP)); - Class expectedTargetFormat = OCIManifestTemplate.class; - CacheConfiguration expectedApplicationLayersCacheConfiguration = - CacheConfiguration.forPath(Paths.get("application/layers")); - CacheConfiguration expectedBaseImageLayersCacheConfiguration = - CacheConfiguration.forPath(Paths.get("base/image/layers")); - LayerConfiguration expectedExtraFilesLayerConfiguration = - LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build(); - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage( - ImageReference.of( - expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) - .setBaseImageCredentialHelperName(expectedBaseImageCredentialHelperName) - .setKnownBaseRegistryCredentials(expectedKnownBaseRegistryCredentials) - .setTargetImage( - ImageReference.of( - expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) - .setTargetImageCredentialHelperName(expectedTargetImageCredentialHelperName) - .setKnownTargetRegistryCredentials(expectedKnownTargetRegistryCredentials) - .setMainClass(expectedMainClass) - .setJavaArguments(expectedJavaArguments) - .setJvmFlags(expectedJvmFlags) - .setEnvironment(expectedEnvironment) - .setExposedPorts(expectedExposedPorts) - .setTargetFormat(OCIManifestTemplate.class) - .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) - .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) - .setAllowHttp(true) - .setExtraFilesLayerConfiguration(expectedExtraFilesLayerConfiguration); - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - Assert.assertEquals(expectedBaseImageServerUrl, buildConfiguration.getBaseImageRegistry()); - Assert.assertEquals(expectedBaseImageName, buildConfiguration.getBaseImageRepository()); - Assert.assertEquals(expectedBaseImageTag, buildConfiguration.getBaseImageTag()); - Assert.assertEquals( - expectedBaseImageCredentialHelperName, - buildConfiguration.getBaseImageCredentialHelperName()); - Assert.assertEquals(expectedTargetServerUrl, buildConfiguration.getTargetImageRegistry()); - Assert.assertEquals(expectedTargetImageName, buildConfiguration.getTargetImageRepository()); - Assert.assertEquals(expectedTargetTag, buildConfiguration.getTargetImageTag()); - Assert.assertEquals( - expectedTargetImageCredentialHelperName, - buildConfiguration.getTargetImageCredentialHelperName()); - Assert.assertEquals(expectedMainClass, buildConfiguration.getMainClass()); - Assert.assertEquals(expectedJavaArguments, buildConfiguration.getJavaArguments()); - Assert.assertEquals(expectedJvmFlags, buildConfiguration.getJvmFlags()); - Assert.assertEquals(expectedEnvironment, buildConfiguration.getEnvironment()); - Assert.assertEquals(expectedExposedPorts, buildConfiguration.getExposedPorts()); - Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); - Assert.assertEquals( - expectedApplicationLayersCacheConfiguration, - buildConfiguration.getApplicationLayersCacheConfiguration()); - Assert.assertEquals( - expectedBaseImageLayersCacheConfiguration, - buildConfiguration.getBaseImageLayersCacheConfiguration()); - Assert.assertTrue(buildConfiguration.getAllowHttp()); - Assert.assertEquals( - expectedExtraFilesLayerConfiguration, buildConfiguration.getExtraFilesLayerConfiguration()); - } - - @Test - public void testBuilder_default() { - // These are required and don't have defaults. - String expectedBaseImageServerUrl = "someserver"; - String expectedBaseImageName = "baseimage"; - String expectedBaseImageTag = "baseimagetag"; - String expectedTargetServerUrl = "someotherserver"; - String expectedTargetImageName = "targetimage"; - String expectedTargetTag = "targettag"; - String expectedMainClass = "mainclass"; - - BuildConfiguration buildConfiguration = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage( - ImageReference.of( - expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) - .setTargetImage( - ImageReference.of( - expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) - .setMainClass(expectedMainClass) - .build(); - - Assert.assertNull(buildConfiguration.getBaseImageCredentialHelperName()); - Assert.assertNull(buildConfiguration.getKnownBaseRegistryCredentials()); - Assert.assertNull(buildConfiguration.getTargetImageCredentialHelperName()); - Assert.assertNull(buildConfiguration.getKnownTargetRegistryCredentials()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getJavaArguments()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getJvmFlags()); - Assert.assertEquals(Collections.emptyMap(), buildConfiguration.getEnvironment()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getExposedPorts()); - Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); - Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); - Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); - Assert.assertFalse(buildConfiguration.getAllowHttp()); - Assert.assertNull(buildConfiguration.getExtraFilesLayerConfiguration()); - } - - @Test - public void testBuilder_missingValues() { - // Main class is missing - try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage(Mockito.mock(ImageReference.class)) - .setTargetImage(Mockito.mock(ImageReference.class)) - .build(); - Assert.fail("Build configuration should not be built with missing values"); - - } catch (IllegalStateException ex) { - Assert.assertEquals("main class is required but not set", ex.getMessage()); - } - - // Main class and target image are missing - try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage(Mockito.mock(ImageReference.class)) - .build(); - Assert.fail("Build configuration should not be built with missing values"); - - } catch (IllegalStateException ex) { - Assert.assertEquals( - "target image is required but not set and main class is required but not set", - ex.getMessage()); - } - - // All required fields missing - try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).build(); - Assert.fail("Build configuration should not be built with missing values"); - - } catch (IllegalStateException ex) { - Assert.assertEquals( - "base image is required but not set, target image is required but not set, and main class is required but not set", - ex.getMessage()); - } - } - - @Test - public void testValidJavaClassRegex() { - Assert.assertTrue(BuildConfiguration.isValidJavaClass("my.Class")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("my.java_Class$valid")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("multiple.package.items")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("is123.valid")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("${start-class}")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("123not.Valid")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("{class}")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("not valid")); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java deleted file mode 100644 index f6da756da0..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import java.util.Arrays; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -/** Tests for {@link EntrypointBuilder}. */ -public class EntrypointBuilderTest { - - @Test - public void testMakeEntrypoint() { - String expectedDependenciesPath = "/app/libs/"; - String expectedResourcesPath = "/app/resources/"; - String expectedClassesPath = "/app/classes/"; - List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag"); - String expectedMainClass = "SomeMainClass"; - - SourceFilesConfiguration mockSourceFilesConfiguration = - Mockito.mock(SourceFilesConfiguration.class); - - Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) - .thenReturn(expectedDependenciesPath); - Mockito.when(mockSourceFilesConfiguration.getResourcesPathOnImage()) - .thenReturn(expectedResourcesPath); - Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) - .thenReturn(expectedClassesPath); - - Assert.assertEquals( - Arrays.asList( - "java", - "-flag", - "anotherFlag", - "-cp", - "/app/libs/*:/app/resources/:/app/classes/", - "SomeMainClass"), - EntrypointBuilder.makeEntrypoint( - mockSourceFilesConfiguration, expectedJvmFlags, expectedMainClass)); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java new file mode 100644 index 0000000000..18faf049d4 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ProgressEventDispatcher}. */ +@RunWith(MockitoJUnitRunner.class) +public class ProgressEventDispatcherTest { + + @Mock private EventHandlers mockEventHandlers; + + @Test + public void testDispatch() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 10); + ProgressEventDispatcher ignored = + progressEventDispatcher.newChildProducer().create("ignored", 20)) { + // empty + } + + ArgumentCaptor progressEventArgumentCaptor = + ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventHandlers, Mockito.times(4)) + .dispatch(progressEventArgumentCaptor.capture()); + List progressEvents = progressEventArgumentCaptor.getAllValues(); + + Assert.assertSame(progressEvents.get(0).getAllocation(), progressEvents.get(3).getAllocation()); + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(2).getAllocation()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); + Assert.assertEquals(0, progressEvents.get(1).getUnits()); + Assert.assertEquals(20, progressEvents.get(2).getUnits()); + Assert.assertEquals(9, progressEvents.get(3).getUnits()); + } + + @Test + public void testDispatch_safeWithtooMuchProgress() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventHandlers, "allocation description", 10)) { + progressEventDispatcher.dispatchProgress(6); + progressEventDispatcher.dispatchProgress(8); + progressEventDispatcher.dispatchProgress(1); + } + + ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventHandlers, Mockito.times(4)).dispatch(eventsCaptor.capture()); + List progressEvents = eventsCaptor.getAllValues(); + + Assert.assertSame(progressEvents.get(0).getAllocation(), progressEvents.get(1).getAllocation()); + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(2).getAllocation()); + Assert.assertSame(progressEvents.get(2).getAllocation(), progressEvents.get(3).getAllocation()); + + Assert.assertEquals(10, progressEvents.get(0).getAllocation().getAllocationUnits()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); + Assert.assertEquals(6, progressEvents.get(1).getUnits()); + Assert.assertEquals(4, progressEvents.get(2).getUnits()); + Assert.assertEquals(0, progressEvents.get(3).getUnits()); + } + + @Test + public void testDispatch_safeWithTooManyChildren() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventHandlers, "allocation description", 1); + ProgressEventDispatcher ignored1 = + progressEventDispatcher.newChildProducer().create("ignored", 5); + ProgressEventDispatcher ignored2 = + progressEventDispatcher.newChildProducer().create("ignored", 4)) { + // empty + } + + ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventHandlers, Mockito.times(5)).dispatch(eventsCaptor.capture()); + List progressEvents = eventsCaptor.getAllValues(); + + Assert.assertEquals(1, progressEvents.get(0).getAllocation().getAllocationUnits()); + Assert.assertEquals(5, progressEvents.get(1).getAllocation().getAllocationUnits()); + Assert.assertEquals(4, progressEvents.get(2).getAllocation().getAllocationUnits()); + + // child1 (of allocation 5) opening and closing + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(4).getAllocation()); + // child1 (of allocation 4) opening and closing + Assert.assertSame(progressEvents.get(2).getAllocation(), progressEvents.get(3).getAllocation()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); // 0-progress sent when root creation + Assert.assertEquals(0, progressEvents.get(1).getUnits()); // when child1 creation + Assert.assertEquals(0, progressEvents.get(2).getUnits()); // when child2 creation + Assert.assertEquals(4, progressEvents.get(3).getUnits()); // when child2 closes + Assert.assertEquals(5, progressEvents.get(4).getUnits()); // when child1 closes + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java deleted file mode 100644 index 45720a5df1..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Implementation of {@link BuildLogger} for testing purposes. */ -public class TestBuildLogger implements BuildLogger { - - private static final Logger LOGGER = LoggerFactory.getLogger(TestBuildLogger.class); - - @Override - public void debug(CharSequence message) { - LOGGER.debug(message.toString()); - } - - @Override - public void info(CharSequence message) { - LOGGER.info(message.toString()); - } - - @Override - public void warn(CharSequence message) { - LOGGER.warn(message.toString()); - } - - @Override - public void error(CharSequence message) { - LOGGER.error(message.toString()); - } - - @Override - public void lifecycle(CharSequence message) { - info(message); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java deleted file mode 100644 index 49d27ce489..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; - -/** Implementation of {@link SourceFilesConfiguration} that uses test resources. */ -public class TestSourceFilesConfiguration implements SourceFilesConfiguration { - - private static final String EXTRACTION_PATH = "/some/extraction/path/"; - - private final ImmutableList dependenciesSourceFiles; - private final ImmutableList resourcesSourceFiles; - private final ImmutableList classesSourceFiles; - - public TestSourceFilesConfiguration() throws URISyntaxException, IOException { - dependenciesSourceFiles = getFilesList("application/dependencies"); - resourcesSourceFiles = getFilesList("application/resources"); - classesSourceFiles = getFilesList("application/classes"); - } - - @Override - public ImmutableList getDependenciesFiles() { - return dependenciesSourceFiles; - } - - @Override - public ImmutableList getResourcesFiles() { - return resourcesSourceFiles; - } - - @Override - public ImmutableList getClassesFiles() { - return classesSourceFiles; - } - - @Override - public String getDependenciesPathOnImage() { - return EXTRACTION_PATH + "libs/"; - } - - @Override - public String getResourcesPathOnImage() { - return EXTRACTION_PATH + "resources/"; - } - - @Override - public String getClassesPathOnImage() { - return EXTRACTION_PATH + "classes/"; - } - - /** Lists the files in the {@code resourcePath} resources directory. */ - private ImmutableList getFilesList(String resourcePath) - throws URISyntaxException, IOException { - try (Stream fileStream = - Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { - return fileStream.collect(ImmutableList.toImmutableList()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java new file mode 100644 index 0000000000..b19ec8eca3 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Deque; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link TimerEventDispatcher}. */ +@RunWith(MockitoJUnitRunner.class) +public class TimerEventDispatcherTest { + + private final Deque timerEventQueue = new ArrayDeque<>(); + + @Mock private Clock mockClock; + + @Test + public void testLogging() { + EventHandlers eventHandlers = + EventHandlers.builder().add(TimerEvent.class, timerEventQueue::add).build(); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); + try (TimerEventDispatcher parentTimerEventDispatcher = + new TimerEventDispatcher(eventHandlers, "description", mockClock, null)) { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1)); + parentTimerEventDispatcher.lap(); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1).plusNanos(1)); + try (TimerEventDispatcher ignored = + parentTimerEventDispatcher.subTimer("child description")) { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(2)); + // Laps on close. + } + } + + TimerEvent timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStartState(timerEvent); + verifyDescription(timerEvent, "description"); + + TimerEvent.Timer parentTimer = timerEvent.getTimer(); + + timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStateFirstLap(timerEvent, State.LAP); + verifyDescription(timerEvent, "description"); + + timerEvent = getNextTimerEvent(); + verifyParent(timerEvent, parentTimer); + verifyStartState(timerEvent); + verifyDescription(timerEvent, "child description"); + + timerEvent = getNextTimerEvent(); + verifyParent(timerEvent, parentTimer); + verifyStateFirstLap(timerEvent, State.FINISHED); + verifyDescription(timerEvent, "child description"); + + timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStateNotFirstLap(timerEvent, State.FINISHED); + verifyDescription(timerEvent, "description"); + + Assert.assertTrue(timerEventQueue.isEmpty()); + } + + /** + * Verifies that the {@code timerEvent}'s timer has no parent. + * + * @param timerEvent the {@link TimerEvent} to verify + */ + private void verifyNoParent(TimerEvent timerEvent) { + Assert.assertFalse(timerEvent.getTimer().getParent().isPresent()); + } + + /** + * Verifies that the {@code timerEvent}'s timer has parent {@code expectedParentTimer}. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedParentTimer the expected parent timer + */ + private void verifyParent(TimerEvent timerEvent, TimerEvent.Timer expectedParentTimer) { + Assert.assertTrue(timerEvent.getTimer().getParent().isPresent()); + Assert.assertSame(expectedParentTimer, timerEvent.getTimer().getParent().get()); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@link State#START}. + * + * @param timerEvent the {@link TimerEvent} to verify + */ + private void verifyStartState(TimerEvent timerEvent) { + Assert.assertEquals(State.START, timerEvent.getState()); + Assert.assertEquals(Duration.ZERO, timerEvent.getDuration()); + Assert.assertEquals(Duration.ZERO, timerEvent.getElapsed()); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@code expectedState} and that this is the + * first lap for the timer. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedState the expected {@link State} + */ + private void verifyStateFirstLap(TimerEvent timerEvent, State expectedState) { + Assert.assertEquals(expectedState, timerEvent.getState()); + Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); + Assert.assertEquals(0, timerEvent.getElapsed().compareTo(timerEvent.getDuration())); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@code expectedState} and that this is not the + * first lap for the timer. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedState the expected {@link State} + */ + private void verifyStateNotFirstLap(TimerEvent timerEvent, State expectedState) { + Assert.assertEquals(expectedState, timerEvent.getState()); + Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); + Assert.assertTrue(timerEvent.getElapsed().compareTo(timerEvent.getDuration()) > 0); + } + + /** + * Verifies that the {@code timerEvent}'s description is {@code expectedDescription}. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedDescription the expected description + */ + private void verifyDescription(TimerEvent timerEvent, String expectedDescription) { + Assert.assertEquals(expectedDescription, timerEvent.getDescription()); + } + + /** + * Gets the next {@link TimerEvent} on the {@link #timerEventQueue}. + * + * @return the next {@link TimerEvent} + */ + private TimerEvent getNextTimerEvent() { + TimerEvent timerEvent = timerEventQueue.poll(); + Assert.assertNotNull(timerEvent); + return timerEvent; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java new file mode 100644 index 0000000000..46afc78da8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link Timer}. */ +@RunWith(MockitoJUnitRunner.class) +public class TimerTest { + + @Mock private Clock mockClock; + + @Test + public void testLap() { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); + Timer parentTimer = new Timer(mockClock, null); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(5)); + Duration parentDuration1 = parentTimer.lap(); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(15)); + Duration parentDuration2 = parentTimer.lap(); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16)); + Timer childTimer = new Timer(mockClock, parentTimer); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16).plusNanos(1)); + Duration childDuration = childTimer.lap(); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16).plusNanos(2)); + Duration parentDuration3 = parentTimer.lap(); + + Assert.assertTrue(parentDuration2.compareTo(parentDuration1) > 0); + Assert.assertTrue(parentDuration1.compareTo(parentDuration3) > 0); + Assert.assertTrue(parentDuration3.compareTo(childDuration) > 0); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 0c2049f855..df8ecf2152 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,27 +16,32 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.TestBuildLogger; -import com.google.cloud.tools.jib.builder.TestSourceFilesConfiguration; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.CacheReader; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; -import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.ExecutionException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -49,96 +54,196 @@ @RunWith(MockitoJUnitRunner.class) public class BuildAndCacheApplicationLayerStepTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + // TODO: Consolidate with BuildStepsIntegrationTest. + private static final AbsoluteUnixPath EXTRACTION_PATH_ROOT = + AbsoluteUnixPath.get("/some/extraction/path/"); - @Mock private BuildConfiguration mockBuildConfiguration; + private static final AbsoluteUnixPath EXTRA_FILES_LAYER_EXTRACTION_PATH = + AbsoluteUnixPath.get("/extra"); + + /** + * Lists the files in the {@code resourcePath} resources directory and creates a {@link + * FileEntriesLayer} with entries from those files. + */ + private static FileEntriesLayer makeLayerConfiguration( + String resourcePath, AbsoluteUnixPath extractionPath) throws URISyntaxException, IOException { + try (Stream fileStream = + Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { + FileEntriesLayer.Builder layerConfigurationBuilder = FileEntriesLayer.builder(); + fileStream.forEach( + sourceFile -> + layerConfigurationBuilder.addEntry( + sourceFile, extractionPath.resolve(sourceFile.getFileName()))); + return layerConfigurationBuilder.build(); + } + } + + private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOException { + Assert.assertArrayEquals(Blobs.writeToByteArray(expectedBlob), Blobs.writeToByteArray(blob)); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private BuildContext mockBuildContext; + + private Cache cache; + + private FileEntriesLayer fakeDependenciesLayerConfiguration; + private FileEntriesLayer fakeSnapshotDependenciesLayerConfiguration; + private FileEntriesLayer fakeResourcesLayerConfiguration; + private FileEntriesLayer fakeClassesLayerConfiguration; + private FileEntriesLayer fakeExtraFilesLayerConfiguration; + private FileEntriesLayer emptyLayerConfiguration; + + @Before + public void setUp() throws IOException, URISyntaxException, CacheDirectoryCreationException { + fakeDependenciesLayerConfiguration = + makeLayerConfiguration( + "core/application/dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); + fakeSnapshotDependenciesLayerConfiguration = + makeLayerConfiguration( + "core/application/snapshot-dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); + fakeResourcesLayerConfiguration = + makeLayerConfiguration( + "core/application/resources", EXTRACTION_PATH_ROOT.resolve("resources")); + fakeClassesLayerConfiguration = + makeLayerConfiguration("core/application/classes", EXTRACTION_PATH_ROOT.resolve("classes")); + fakeExtraFilesLayerConfiguration = + FileEntriesLayer.builder() + .addEntry( + Paths.get(Resources.getResource("core/fileA").toURI()), + EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileA")) + .addEntry( + Paths.get(Resources.getResource("core/fileB").toURI()), + EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) + .build(); + emptyLayerConfiguration = FileEntriesLayer.builder().build(); + + cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + Mockito.when(mockBuildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(mockBuildContext.getApplicationLayersCache()).thenReturn(cache); + } + + private List buildFakeLayersToCache() + throws LayerPropertyNotFoundException, IOException, CacheCorruptedException { + List applicationLayers = new ArrayList<>(); + + ImmutableList buildAndCacheApplicationLayerSteps = + BuildAndCacheApplicationLayerStep.makeList( + mockBuildContext, + ProgressEventDispatcher.newRoot(EventHandlers.NONE, "ignored", 1).newChildProducer()); + + for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : + buildAndCacheApplicationLayerSteps) { + applicationLayers.add(buildAndCacheApplicationLayerStep.call()); + } + + return applicationLayers; + } @Test public void testRun() - throws LayerPropertyNotFoundException, IOException, CacheMetadataCorruptedException, - URISyntaxException, ExecutionException { - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestBuildLogger()); - TestSourceFilesConfiguration testSourceFilesConfiguration = new TestSourceFilesConfiguration(); - Path temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); - - // Adds an extra file layer. - ImmutableList extraFilesLayerSourceFiles = + throws LayerPropertyNotFoundException, IOException, CacheCorruptedException { + ImmutableList fakeLayerConfigurations = ImmutableList.of( - Paths.get(Resources.getResource("fileA").toURI()), - Paths.get(Resources.getResource("fileB").toURI())); - String extraFilesLayerExtractionPath = "/extra"; - Mockito.when(mockBuildConfiguration.getExtraFilesLayerConfiguration()) - .thenReturn( - LayerConfiguration.builder() - .addEntry(extraFilesLayerSourceFiles, extraFilesLayerExtractionPath) - .build()); - - ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); - ImageLayers applicationLayers; - - try (Cache cache = Cache.init(temporaryCacheDirectory)) { - ImmutableList buildAndCacheApplicationLayerSteps = - BuildAndCacheApplicationLayerStep.makeList( - MoreExecutors.newDirectExecutorService(), - mockBuildConfiguration, - testSourceFilesConfiguration, - cache); - - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - applicationLayersBuilder.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); - } - - applicationLayers = applicationLayersBuilder.build(); - cache.addCachedLayersWithMetadataToMetadata(applicationLayers.getLayers()); - Assert.assertEquals(4, applicationLayers.size()); - } + fakeDependenciesLayerConfiguration, + fakeSnapshotDependenciesLayerConfiguration, + fakeResourcesLayerConfiguration, + fakeClassesLayerConfiguration, + fakeExtraFilesLayerConfiguration); + Mockito.when(mockBuildContext.getLayerConfigurations()).thenReturn(fakeLayerConfigurations); - // Re-initialize cache with the updated metadata. - Cache cache = Cache.init(temporaryCacheDirectory); + // Populates the cache. + List applicationLayers = buildFakeLayersToCache(); + Assert.assertEquals(5, applicationLayers.size()); - ImmutableList dependenciesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getDependenciesFiles(), - testSourceFilesConfiguration.getDependenciesPathOnImage())); - ImmutableList resourcesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getResourcesFiles(), - testSourceFilesConfiguration.getResourcesPathOnImage())); - ImmutableList classesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getClassesFiles(), - testSourceFilesConfiguration.getClassesPathOnImage())); - ImmutableList extraFilesLayerEntry = - ImmutableList.of(new LayerEntry(extraFilesLayerSourceFiles, extraFilesLayerExtractionPath)); + ImmutableList dependenciesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(0).getEntries()); + ImmutableList snapshotDependenciesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(1).getEntries()); + ImmutableList resourcesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(2).getEntries()); + ImmutableList classesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(3).getEntries()); + ImmutableList extraFilesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(4).getEntries()); + + CachedLayer dependenciesCachedLayer = + cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer snapshotDependenciesCachedLayer = + cache.retrieve(snapshotDependenciesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer resourcesCachedLayer = + cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer classesCachedLayer = + cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer extraFilesCachedLayer = + cache.retrieve(extraFilesLayerEntries).orElseThrow(AssertionError::new); // Verifies that the cached layers are up-to-date. - CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( - applicationLayers.get(0).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntry).getBlobDescriptor()); + applicationLayers.get(0).getBlobDescriptor().getDigest(), + dependenciesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(1).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntry).getBlobDescriptor()); + applicationLayers.get(1).getBlobDescriptor().getDigest(), + snapshotDependenciesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(2).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntry).getBlobDescriptor()); + applicationLayers.get(2).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(3).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(extraFilesLayerEntry).getBlobDescriptor()); + applicationLayers.get(3).getBlobDescriptor().getDigest(), classesCachedLayer.getDigest()); + Assert.assertEquals( + applicationLayers.get(4).getBlobDescriptor().getDigest(), + extraFilesCachedLayer.getDigest()); // Verifies that the cache reader gets the same layers as the newest application layers. + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(1).getBlob(), snapshotDependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), resourcesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(3).getBlob(), classesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(4).getBlob(), extraFilesCachedLayer.getBlob()); + } + + @Test + public void testRun_emptyLayersIgnored() throws IOException, CacheCorruptedException { + ImmutableList fakeLayerConfigurations = + ImmutableList.of( + fakeDependenciesLayerConfiguration, + emptyLayerConfiguration, + fakeResourcesLayerConfiguration, + fakeClassesLayerConfiguration, + emptyLayerConfiguration); + Mockito.when(mockBuildContext.getLayerConfigurations()).thenReturn(fakeLayerConfigurations); + + // Populates the cache. + List applicationLayers = buildFakeLayersToCache(); + Assert.assertEquals(3, applicationLayers.size()); + + ImmutableList dependenciesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(0).getEntries()); + ImmutableList resourcesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(2).getEntries()); + ImmutableList classesLayerEntries = + ImmutableList.copyOf(fakeLayerConfigurations.get(3).getEntries()); + + CachedLayer dependenciesCachedLayer = + cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer resourcesCachedLayer = + cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); + CachedLayer classesCachedLayer = + cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); + + // Verifies that the cached layers are up-to-date. Assert.assertEquals( - applicationLayers.get(0).getContentFile(), - cacheReader.getLayerFile(dependenciesLayerEntry)); - Assert.assertEquals( - applicationLayers.get(1).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntry)); + applicationLayers.get(0).getBlobDescriptor().getDigest(), + dependenciesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(classesLayerEntry)); + applicationLayers.get(1).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(3).getContentFile(), cacheReader.getLayerFile(extraFilesLayerEntry)); + applicationLayers.get(2).getBlobDescriptor().getDigest(), classesCachedLayer.getDigest()); + + // Verifies that the cache reader gets the same layers as the newest application layers. + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(1).getBlob(), resourcesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), classesCachedLayer.getBlob()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 3ae274f153..b996946a0a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,21 +16,27 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.MoreExecutors; -import java.nio.file.Paths; +import com.google.common.collect.ImmutableSet; import java.security.DigestException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -43,57 +49,354 @@ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { - @Mock private BuildConfiguration mockBuildConfiguration; - @Mock private BuildLogger mockBuildLogger; - @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; - @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStep; + @Mock private ProgressEventDispatcher.Factory mockProgressEventDispatcherFactory; + @Mock private BuildContext mockBuildContext; + @Mock private ContainerConfiguration mockContainerConfiguration; + @Mock private CachedLayer mockCachedLayer; + + private Image baseImage; + private List baseImageLayers; + private List applicationLayers; private DescriptorDigest testDescriptorDigest; + private HistoryEntry nonEmptyLayerHistory; + private HistoryEntry emptyLayerHistory; @Before public void setUp() throws DigestException { testDescriptorDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - CachedLayerWithMetadata testCachedLayer = - new CachedLayerWithMetadata( - new CachedLayer( - Paths.get(""), new BlobDescriptor(testDescriptorDigest), testDescriptorDigest), - null); - - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockBuildConfiguration.getEnvironment()).thenReturn(ImmutableMap.of()); - Mockito.when(mockBuildConfiguration.getJavaArguments()).thenReturn(ImmutableList.of()); - Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); - - Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) + + Mockito.when(mockBuildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(mockBuildContext.getContainerConfiguration()) + .thenReturn(mockContainerConfiguration); + Mockito.when(mockBuildContext.getToolName()).thenReturn("jib"); + Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); + Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableSet.of()); + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getUser()).thenReturn("root"); + Mockito.when(mockCachedLayer.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(0, testDescriptorDigest)); + + nonEmptyLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("JibBase") + .setCreatedBy("jib-test") + .build(); + emptyLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("JibBase") + .setCreatedBy("jib-test") + .setEmptyLayer(true) + .build(); + + baseImage = + Image.builder(V22ManifestTemplate.class) + .setArchitecture("wasm") + .setOs("js") + .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "BASE_ENV_2", "DEFAULT")) + .addLabel("base.label", "base.label.value") + .addLabel("base.label.2", "default") + .setUser("base:user") + .setWorkingDirectory("/base/working/directory") + .setEntrypoint(ImmutableList.of("baseImageEntrypoint")) + .setProgramArguments(ImmutableList.of("catalina.sh", "run")) + .setHealthCheck( + DockerHealthCheck.fromCommand(ImmutableList.of("CMD-SHELL", "echo hi")) + .setInterval(Duration.ofSeconds(3)) + .setTimeout(Duration.ofSeconds(2)) + .setStartPeriod(Duration.ofSeconds(1)) + .setRetries(20) + .build()) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.udp(2000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/base/path1"), AbsoluteUnixPath.get("/base/path2"))) + .addHistory(nonEmptyLayerHistory) + .addHistory(emptyLayerHistory) + .addHistory(emptyLayerHistory) + .build(); + baseImageLayers = + Arrays.asList( + new PreparedLayer.Builder(mockCachedLayer).build(), + new PreparedLayer.Builder(mockCachedLayer).build(), + new PreparedLayer.Builder(mockCachedLayer).build()); + applicationLayers = + Arrays.asList( + new PreparedLayer.Builder(mockCachedLayer).setName("dependencies").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("resources").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("classes").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("extra files").build()); + } + + @Test + public void test_basicCase() { + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + Assert.assertEquals("root", image.getUser()); + Assert.assertEquals( + testDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest()); + } + + @Test + public void test_propagateBaseImageConfiguration() { + Mockito.when(mockContainerConfiguration.getEnvironmentMap()) + .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE")); + Mockito.when(mockContainerConfiguration.getLabels()) + .thenReturn(ImmutableMap.of("my.label", "my.label.value", "base.label.2", "new.value")); + Mockito.when(mockContainerConfiguration.getExposedPorts()) + .thenReturn(ImmutableSet.of(Port.tcp(3000), Port.udp(4000))); + Mockito.when(mockContainerConfiguration.getVolumes()) .thenReturn( - Futures.immediateFuture( - ImmutableList.of( - mockPullAndCacheBaseImageLayerStep, - mockPullAndCacheBaseImageLayerStep, - mockPullAndCacheBaseImageLayerStep))); - Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); - Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + ImmutableSet.of( + AbsoluteUnixPath.get("/new/path1"), AbsoluteUnixPath.get("/new/path2"))); + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + Assert.assertEquals("wasm", image.getArchitecture()); + Assert.assertEquals("js", image.getOs()); + Assert.assertEquals( + ImmutableMap.of( + "BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE"), + image.getEnvironment()); + Assert.assertEquals( + ImmutableMap.of( + "base.label", + "base.label.value", + "my.label", + "my.label.value", + "base.label.2", + "new.value"), + image.getLabels()); + Assert.assertNotNull(image.getHealthCheck()); + Assert.assertEquals( + ImmutableList.of("CMD-SHELL", "echo hi"), image.getHealthCheck().getCommand()); + Assert.assertTrue(image.getHealthCheck().getInterval().isPresent()); + Assert.assertEquals(Duration.ofSeconds(3), image.getHealthCheck().getInterval().get()); + Assert.assertTrue(image.getHealthCheck().getTimeout().isPresent()); + Assert.assertEquals(Duration.ofSeconds(2), image.getHealthCheck().getTimeout().get()); + Assert.assertTrue(image.getHealthCheck().getStartPeriod().isPresent()); + Assert.assertEquals(Duration.ofSeconds(1), image.getHealthCheck().getStartPeriod().get()); + Assert.assertTrue(image.getHealthCheck().getRetries().isPresent()); + Assert.assertEquals(20, (int) image.getHealthCheck().getRetries().get()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1000), Port.udp(2000), Port.tcp(3000), Port.udp(4000)), + image.getExposedPorts()); + Assert.assertEquals( + ImmutableSet.of( + AbsoluteUnixPath.get("/base/path1"), + AbsoluteUnixPath.get("/base/path2"), + AbsoluteUnixPath.get("/new/path1"), + AbsoluteUnixPath.get("/new/path2")), + image.getVolumes()); + Assert.assertEquals("/base/working/directory", image.getWorkingDirectory()); + Assert.assertEquals("root", image.getUser()); + + Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); + Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); + Assert.assertEquals(image.getHistory().get(2), emptyLayerHistory); + Assert.assertEquals(ImmutableList.of(), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of(), image.getProgramArguments()); + } + + @Test + public void testOverrideWorkingDirectory() { + Mockito.when(mockContainerConfiguration.getWorkingDirectory()) + .thenReturn(AbsoluteUnixPath.get("/my/directory")); + + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + Assert.assertEquals("/my/directory", image.getWorkingDirectory()); + } + + @Test + public void test_inheritedUser() { + Mockito.when(mockContainerConfiguration.getUser()).thenReturn(null); + + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + Assert.assertEquals("base:user", image.getUser()); + } + + @Test + public void test_inheritedEntrypoint() { + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); + Mockito.when(mockContainerConfiguration.getProgramArguments()) + .thenReturn(ImmutableList.of("test")); + + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of("test"), image.getProgramArguments()); } @Test - public void test_validateAsyncDependencies() throws ExecutionException, InterruptedException { - BuildImageStep buildImageStep = + public void test_inheritedEntrypointAndProgramArguments() { + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); + + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep), - ImmutableList.of()); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of("catalina.sh", "run"), image.getProgramArguments()); + } + + @Test + public void test_notInheritedProgramArguments() { + Mockito.when(mockContainerConfiguration.getEntrypoint()) + .thenReturn(ImmutableList.of("myEntrypoint")); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); + + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + Assert.assertEquals(ImmutableList.of("myEntrypoint"), image.getEntrypoint()); + Assert.assertNull(image.getProgramArguments()); + } + + @Test + public void test_generateHistoryObjects() { + Image image = + new BuildImageStep( + mockBuildContext, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); + + // Make sure history is as expected + HistoryEntry expectedAddedBaseLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setComment("auto-generated by Jib") + .build(); + + HistoryEntry expectedApplicationLayerHistoryDependencies = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("dependencies") + .build(); + + HistoryEntry expectedApplicationLayerHistoryResources = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("resources") + .build(); + + HistoryEntry expectedApplicationLayerHistoryClasses = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("classes") + .build(); + + HistoryEntry expectedApplicationLayerHistoryExtrafiles = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("extra files") + .build(); + + // Base layers (1 non-empty propagated, 2 empty propagated, 2 non-empty generated) + Assert.assertEquals(nonEmptyLayerHistory, image.getHistory().get(0)); + Assert.assertEquals(emptyLayerHistory, image.getHistory().get(1)); + Assert.assertEquals(emptyLayerHistory, image.getHistory().get(2)); + Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(3)); + Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(4)); + + // Application layers (4 generated) + Assert.assertEquals(expectedApplicationLayerHistoryDependencies, image.getHistory().get(5)); + Assert.assertEquals(expectedApplicationLayerHistoryResources, image.getHistory().get(6)); + Assert.assertEquals(expectedApplicationLayerHistoryClasses, image.getHistory().get(7)); + Assert.assertEquals(expectedApplicationLayerHistoryExtrafiles, image.getHistory().get(8)); + + // Should be exactly 9 total + Assert.assertEquals(9, image.getHistory().size()); + } + + @Test + public void testTruncateLongClasspath_shortClasspath() { + ImmutableList entrypoint = + ImmutableList.of( + "java", "-Dmy-property=value", "-cp", "/app/classes:/app/libs/*", "com.example.Main"); + + Assert.assertEquals( + "[java, -Dmy-property=value, -cp, /app/classes:/app/libs/*, com.example.Main]", + BuildImageStep.truncateLongClasspath(entrypoint)); + } + + @Test + public void testTruncateLongClasspath_longClasspath() { + String classpath = + "/app/resources:/app/classes:/app/libs/spring-boot-starter-web-2.0.3.RELEASE.jar:/app/libs/" + + "shared-library-0.1.0.jar:/app/libs/spring-boot-starter-json-2.0.3.RELEASE.jar:/app/" + + "libs/spring-boot-starter-2.0.3.RELEASE.jar:/app/libs/spring-boot-starter-tomcat-2.0." + + "3.RELEASE.jar"; + ImmutableList entrypoint = + ImmutableList.of("java", "-Dmy-property=value", "-cp", classpath, "com.example.Main"); + Assert.assertEquals( - testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); + "[java, -Dmy-property=value, -cp, /app/resources:/app/classes:/app/libs/spring-boot-starter" + + "-web-2.0.3.RELEASE.jar:/app/libs/shared-library-0.1.0.jar:/app/libs/spring-boot-" + + "starter-json-2.0.3.RELEASE.jar:/app/libs/spring-boot-starter-2.<... classpath " + + "truncated ...>, com.example.Main]", + BuildImageStep.truncateLongClasspath(entrypoint)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStepTest.java new file mode 100644 index 0000000000..be8951005e --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStepTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link BuildManifestListOrSingleManifest}. */ +@RunWith(MockitoJUnitRunner.class) +public class BuildManifestListOrSingleManifestStepTest { + + @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; + @Mock private BuildContext buildContext; + @Mock private Layer layer; + + private Image image1; + private Image image2; + + @Before + public void setUp() { + Mockito.when(buildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.doReturn(V22ManifestTemplate.class).when(buildContext).getTargetFormat(); + Mockito.when(layer.getBlobDescriptor()).thenReturn(new BlobDescriptor(0, null)); + + image1 = + Image.builder(V22ManifestTemplate.class) + .setArchitecture("amd64") + .setOs("linux") + .addLayer(layer) + .build(); + image2 = + Image.builder(V22ManifestTemplate.class) + .setArchitecture("arm64") + .setOs("windows") + .addLayer(layer) + .build(); + } + + @Test + public void testCall_singleManifest() throws IOException { + + // Expected manifest JSON + // { + // "schemaVersion":2, + // "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + // "config":{ + // "mediaType":"application/vnd.docker.container.image.v1+json", + // "digest":"sha256:1b2ff280940537177565443144a81319ad48528fd35d1cdc38cbde07f24f6912", + // "size":158 + // }, + // "layers":[ + // { + // "mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip", + // "size":0 + // } + // ] + // } + + ManifestTemplate manifestTemplate = + new BuildManifestListOrSingleManifestStep( + buildContext, progressDispatcherFactory, Arrays.asList(image1)) + .call(); + + Assert.assertTrue(manifestTemplate instanceof V22ManifestTemplate); + V22ManifestTemplate manifest = (V22ManifestTemplate) manifestTemplate; + Assert.assertEquals(2, manifest.getSchemaVersion()); + Assert.assertEquals( + "application/vnd.docker.distribution.manifest.v2+json", manifest.getManifestMediaType()); + Assert.assertEquals( + "sha256:1b2ff280940537177565443144a81319ad48528fd35d1cdc38cbde07f24f6912", + manifest.getContainerConfiguration().getDigest().toString()); + Assert.assertEquals(0, manifest.getLayers().get(0).getSize()); + Assert.assertEquals(158, manifest.getContainerConfiguration().getSize()); + } + + @Test + public void testCall_manifestList() throws IOException { + + // Expected Manifest List JSON + // { + // "schemaVersion":2, + // "mediaType":"application/vnd.docker.distribution.manifest.list.v2+json", + // "manifests":[ + // { + // "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + // "digest":"sha256:9467fc431ac5dd84dafdc13f75111fc467cd57aff4732edda8c9e0bbcabe0183", + // "size":338, + // "platform":{ + // "architecture":"amd64", + // "os":"linux" + // } + // }, + // { + // "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + // "digest":"sha256:439351c848845c46a3952f28416992b66003361d00943b6cdb04b6d5533f02bf", + // "size":338, + // "platform":{ + // "architecture":"arm64", + // "os":"windows" + // } + // } + // ] + // } + + ManifestTemplate manifestTemplate = + new BuildManifestListOrSingleManifestStep( + buildContext, progressDispatcherFactory, Arrays.asList(image1, image2)) + .call(); + + Assert.assertTrue(manifestTemplate instanceof V22ManifestListTemplate); + V22ManifestListTemplate manifestList = (V22ManifestListTemplate) manifestTemplate; + Assert.assertEquals(2, manifestList.getSchemaVersion()); + Assert.assertEquals( + Arrays.asList("sha256:9467fc431ac5dd84dafdc13f75111fc467cd57aff4732edda8c9e0bbcabe0183"), + manifestList.getDigestsForPlatform("amd64", "linux")); + Assert.assertEquals( + Arrays.asList("sha256:439351c848845c46a3952f28416992b66003361d00943b6cdb04b6d5533f02bf"), + manifestList.getDigestsForPlatform("arm64", "windows")); + } + + @Test + public void testCall_emptyImagesList() throws IOException { + try { + new BuildManifestListOrSingleManifestStep( + buildContext, progressDispatcherFactory, Collections.emptyList()) + .call(); + Assert.fail(); + } catch (IllegalStateException ex) { + Assert.assertEquals("no images given", ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java new file mode 100644 index 0000000000..3c42d6e091 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import java.io.IOException; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link BuildResult}. */ +public class BuildResultTest { + + private DescriptorDigest digest1; + private DescriptorDigest digest2; + private DescriptorDigest id; + private DescriptorDigest id2; + + @Before + public void setUp() throws DigestException { + digest1 = + DescriptorDigest.fromDigest( + "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"); + digest2 = + DescriptorDigest.fromDigest( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + id = + DescriptorDigest.fromDigest( + "sha256:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); + + id2 = + DescriptorDigest.fromDigest( + "sha256:1234543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); + } + + @Test + public void testCreated() { + BuildResult container = new BuildResult(digest1, id, true); + Assert.assertEquals(digest1, container.getImageDigest()); + Assert.assertEquals(id, container.getImageId()); + Assert.assertTrue(container.isImagePushed()); + } + + @Test + public void testEquality() { + BuildResult container1 = new BuildResult(digest1, id, true); + BuildResult container2 = new BuildResult(digest1, id, true); + BuildResult container3 = new BuildResult(digest2, id, true); + BuildResult container4 = new BuildResult(digest1, id, false); + BuildResult container5 = new BuildResult(digest1, id2, false); + + Assert.assertEquals(container1, container2); + Assert.assertEquals(container1, container1); + Assert.assertNotEquals(container1, container5); + Assert.assertNotEquals(container1, new Object()); + + Assert.assertEquals(container1.hashCode(), container2.hashCode()); + Assert.assertEquals(container1.hashCode(), container4.hashCode()); + Assert.assertNotEquals(container1, container3); + Assert.assertNotEquals(container1, container4); + } + + @Test + public void testFromImage() throws IOException { + Image image1 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image2 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image3 = Image.builder(V22ManifestTemplate.class).setUser("anotherUser").build(); + Assert.assertEquals( + BuildResult.fromImage(image1, V22ManifestTemplate.class), + BuildResult.fromImage(image2, V22ManifestTemplate.class)); + Assert.assertNotEquals( + BuildResult.fromImage(image1, V22ManifestTemplate.class), + BuildResult.fromImage(image3, V22ManifestTemplate.class)); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java new file mode 100644 index 0000000000..97bd9f1217 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageDetails; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.docker.CliDockerClient; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.Optional; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LocalBaseImageStepsTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + + @Mock private BuildContext buildContext; + @Mock private EventHandlers eventHandlers; + @Mock private ProgressEventDispatcher.Factory progressEventDispatcherFactory; + @Mock private ProgressEventDispatcher progressEventDispatcher; + @Mock private ProgressEventDispatcher.Factory childFactory; + @Mock private ProgressEventDispatcher childDispatcher; + + private static Path getResource(String resource) throws URISyntaxException { + return Paths.get(Resources.getResource(resource).toURI()); + } + + @Before + public void setup() throws IOException, CacheDirectoryCreationException { + Mockito.when(buildContext.getExecutorService()) + .thenReturn(MoreExecutors.newDirectExecutorService()); + Mockito.when(buildContext.getBaseImageLayersCache()) + .thenReturn(Cache.withDirectory(temporaryFolder.newFolder().toPath())); + Mockito.when(buildContext.getEventHandlers()).thenReturn(eventHandlers); + Mockito.when(progressEventDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(progressEventDispatcher); + Mockito.when(progressEventDispatcher.newChildProducer()).thenReturn(childFactory); + Mockito.when(childFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(childDispatcher); + } + + @After + public void tearDown() { + tempDirectoryProvider.close(); + } + + @Test + public void testCacheDockerImageTar_validDocker() throws Exception { + Path dockerBuild = getResource("core/extraction/docker-save.tar"); + LocalImage result = + LocalBaseImageSteps.cacheDockerImageTar( + buildContext, dockerBuild, progressEventDispatcherFactory, tempDirectoryProvider); + + Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); + Assert.assertEquals(2, result.layers.size()); + Assert.assertEquals( + "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", + result.layers.get(0).get().getDiffId().getHash()); + Assert.assertEquals( + "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", + result.layers.get(0).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals( + "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", + result.layers.get(1).get().getDiffId().getHash()); + Assert.assertEquals( + "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", + result.layers.get(1).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals(2, result.configurationTemplate.getLayerCount()); + } + + @Test + public void testCacheDockerImageTar_validTar() throws Exception { + Path tarBuild = getResource("core/extraction/jib-image.tar"); + LocalImage result = + LocalBaseImageSteps.cacheDockerImageTar( + buildContext, tarBuild, progressEventDispatcherFactory, tempDirectoryProvider); + + Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); + Assert.assertEquals(2, result.layers.size()); + Assert.assertEquals( + "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", + result.layers.get(0).get().getDiffId().getHash()); + Assert.assertEquals( + "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", + result.layers.get(0).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals( + "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", + result.layers.get(1).get().getDiffId().getHash()); + Assert.assertEquals( + "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", + result.layers.get(1).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals(2, result.configurationTemplate.getLayerCount()); + } + + @Test + public void testGetCachedDockerImage() + throws IOException, DigestException, CacheDirectoryCreationException, CacheCorruptedException, + URISyntaxException { + String dockerInspectJson = + "{\"Size\": 0," + + "\"Id\": \"sha256:066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0\"," + + "\"RootFS\": { \"Layers\": [" + + " \"sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd\"," + + " \"sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e\" ] } }"; + ImageDetails dockerImageDetails = + JsonTemplateMapper.readJson(dockerInspectJson, CliDockerClient.DockerImageDetails.class); + Path cachePath = temporaryFolder.newFolder("cache").toPath(); + Files.createDirectories(cachePath.resolve("local/config")); + Cache cache = Cache.withDirectory(cachePath); + + // Image not in cache + Optional localImage = + LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // Config in cache, but not layers + String configHash = "066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0"; + Files.copy( + getResource("core/extraction/test-cache/local/config/" + configHash), + cachePath.resolve("local/config/" + configHash)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // One layer missing + String diffId = "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd"; + String digest = "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f"; + Files.createDirectories(cachePath.resolve("local").resolve(diffId)); + Files.copy( + getResource("core/extraction/test-cache/local/" + diffId + "/" + digest), + cachePath.resolve("local").resolve(diffId).resolve(digest)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // Image fully in cache + diffId = "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e"; + digest = "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94"; + Files.createDirectories(cachePath.resolve("local").resolve(diffId)); + Files.copy( + getResource("core/extraction/test-cache/local/" + diffId + "/" + digest), + cachePath.resolve("local").resolve(diffId).resolve(digest)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertTrue(localImage.isPresent()); + LocalImage image = localImage.get(); + Assert.assertEquals(2, image.configurationTemplate.getLayerCount()); + Assert.assertEquals(2, image.layers.size()); + } + + @Test + public void testIsGzipped() throws URISyntaxException, IOException { + Assert.assertTrue( + LocalBaseImageSteps.isGzipped(getResource("core/extraction/compressed.tar.gz"))); + Assert.assertFalse( + LocalBaseImageSteps.isGzipped(getResource("core/extraction/not-compressed.tar"))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java new file mode 100644 index 0000000000..1eb560c1de --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.ReferenceLayer; +import com.google.cloud.tools.jib.registry.RegistryClient; +import java.io.IOException; +import java.security.DigestException; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer3; + +/** Tests for {@link ObtainBaseImageLayerStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class ObtainBaseImageLayerStepTest { + + private DescriptorDigest existingLayerDigest; + private DescriptorDigest freshLayerDigest; + + @Mock private Layer existingLayer; + @Mock private Layer freshLayer; + @Mock private RegistryClient registryClient; + + @Mock(answer = Answers.RETURNS_MOCKS) + private BuildContext buildContext; + + @Mock(answer = Answers.RETURNS_MOCKS) + private ProgressEventDispatcher.Factory progressDispatcherFactory; + + @Before + public void setUp() throws IOException, RegistryException, DigestException { + existingLayerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + freshLayerDigest = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + DescriptorDigest diffId = Mockito.mock(DescriptorDigest.class); + existingLayer = new ReferenceLayer(new BlobDescriptor(existingLayerDigest), diffId); + freshLayer = new ReferenceLayer(new BlobDescriptor(freshLayerDigest), diffId); + + Mockito.when(registryClient.checkBlob(existingLayerDigest)) + .thenReturn(Optional.of(Mockito.mock(BlobDescriptor.class))); + Mockito.when(registryClient.checkBlob(freshLayerDigest)).thenReturn(Optional.empty()); + + // necessary to prevent error from classes dealing with progress report + Answer3, Consumer> progressSizeSetter = + (ignored1, progressSizeConsumer, ignored2) -> { + progressSizeConsumer.accept(Long.valueOf(12345)); + return null; + }; + Mockito.when(registryClient.pullBlob(Mockito.any(), Mockito.any(), Mockito.any())) + .thenAnswer(AdditionalAnswers.answer(progressSizeSetter)); + } + + @Test + public void testForSelectiveDownload_existingLayer() + throws IOException, CacheCorruptedException, RegistryException { + ObtainBaseImageLayerStep puller = + ObtainBaseImageLayerStep.forSelectiveDownload( + buildContext, progressDispatcherFactory, existingLayer, registryClient, registryClient); + + PreparedLayer preparedLayer = puller.call(); + + Assert.assertEquals(StateInTarget.EXISTING, preparedLayer.getStateInTarget()); + // Should have queried the blob. + Mockito.verify(registryClient).checkBlob(existingLayerDigest); + // The layer should not be pulled. + Mockito.verify(registryClient, Mockito.never()) + .pullBlob(Mockito.eq(existingLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verifyNoMoreInteractions(registryClient); + } + + @Test + public void testForSelectiveDownload_freshLayer() + throws IOException, CacheCorruptedException, RegistryException { + ObtainBaseImageLayerStep puller = + ObtainBaseImageLayerStep.forSelectiveDownload( + buildContext, progressDispatcherFactory, freshLayer, registryClient, registryClient); + + PreparedLayer preparedLayer = puller.call(); + + Assert.assertEquals(StateInTarget.MISSING, preparedLayer.getStateInTarget()); + // Should have queried the blob. + Mockito.verify(registryClient).checkBlob(freshLayerDigest); + // The layer should not be pulled. + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(freshLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verifyNoMoreInteractions(registryClient); + } + + @Test + public void testForForcedDownload_existingLayer() + throws IOException, CacheCorruptedException, RegistryException { + ObtainBaseImageLayerStep puller = + ObtainBaseImageLayerStep.forForcedDownload( + buildContext, progressDispatcherFactory, existingLayer, registryClient); + PreparedLayer preparedLayer = puller.call(); + + // existence unknown + Assert.assertEquals(StateInTarget.UNKNOWN, preparedLayer.getStateInTarget()); + // No blob checking should happen. + Mockito.verify(registryClient, Mockito.never()).checkBlob(Mockito.any()); + // The layer should be pulled. + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(existingLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verifyNoMoreInteractions(registryClient); + } + + @Test + public void testForForcedDownload_freshLayer() + throws IOException, CacheCorruptedException, RegistryException { + ObtainBaseImageLayerStep puller = + ObtainBaseImageLayerStep.forForcedDownload( + buildContext, progressDispatcherFactory, freshLayer, registryClient); + PreparedLayer preparedLayer = puller.call(); + + // existence unknown + Assert.assertEquals(StateInTarget.UNKNOWN, preparedLayer.getStateInTarget()); + // No blob checking should happen. + Mockito.verify(registryClient, Mockito.never()).checkBlob(Mockito.any()); + // The layer should be pulled. + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(freshLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verifyNoMoreInteractions(registryClient); + } + + @Test + public void testLayerMissingInCacheInOfflineMode() + throws CacheCorruptedException, RegistryException { + Mockito.when(buildContext.isOffline()).thenReturn(true); + + ObtainBaseImageLayerStep puller = + ObtainBaseImageLayerStep.forForcedDownload( + buildContext, progressDispatcherFactory, freshLayer, registryClient); + try { + puller.call(); + Assert.fail(); + } catch (IOException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb. Rerun " + + "Jib in online mode with \"-Djib.alwaysCacheBaseImage=true\" to re-download the " + + "base image layers.", + ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PlatformCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PlatformCheckerTest.java new file mode 100644 index 0000000000..d6bcb38475 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PlatformCheckerTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.PlatformNotFoundInBaseImageException; +import com.google.common.collect.ImmutableSet; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PlatformChecker}. */ +@RunWith(MockitoJUnitRunner.class) +public class PlatformCheckerTest { + + @Mock private BuildContext buildContext; + @Mock private ContainerConfiguration containerConfig; + + @Before + public void setUp() { + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).build()); + Mockito.when(buildContext.getContainerConfiguration()).thenReturn(containerConfig); + } + + @Test + public void testCheckManifestPlatform_mismatch() { + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("configured arch", "configured OS"))); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setArchitecture("actual arch"); + containerConfigJson.setOs("actual OS"); + Exception ex = + assertThrows( + PlatformNotFoundInBaseImageException.class, + () -> PlatformChecker.checkManifestPlatform(buildContext, containerConfigJson)); + assertThat(ex) + .hasMessageThat() + .isEqualTo( + "the configured platform (configured arch/configured OS) doesn't match the " + + "platform (actual arch/actual OS) of the base image (scratch)"); + } + + @Test + public void testCheckManifestPlatform_noExceptionIfDefaultAmd64Linux() + throws PlatformNotFoundInBaseImageException { + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"))); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setArchitecture("actual arch"); + containerConfigJson.setOs("actual OS"); + PlatformChecker.checkManifestPlatform(buildContext, containerConfigJson); + } + + @Test + public void testCheckManifestPlatform_multiplePlatformsConfigured() { + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"), new Platform("arch", "os"))); + Exception ex = + assertThrows( + PlatformNotFoundInBaseImageException.class, + () -> + PlatformChecker.checkManifestPlatform( + buildContext, new ContainerConfigurationTemplate())); + assertThat(ex) + .hasMessageThat() + .isEqualTo( + "cannot build for multiple platforms since the base image 'scratch' is not a manifest list."); + } + + @Test + public void testCheckManifestPlatform_tarBaseImage() { + Path tar = Paths.get("/foo/bar.tar"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).setTarPath(tar).build()); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"), new Platform("arch", "os"))); + + Exception ex = + assertThrows( + PlatformNotFoundInBaseImageException.class, + () -> + PlatformChecker.checkManifestPlatform( + buildContext, new ContainerConfigurationTemplate())); + assertThat(ex) + .hasMessageThat() + .isEqualTo( + "cannot build for multiple platforms since the base image '" + + tar + + "' is not a manifest list."); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java new file mode 100644 index 0000000000..f3f5005a80 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java @@ -0,0 +1,728 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.eq; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.PlatformNotFoundInBaseImageException; +import com.google.cloud.tools.jib.image.json.UnlistedPlatformInManifestListException; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.security.DigestException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PullBaseImageStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class PullBaseImageStepTest { + + @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; + @Mock private ProgressEventDispatcher progressDispatcher; + @Mock private BuildContext buildContext; + @Mock private RegistryClient registryClient; + @Mock private RegistryClient.Factory registryClientFactory; + @Mock private ImageConfiguration imageConfiguration; + @Mock private ContainerConfiguration containerConfig; + @Mock private Cache cache; + @Mock private EventHandlers eventHandlers; + + private PullBaseImageStep pullBaseImageStep; + + @Before + public void setUp() { + Mockito.when(buildContext.getBaseImageConfiguration()).thenReturn(imageConfiguration); + Mockito.when(buildContext.getEventHandlers()).thenReturn(eventHandlers); + Mockito.when(buildContext.getBaseImageLayersCache()).thenReturn(cache); + Mockito.when(buildContext.newBaseImageRegistryClientFactory()) + .thenReturn(registryClientFactory); + Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); + Mockito.when(buildContext.getContainerConfiguration()).thenReturn(containerConfig); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("slim arch", "fat system"))); + Mockito.when(progressDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(progressDispatcher); + Mockito.when(progressDispatcher.newChildProducer()).thenReturn(progressDispatcherFactory); + + pullBaseImageStep = new PullBaseImageStep(buildContext, progressDispatcherFactory); + } + + @Test + public void testCall_scratch_singlePlatform() + throws LayerPropertyNotFoundException, IOException, RegistryException, + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.scratch()); + ImagesAndRegistryClient result = pullBaseImageStep.call(); + + Assert.assertEquals(1, result.images.size()); + Assert.assertEquals("slim arch", result.images.get(0).getArchitecture()); + Assert.assertEquals("fat system", result.images.get(0).getOs()); + Assert.assertNull(result.registryClient); + } + + @Test + public void testCall_scratch_multiplePlatforms() + throws LayerPropertyNotFoundException, IOException, RegistryException, + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.scratch()); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn( + ImmutableSet.of( + new Platform("architecture1", "os1"), new Platform("architecture2", "os2"))); + ImagesAndRegistryClient result = pullBaseImageStep.call(); + + Assert.assertEquals(2, result.images.size()); + Assert.assertEquals("architecture1", result.images.get(0).getArchitecture()); + Assert.assertEquals("os1", result.images.get(0).getOs()); + Assert.assertEquals("architecture2", result.images.get(1).getArchitecture()); + Assert.assertEquals("os2", result.images.get(1).getOs()); + Assert.assertNull(result.registryClient); + } + + @Test + public void testCall_digestBaseImage() + throws LayerPropertyNotFoundException, IOException, RegistryException, + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException, InvalidImageReferenceException { + ImageReference imageReference = + ImageReference.parse( + "awesome@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + Assert.assertTrue(imageReference.getDigest().isPresent()); + Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setArchitecture("slim arch"); + containerConfigJson.setOs("fat system"); + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestTemplate(), containerConfigJson, "sha256:digest"); + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(manifestAndConfig.getManifest())).thenReturn(true); + + ImagesAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.images.get(0).getOs()); + Assert.assertEquals(registryClient, result.registryClient); + } + + @Test + public void testCall_offlineMode_notCached() + throws LayerPropertyNotFoundException, RegistryException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException, InvalidImageReferenceException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.parse("cat")); + Mockito.when(buildContext.isOffline()).thenReturn(true); + + try { + pullBaseImageStep.call(); + Assert.fail(); + } catch (IOException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; cat not found in local Jib cache", ex.getMessage()); + } + } + + @Test + public void testCall_offlineMode_cached() + throws LayerPropertyNotFoundException, RegistryException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException, InvalidImageReferenceException, IOException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); + Mockito.when(buildContext.isOffline()).thenReturn(true); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setArchitecture("slim arch"); + containerConfigJson.setOs("fat system"); + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestTemplate(), containerConfigJson, "sha256:digest"); + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(manifestAndConfig.getManifest())).thenReturn(true); + + ImagesAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.images.get(0).getOs()); + Assert.assertNull(result.registryClient); + + Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(); + } + + @Test + public void testLookUpPlatformSpecificDockerImageManifest() + throws IOException, UnlistedPlatformInManifestListException { + String manifestListJson = + " {\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.list.v2+json\",\n" + + " \"manifests\": [\n" + + " {\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n" + + " \"size\": 424,\n" + + " \"digest\": \"sha256:1111111111111111111111111111111111111111111111111111111111111111\",\n" + + " \"platform\": {\n" + + " \"architecture\": \"arm64\",\n" + + " \"os\": \"linux\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n" + + " \"size\": 425,\n" + + " \"digest\": \"sha256:2222222222222222222222222222222222222222222222222222222222222222\",\n" + + " \"platform\": {\n" + + " \"architecture\": \"targetArchitecture\",\n" + + " \"os\": \"targetOS\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + V22ManifestListTemplate manifestList = + JsonTemplateMapper.readJson(manifestListJson, V22ManifestListTemplate.class); + + String manifestDigest = + pullBaseImageStep.lookUpPlatformSpecificImageManifest( + manifestList, new Platform("targetArchitecture", "targetOS")); + + Assert.assertEquals( + "sha256:2222222222222222222222222222222222222222222222222222222222222222", manifestDigest); + } + + @Test + public void testLookUpPlatformSpecificOciManifest() + throws IOException, UnlistedPlatformInManifestListException { + String manifestListJson = + " {\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.oci.image.index.v1+json\",\n" + + " \"manifests\": [\n" + + " {\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"size\": 424,\n" + + " \"digest\": \"sha256:1111111111111111111111111111111111111111111111111111111111111111\",\n" + + " \"platform\": {\n" + + " \"architecture\": \"arm64\",\n" + + " \"os\": \"linux\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"size\": 425,\n" + + " \"digest\": \"sha256:2222222222222222222222222222222222222222222222222222222222222222\",\n" + + " \"platform\": {\n" + + " \"architecture\": \"targetArchitecture\",\n" + + " \"os\": \"targetOS\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + OciIndexTemplate manifestList = + JsonTemplateMapper.readJson(manifestListJson, OciIndexTemplate.class); + + String manifestDigest = + pullBaseImageStep.lookUpPlatformSpecificImageManifest( + manifestList, new Platform("targetArchitecture", "targetOS")); + + Assert.assertEquals( + "sha256:2222222222222222222222222222222222222222222222222222222222222222", manifestDigest); + } + + @Test + public void testGetCachedBaseImages_emptyCache() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, PlatformNotFoundInBaseImageException, + BadContainerConfigurationFormatException, LayerCountMismatchException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.empty()); + + Assert.assertEquals(Arrays.asList(), pullBaseImageStep.getCachedBaseImages()); + } + + @Test + public void testGetCachedBaseImages_partiallyCached_emptyListReturned() + throws InvalidImageReferenceException, CacheCorruptedException, IOException, + LayerCountMismatchException, PlatformNotFoundInBaseImageException, + BadContainerConfigurationFormatException, UnlistedPlatformInManifestListException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + ManifestTemplate manifest = Mockito.mock(ManifestTemplate.class); + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate( + null, Arrays.asList(new ManifestAndConfigTemplate(manifest, null))); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(manifest)).thenReturn(false); + + assertThat(pullBaseImageStep.getCachedBaseImages()).isEmpty(); + } + + @Test + public void testGetCachedBaseImages_v21ManifestCached() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, BadContainerConfigurationFormatException, + LayerCountMismatchException, DigestException, PlatformNotFoundInBaseImageException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "1111111111111111111111111111111111111111111111111111111111111111"); + V21ManifestTemplate v21Manifest = Mockito.mock(V21ManifestTemplate.class); + Mockito.when(v21Manifest.getLayerDigests()).thenReturn(Arrays.asList(layerDigest)); + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate( + null, Arrays.asList(new ManifestAndConfigTemplate(v21Manifest, null))); + + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(v21Manifest)).thenReturn(true); + + List images = pullBaseImageStep.getCachedBaseImages(); + + Assert.assertEquals(1, images.size()); + Assert.assertEquals(1, images.get(0).getLayers().size()); + Assert.assertEquals( + "1111111111111111111111111111111111111111111111111111111111111111", + images.get(0).getLayers().get(0).getBlobDescriptor().getDigest().getHash()); + } + + @Test + public void testGetCachedBaseImages_manifestCached() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, BadContainerConfigurationFormatException, + LayerCountMismatchException, PlatformNotFoundInBaseImageException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setArchitecture("slim arch"); + containerConfigJson.setOs("fat system"); + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), containerConfigJson, "sha256:digest"); + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(manifestAndConfig.getManifest())).thenReturn(true); + + List images = pullBaseImageStep.getCachedBaseImages(); + + Assert.assertEquals(1, images.size()); + Assert.assertEquals("slim arch", images.get(0).getArchitecture()); + Assert.assertEquals("fat system", images.get(0).getOs()); + } + + @Test + public void testGetCachedBaseImages_manifestListCached() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, BadContainerConfigurationFormatException, + LayerCountMismatchException, PlatformNotFoundInBaseImageException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + + ContainerConfigurationTemplate containerConfigJson1 = new ContainerConfigurationTemplate(); + ContainerConfigurationTemplate containerConfigJson2 = new ContainerConfigurationTemplate(); + containerConfigJson1.setContainerUser("user1"); + containerConfigJson2.setContainerUser("user2"); + + ManifestListTemplate manifestList = Mockito.mock(ManifestListTemplate.class); + Mockito.when(manifestList.getDigestsForPlatform("arch1", "os1")) + .thenReturn(Arrays.asList("sha256:digest1")); + Mockito.when(manifestList.getDigestsForPlatform("arch2", "os2")) + .thenReturn(Arrays.asList("sha256:digest2")); + + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate( + manifestList, + Arrays.asList( + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), + containerConfigJson1, + "sha256:digest1"), + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), + containerConfigJson2, + "sha256:digest2"))); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when( + cache.areAllLayersCached(imageMetadata.getManifestsAndConfigs().get(0).getManifest())) + .thenReturn(true); + Mockito.when( + cache.areAllLayersCached(imageMetadata.getManifestsAndConfigs().get(1).getManifest())) + .thenReturn(true); + + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("arch1", "os1"), new Platform("arch2", "os2"))); + + List images = pullBaseImageStep.getCachedBaseImages(); + + Assert.assertEquals(2, images.size()); + Assert.assertEquals("user1", images.get(0).getUser()); + Assert.assertEquals("user2", images.get(1).getUser()); + } + + @Test + public void testGetCachedBaseImages_manifestListCached_partialMatches() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, BadContainerConfigurationFormatException, + LayerCountMismatchException, PlatformNotFoundInBaseImageException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + + ManifestListTemplate manifestList = Mockito.mock(ManifestListTemplate.class); + Mockito.when(manifestList.getDigestsForPlatform("arch1", "os1")) + .thenReturn(Arrays.asList("sha256:digest1")); + Mockito.when(manifestList.getDigestsForPlatform("arch2", "os2")) + .thenReturn(Arrays.asList("sha256:digest2")); + + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate( + manifestList, + Arrays.asList( + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), + new ContainerConfigurationTemplate(), + "sha256:digest1"))); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when( + cache.areAllLayersCached(imageMetadata.getManifestsAndConfigs().get(0).getManifest())) + .thenReturn(true); + + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("arch1", "os1"), new Platform("arch2", "os2"))); + + Assert.assertEquals(Arrays.asList(), pullBaseImageStep.getCachedBaseImages()); + } + + @Test + public void testGetCachedBaseImages_manifestListCached_onlyPlatforms() + throws InvalidImageReferenceException, IOException, CacheCorruptedException, + UnlistedPlatformInManifestListException, PlatformNotFoundInBaseImageException, + BadContainerConfigurationFormatException, LayerCountMismatchException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(imageReference).build()); + + ManifestListTemplate manifestList = Mockito.mock(ManifestListTemplate.class); + Mockito.when(manifestList.getDigestsForPlatform("target-arch", "target-os")) + .thenReturn(Arrays.asList("sha256:target-digest")); + + ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setContainerUser("target-user"); + + ManifestAndConfigTemplate targetManifestAndConfig = + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), + containerConfigJson, + "sha256:target-digest"); + ManifestAndConfigTemplate unrelatedManifestAndConfig = + new ManifestAndConfigTemplate( + Mockito.mock(BuildableManifestTemplate.class), + new ContainerConfigurationTemplate(), + "sha256:unrelated-digest"); + + ImageMetadataTemplate imageMetadata = + new ImageMetadataTemplate( + manifestList, + Arrays.asList( + unrelatedManifestAndConfig, targetManifestAndConfig, unrelatedManifestAndConfig)); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(imageMetadata)); + Mockito.when(cache.areAllLayersCached(targetManifestAndConfig.getManifest())).thenReturn(true); + + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("target-arch", "target-os"))); + + List images = pullBaseImageStep.getCachedBaseImages(); + + Assert.assertEquals(1, images.size()); + Assert.assertEquals("target-user", images.get(0).getUser()); + } + + @Test + public void testTryMirrors_noMatchingMirrors() + throws LayerCountMismatchException, BadContainerConfigurationFormatException, + PlatformNotFoundInBaseImageException { + Mockito.when(imageConfiguration.getImageRegistry()).thenReturn("registry"); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("unmatched1", "mirror1", "unmatched2", "mirror2")); + + Optional result = + pullBaseImageStep.tryMirrors(buildContext, progressDispatcherFactory); + Assert.assertEquals(Optional.empty(), result); + + InOrder inOrder = Mockito.inOrder(eventHandlers); + inOrder.verify(eventHandlers).dispatch(LogEvent.debug("mirror config: unmatched1 --> mirror1")); + inOrder.verify(eventHandlers).dispatch(LogEvent.debug("mirror config: unmatched2 --> mirror2")); + Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(Mockito.any()); + } + + @Test + public void testTryMirrors_mirrorIoError() + throws LayerCountMismatchException, BadContainerConfigurationFormatException, IOException, + RegistryException { + Mockito.when(imageConfiguration.getImageRegistry()).thenReturn("registry"); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("registry", "gcr.io")); + Mockito.when(buildContext.newBaseImageRegistryClientFactory("gcr.io")) + .thenReturn(registryClientFactory); + Mockito.when(registryClient.pullManifest(Mockito.any())) + .thenThrow(new IOException("test exception")); + + Optional result = + pullBaseImageStep.tryMirrors(buildContext, progressDispatcherFactory); + Assert.assertEquals(Optional.empty(), result); + + InOrder inOrder = Mockito.inOrder(eventHandlers); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.info("trying mirror gcr.io for the base image")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.debug("failed to get manifest from mirror gcr.io: test exception")); + } + + @Test + public void testTryMirrors_multipleMirrors() + throws LayerCountMismatchException, BadContainerConfigurationFormatException, IOException, + RegistryException, InvalidImageReferenceException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.parse("registry/repo")); + Mockito.when(imageConfiguration.getImageRegistry()).thenReturn("registry"); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("registry", "quay.io", "registry", "gcr.io")); + + Mockito.when(buildContext.newBaseImageRegistryClientFactory("quay.io")) + .thenReturn(registryClientFactory); + Mockito.when(registryClient.pullManifest(Mockito.any())) + .thenThrow(new RegistryException("not found")); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"))); + + RegistryClient.Factory gcrRegistryClientFactory = + setUpWorkingRegistryClientFactoryWithV22ManifestTemplate(); + Mockito.when(buildContext.newBaseImageRegistryClientFactory("gcr.io")) + .thenReturn(gcrRegistryClientFactory); + + Optional result = + pullBaseImageStep.tryMirrors(buildContext, progressDispatcherFactory); + Assert.assertTrue(result.isPresent()); + Assert.assertEquals(gcrRegistryClientFactory.newRegistryClient(), result.get().registryClient); + + InOrder inOrder = Mockito.inOrder(eventHandlers); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.info("trying mirror quay.io for the base image")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.debug("failed to get manifest from mirror quay.io: not found")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.info("trying mirror gcr.io for the base image")); + inOrder.verify(eventHandlers).dispatch(LogEvent.info("pulled manifest from mirror gcr.io")); + } + + @Test + public void testCall_allMirrorsFail() + throws InvalidImageReferenceException, IOException, RegistryException, + LayerPropertyNotFoundException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.parse("registry/repo")); + Mockito.when(imageConfiguration.getImageRegistry()).thenReturn("registry"); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("registry", "quay.io", "registry", "gcr.io")); + + Mockito.when(buildContext.newBaseImageRegistryClientFactory(Mockito.any())) + .thenReturn(registryClientFactory); + Mockito.when(registryClient.pullManifest(Mockito.any())) + .thenThrow(new RegistryException("not found")); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"))); + RegistryClient.Factory dockerHubRegistryClientFactory = + setUpWorkingRegistryClientFactoryWithV22ManifestTemplate(); + Mockito.when(buildContext.newBaseImageRegistryClientFactory()) + .thenReturn(dockerHubRegistryClientFactory); + + ImagesAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals(dockerHubRegistryClientFactory.newRegistryClient(), result.registryClient); + + InOrder inOrder = Mockito.inOrder(eventHandlers); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.info("trying mirror quay.io for the base image")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.debug("failed to get manifest from mirror quay.io: not found")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.info("trying mirror gcr.io for the base image")); + inOrder + .verify(eventHandlers) + .dispatch(LogEvent.debug("failed to get manifest from mirror gcr.io: not found")); + } + + @Test + public void testCall_ManifestList() + throws InvalidImageReferenceException, IOException, RegistryException, + LayerPropertyNotFoundException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException { + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.parse("multiarch")).build()); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("registry", "gcr.io")); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"))); + RegistryClient.Factory dockerHubRegistryClientFactory = + setUpWorkingRegistryClientFactoryWithV22ManifestList(); + Mockito.when(buildContext.newBaseImageRegistryClientFactory()) + .thenReturn(dockerHubRegistryClientFactory); + + ImagesAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals(V22ManifestTemplate.class, result.images.get(0).getImageFormat()); + Assert.assertEquals("linux", result.images.get(0).getOs()); + Assert.assertEquals("amd64", result.images.get(0).getArchitecture()); + } + + @Test(expected = UnlistedPlatformInManifestListException.class) + public void testCall_ManifestList_UnknownArchitecture() + throws InvalidImageReferenceException, IOException, RegistryException, + LayerPropertyNotFoundException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException { + Mockito.when(buildContext.getBaseImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.parse("multiarch")).build()); + Mockito.when(buildContext.getRegistryMirrors()) + .thenReturn(ImmutableListMultimap.of("registry", "gcr.io")); + Mockito.when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("arm64", "linux"))); + RegistryClient.Factory dockerHubRegistryClientFactory = + setUpWorkingRegistryClientFactoryWithV22ManifestList(); + Mockito.when(buildContext.newBaseImageRegistryClientFactory()) + .thenReturn(dockerHubRegistryClientFactory); + + pullBaseImageStep.call(); + } + + private static RegistryClient.Factory setUpWorkingRegistryClientFactoryWithV22ManifestTemplate() + throws IOException, RegistryException { + DescriptorDigest digest = Mockito.mock(DescriptorDigest.class); + V22ManifestTemplate manifest = new V22ManifestTemplate(); + manifest.setContainerConfiguration(1234, digest); + + RegistryClient.Factory clientFactory = Mockito.mock(RegistryClient.Factory.class); + RegistryClient client = Mockito.mock(RegistryClient.class); + Mockito.when(clientFactory.newRegistryClient()).thenReturn(client); + Mockito.when(client.pullManifest(Mockito.any())) + .thenReturn(new ManifestAndDigest<>(manifest, digest)); + // mocking pulling container config json + Mockito.when(client.pullBlob(Mockito.any(), Mockito.any(), Mockito.any())) + .then( + invocation -> { + Consumer blobSizeListener = invocation.getArgument(1); + blobSizeListener.accept(1L); + return Blobs.from("{}"); + }); + return clientFactory; + } + + private static RegistryClient.Factory setUpWorkingRegistryClientFactoryWithV22ManifestList() + throws IOException, RegistryException { + DescriptorDigest digest = Mockito.mock(DescriptorDigest.class); + V22ManifestListTemplate manifestList = new V22ManifestListTemplate(); + V22ManifestListTemplate.ManifestDescriptorTemplate platformManifest = + new V22ManifestListTemplate.ManifestDescriptorTemplate(); + platformManifest.setMediaType(V22ManifestTemplate.MANIFEST_MEDIA_TYPE); + platformManifest.setSize(1234); + platformManifest.setDigest("sha256:aaaaaaa"); + platformManifest.setPlatform("amd64", "linux"); + manifestList.addManifest(platformManifest); + + V22ManifestTemplate manifest = new V22ManifestTemplate(); + manifest.setContainerConfiguration(1234, digest); + + RegistryClient.Factory clientFactory = Mockito.mock(RegistryClient.Factory.class); + RegistryClient client = Mockito.mock(RegistryClient.class); + Mockito.when(clientFactory.newRegistryClient()).thenReturn(client); + Mockito.when(client.pullManifest(eq("sha256:aaaaaaa"))) + .thenReturn(new ManifestAndDigest<>(manifest, digest)); + Mockito.when(client.pullManifest(eq("latest"))) + .thenReturn(new ManifestAndDigest<>(manifestList, digest)); + // mocking pulling container config json + Mockito.when(client.pullBlob(Mockito.any(), Mockito.any(), Mockito.any())) + .then( + invocation -> { + Consumer blobSizeListener = invocation.getArgument(1); + blobSizeListener.accept(1L); + return Blobs.from("{}"); + }); + return clientFactory; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java new file mode 100644 index 0000000000..a1003783b6 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.registry.RegistryClient; +import java.io.IOException; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PushBlobStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class PushBlobStepTest { + + @Mock private BlobDescriptor blobDescriptor; + @Mock private RegistryClient registryClient; + + @Mock(answer = Answers.RETURNS_MOCKS) + private ProgressEventDispatcher.Factory progressDispatcherFactory; + + @Mock(answer = Answers.RETURNS_MOCKS) + private BuildContext buildContext; + + @Before + public void setUp() { + Mockito.when(buildContext.getTargetImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).build()); + } + + @Test + public void testCall_doBlobCheckAndBlobExists() throws IOException, RegistryException { + Mockito.when(registryClient.checkBlob(Mockito.any())).thenReturn(Optional.of(blobDescriptor)); + + call(false); + + Mockito.verify(registryClient).checkBlob(Mockito.any()); + Mockito.verify(registryClient, Mockito.never()) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testCall_doBlobCheckAndBlobDoesNotExist() throws IOException, RegistryException { + Mockito.when(registryClient.checkBlob(Mockito.any())).thenReturn(Optional.empty()); + + call(false); + + Mockito.verify(registryClient).checkBlob(Mockito.any()); + Mockito.verify(registryClient) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testCall_forcePushWithNoBlobCheck() throws IOException, RegistryException { + call(true); + + Mockito.verify(registryClient, Mockito.never()).checkBlob(Mockito.any()); + Mockito.verify(registryClient) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + private void call(boolean forcePush) throws IOException, RegistryException { + new PushBlobStep( + buildContext, + progressDispatcherFactory, + registryClient, + blobDescriptor, + null, + forcePush) + .call(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushImageStepTest.java new file mode 100644 index 0000000000..b06096938a --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushImageStepTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PushImageStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class PushImageStepTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + + @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; + @Mock private ProgressEventDispatcher progressDispatcher; + @Mock private BuildContext buildContext; + @Mock private RegistryClient registryClient; + @Mock private ContainerConfiguration containerConfig; + @Mock private DescriptorDigest mockDescriptorDigest; + + private final V22ManifestListTemplate manifestList = new V22ManifestListTemplate(); + + @Before + public void setUp() { + when(buildContext.getAllTargetImageTags()).thenReturn(ImmutableSet.of("tag1", "tag2")); + when(buildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + when(buildContext.getContainerConfiguration()).thenReturn(containerConfig); + doReturn(V22ManifestTemplate.class).when(buildContext).getTargetFormat(); + when(containerConfig.getPlatforms()) + .thenReturn( + ImmutableSet.of(new Platform("amd64", "linux"), new Platform("arm64", "windows"))); + when(progressDispatcherFactory.create(anyString(), anyLong())).thenReturn(progressDispatcher); + when(progressDispatcher.newChildProducer()).thenReturn(progressDispatcherFactory); + + ManifestDescriptorTemplate manifest = new ManifestDescriptorTemplate(); + manifest.setSize(100); + manifest.setDigest("sha256:1f25787aab4669d252bdae09a72b9c345d2a7b8c64c8dbfba4c82af4834dbccc"); + manifestList.addManifest(manifest); + } + + @Test + public void testMakeListForManifestList() throws IOException, RegistryException { + List pushImageStepList = + PushImageStep.makeListForManifestList( + buildContext, progressDispatcherFactory, registryClient, manifestList, false); + + assertThat(pushImageStepList).hasSize(2); + for (PushImageStep pushImageStep : pushImageStepList) { + BuildResult buildResult = pushImageStep.call(); + assertThat(buildResult.getImageDigest().toString()) + .isEqualTo("sha256:64303e82b8a80ef20475dc7f807b81f172cacce1a59191927f3a7ea5222f38ae"); + assertThat(buildResult.getImageId().toString()) + .isEqualTo("sha256:64303e82b8a80ef20475dc7f807b81f172cacce1a59191927f3a7ea5222f38ae"); + } + } + + @Test + public void testMakeList_multiPlatform_platformTags() throws IOException, RegistryException { + Image image = Image.builder(V22ManifestTemplate.class).setArchitecture("wasm").build(); + + when(buildContext.getEnablePlatformTags()).thenReturn(true); + + List pushImageStepList = + PushImageStep.makeList( + buildContext, + progressDispatcherFactory, + registryClient, + new BlobDescriptor(mockDescriptorDigest), + image, + false); + + ArgumentCaptor tagCatcher = ArgumentCaptor.forClass(String.class); + when(registryClient.pushManifest(any(), tagCatcher.capture())).thenReturn(null); + + assertThat(pushImageStepList).hasSize(2); + pushImageStepList.get(0).call(); + pushImageStepList.get(1).call(); + + assertThat(tagCatcher.getAllValues()).containsExactly("tag1-wasm", "tag2-wasm"); + } + + @Test + public void testMakeList_multiPlatform_nonPlatformTags() throws IOException, RegistryException { + Image image = Image.builder(V22ManifestTemplate.class).setArchitecture("wasm").build(); + when(buildContext.getEnablePlatformTags()).thenReturn(false); + + List pushImageStepList = + PushImageStep.makeList( + buildContext, + progressDispatcherFactory, + registryClient, + new BlobDescriptor(mockDescriptorDigest), + image, + false); + + ArgumentCaptor tagCatcher = ArgumentCaptor.forClass(String.class); + when(registryClient.pushManifest(any(), tagCatcher.capture())).thenReturn(null); + + assertThat(pushImageStepList).hasSize(1); + pushImageStepList.get(0).call(); + assertThat(tagCatcher.getAllValues()) + .containsExactly("sha256:0dd75658cf52608fbd72eb95ff5fc5946966258c3676b35d336bfcc7ac5006f1"); + } + + @Test + public void testMakeListForManifestList_singlePlatform() throws IOException { + when(containerConfig.getPlatforms()) + .thenReturn(ImmutableSet.of(new Platform("amd64", "linux"))); + + List pushImageStepList = + PushImageStep.makeListForManifestList( + buildContext, progressDispatcherFactory, registryClient, manifestList, false); + assertThat(pushImageStepList).isEmpty(); + } + + @Test + public void testMakeListForManifestList_manifestListAlreadyExists() throws IOException { + System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); + + List pushImageStepList = + PushImageStep.makeListForManifestList( + buildContext, progressDispatcherFactory, registryClient, manifestList, true); + assertThat(pushImageStepList).isEmpty(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java new file mode 100644 index 0000000000..567220f925 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.util.concurrent.MoreExecutors; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link RegistryCredentialRetriever}. */ +@RunWith(MockitoJUnitRunner.class) +public class RegistryCredentialRetrieverTest { + + @Mock private EventHandlers mockEventHandlers; + + @Test + public void testCall_retrieved() + throws CredentialRetrievalException, CacheDirectoryCreationException { + BuildContext buildContext = + makeFakeBuildContext( + Arrays.asList( + Optional::empty, + () -> Optional.of(Credential.from("baseusername", "basepassword"))), + Arrays.asList( + () -> Optional.of(Credential.from("targetusername", "targetpassword")), + () -> Optional.of(Credential.from("ignored", "ignored")))); + + Assert.assertEquals( + Optional.of(Credential.from("baseusername", "basepassword")), + RegistryCredentialRetriever.getBaseImageCredential(buildContext)); + Assert.assertEquals( + Optional.of(Credential.from("targetusername", "targetpassword")), + RegistryCredentialRetriever.getTargetImageCredential(buildContext)); + } + + @Test + public void testCall_none() throws CredentialRetrievalException, CacheDirectoryCreationException { + BuildContext buildContext = + makeFakeBuildContext( + Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); + Assert.assertFalse( + RegistryCredentialRetriever.getBaseImageCredential(buildContext).isPresent()); + + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.info("No credentials could be retrieved for baseregistry/baserepo")); + + Assert.assertFalse( + RegistryCredentialRetriever.getTargetImageCredential(buildContext).isPresent()); + + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.info("No credentials could be retrieved for targetregistry/targetrepo")); + } + + @Test + public void testCall_exception() throws CacheDirectoryCreationException { + CredentialRetrievalException credentialRetrievalException = + Mockito.mock(CredentialRetrievalException.class); + BuildContext buildContext = + makeFakeBuildContext( + Collections.singletonList( + () -> { + throw credentialRetrievalException; + }), + Collections.emptyList()); + try { + RegistryCredentialRetriever.getBaseImageCredential(buildContext); + Assert.fail("Should have thrown exception"); + + } catch (CredentialRetrievalException ex) { + Assert.assertSame(credentialRetrievalException, ex); + } + } + + private BuildContext makeFakeBuildContext( + List baseCredentialRetrievers, + List targetCredentialRetrievers) + throws CacheDirectoryCreationException { + ImageReference baseImage = ImageReference.of("baseregistry", "baserepo", null); + ImageReference targetImage = ImageReference.of("targetregistry", "targetrepo", null); + return BuildContext.builder() + .setEventHandlers(mockEventHandlers) + .setBaseImageConfiguration( + ImageConfiguration.builder(baseImage) + .setCredentialRetrievers(baseCredentialRetrievers) + .build()) + .setTargetImageConfiguration( + ImageConfiguration.builder(targetImage) + .setCredentialRetrievers(targetCredentialRetrievers) + .build()) + .setContainerConfiguration(ContainerConfiguration.builder().build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()) + .build(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java deleted file mode 100644 index ffda43775f..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; -import javax.annotation.Nullable; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link RetrieveRegistryCredentialsStep}. */ -@RunWith(MockitoJUnitRunner.class) -public class RetrieveRegistryCredentialsStepTest { - - private static final String FAKE_TARGET_REGISTRY = "someRegistry"; - - @Mock private ListeningExecutorService mockListeningExecutorService; - @Mock private BuildConfiguration mockBuildConfiguration; - @Mock private BuildLogger mockBuildLogger; - - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; - @Mock private DockerCredentialHelper mockDockerCredentialHelper; - /** - * A {@link DockerCredentialHelper} that throws {@link - * NonexistentServerUrlDockerCredentialHelperException}. - */ - @Mock private DockerCredentialHelper mockNonexistentServerUrlDockerCredentialHelper; - /** - * A {@link DockerCredentialHelper} that throws {@link - * NonexistentDockerCredentialHelperException}. - */ - @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - - @Mock private Authorization mockAuthorization; - - @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; - - @Mock - private NonexistentServerUrlDockerCredentialHelperException - mockNonexistentServerUrlDockerCredentialHelperException; - - @Mock - private NonexistentDockerCredentialHelperException mockNonexistentDockerCredentialHelperException; - - @Before - public void setUpMocks() - throws NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(mockAuthorization); - Mockito.when(mockNonexistentServerUrlDockerCredentialHelper.retrieve()) - .thenThrow(mockNonexistentServerUrlDockerCredentialHelperException); - Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockNonexistentDockerCredentialHelperException); - } - - @Test - public void testCall_useCredentialHelper() - throws IOException, NonexistentDockerCredentialHelperException { - Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix( - "someOtherCredentialHelper")) - .thenReturn(mockDockerCredentialHelper); - - Assert.assertEquals( - mockAuthorization, - makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someOtherCredentialHelper", null) - .call()); - - Mockito.verify(mockBuildLogger) - .info("Using docker-credential-someOtherCredentialHelper for " + FAKE_TARGET_REGISTRY); - } - - @Test - public void testCall_useKnownRegistryCredentials() - throws IOException, NonexistentDockerCredentialHelperException { - Assert.assertEquals( - mockAuthorization, - makeRetrieveRegistryCredentialsStep( - FAKE_TARGET_REGISTRY, - null, - new RegistryCredentials("credentialSource", mockAuthorization)) - .call()); - - Mockito.verify(mockBuildLogger).info("Using credentialSource for " + FAKE_TARGET_REGISTRY); - } - - @Test - public void testCall_useDockerConfig() - throws IOException, NonexistentDockerCredentialHelperException { - // Credential helper does not have credentials. - Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("someCredentialHelper")) - .thenReturn(mockNonexistentServerUrlDockerCredentialHelper); - - Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(mockAuthorization); - - Assert.assertEquals( - mockAuthorization, - makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someCredentialHelper", null) - .call()); - - Mockito.verify(mockBuildLogger) - .info("Using credentials from Docker config for " + FAKE_TARGET_REGISTRY); - } - - @Test - public void testCall_inferCommonCredentialHelpers() - throws IOException, NonexistentDockerCredentialHelperException { - Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix("gcr")) - .thenReturn(mockDockerCredentialHelper); - Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix("ecr-login")) - .thenReturn(mockNonexistentDockerCredentialHelper); - - Assert.assertEquals( - mockAuthorization, - makeRetrieveRegistryCredentialsStep("something.gcr.io", null, null).call()); - Mockito.verify(mockBuildLogger).info("Using docker-credential-gcr for something.gcr.io"); - - Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); - Assert.assertEquals( - null, makeRetrieveRegistryCredentialsStep("something.amazonaws.com", null, null).call()); - Mockito.verify(mockBuildLogger).warn("warning"); - } - - /** Creates a fake {@link RetrieveRegistryCredentialsStep} for {@code registry}. */ - private RetrieveRegistryCredentialsStep makeRetrieveRegistryCredentialsStep( - String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials) { - Mockito.when(mockBuildConfiguration.getTargetImageRegistry()).thenReturn(FAKE_TARGET_REGISTRY); - - return new RetrieveRegistryCredentialsStep( - mockListeningExecutorService, - mockBuildLogger, - registry, - credentialHelperSuffix, - knownRegistryCredentials, - mockDockerCredentialHelperFactory, - mockDockerConfigCredentialRetriever); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java new file mode 100644 index 0000000000..6468ee051b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java @@ -0,0 +1,288 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.DigestOnlyLayer; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ForwardingExecutorService; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.security.DigestException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link StepsRunner}. */ +@RunWith(MockitoJUnitRunner.class) +public class StepsRunnerTest { + + // ListeningExecutorService is annotated with @DoNotMock, so define a concrete class. + private class MockListeningExecutorService extends ForwardingExecutorService + implements ListeningExecutorService { + + @Override + public ListenableFuture submit(Callable task) { + try { + return Futures.immediateFuture(executorService.submit(task).get()); + } catch (InterruptedException | ExecutionException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public ListenableFuture submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public ListenableFuture submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + protected ExecutorService delegate() { + throw new UnsupportedOperationException(); + } + } + + @Mock private BuildContext buildContext; + @Mock private EventHandlers eventHandlers; + @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; + @Mock private ProgressEventDispatcher progressDispatcher; + @Mock private ExecutorService executorService; + @Mock private Image builtArm64AndLinuxImage; + @Mock private Image builtAmd64AndWindowsImage; + @Mock private Image baseImage1; + @Mock private Image baseImage2; + + private StepsRunner stepsRunner; + + @Before + public void setup() { + stepsRunner = new StepsRunner(new MockListeningExecutorService(), buildContext); + + when(progressDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(progressDispatcher); + } + + @Test + public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() + throws DigestException, InterruptedException, ExecutionException { + when(executorService.submit(Mockito.any(PullBaseImageStep.class))) + .thenReturn(Futures.immediateFuture(new ImagesAndRegistryClient(null, null))); + // Pretend that a thread pulling base images returned some (meaningless) result. + stepsRunner.pullBaseImages(progressDispatcherFactory); + + DescriptorDigest digest1 = + DescriptorDigest.fromHash( + "1111111111111111111111111111111111111111111111111111111111111111"); + DescriptorDigest digest2 = + DescriptorDigest.fromHash( + "2222222222222222222222222222222222222222222222222222222222222222"); + DescriptorDigest digest3 = + DescriptorDigest.fromHash( + "3333333333333333333333333333333333333333333333333333333333333333"); + DigestOnlyLayer layer1 = new DigestOnlyLayer(digest1); + DigestOnlyLayer layer2 = new DigestOnlyLayer(digest2); + DigestOnlyLayer layer3 = new DigestOnlyLayer(digest3); + + PreparedLayer preparedLayer1 = Mockito.mock(PreparedLayer.class); + PreparedLayer preparedLayer2 = Mockito.mock(PreparedLayer.class); + PreparedLayer preparedLayer3 = Mockito.mock(PreparedLayer.class); + when(executorService.submit(Mockito.any(ObtainBaseImageLayerStep.class))) + .thenReturn(Futures.immediateFuture(preparedLayer1)) + .thenReturn(Futures.immediateFuture(preparedLayer2)) + .thenReturn(Futures.immediateFuture(preparedLayer3)); + + Map> preparedLayersCache = new HashMap<>(); + + // 1. Should schedule two threads to obtain new layers. + Image image = Mockito.mock(Image.class); + when(image.getLayers()).thenReturn(ImmutableList.of(layer1, layer2)); + + stepsRunner.obtainBaseImageLayers(image, true, preparedLayersCache, progressDispatcherFactory); + Assert.assertEquals(2, preparedLayersCache.size()); // two new layers cached + Assert.assertEquals(preparedLayer1, preparedLayersCache.get(digest1).get()); + Assert.assertEquals(preparedLayer2, preparedLayersCache.get(digest2).get()); + + // 2. Should not schedule threads for existing layers. + stepsRunner.obtainBaseImageLayers(image, true, preparedLayersCache, progressDispatcherFactory); + Assert.assertEquals(2, preparedLayersCache.size()); // no new layers cached (still 2) + Assert.assertEquals(preparedLayer1, preparedLayersCache.get(digest1).get()); + Assert.assertEquals(preparedLayer2, preparedLayersCache.get(digest2).get()); + + // 3. Another image with one duplicate layer. + when(image.getLayers()).thenReturn(ImmutableList.of(layer3, layer2)); + stepsRunner.obtainBaseImageLayers(image, true, preparedLayersCache, progressDispatcherFactory); + Assert.assertEquals(3, preparedLayersCache.size()); // one new layer cached + Assert.assertEquals(preparedLayer1, preparedLayersCache.get(digest1).get()); + Assert.assertEquals(preparedLayer2, preparedLayersCache.get(digest2).get()); + Assert.assertEquals(preparedLayer3, preparedLayersCache.get(digest3).get()); + + // Total three threads scheduled for the three unique layers. + Mockito.verify(executorService, Mockito.times(3)) + .submit(Mockito.any(ObtainBaseImageLayerStep.class)); + } + + @Test + public void testIsImagePushed_skipExistingEnabledAndManifestPresent() { + Optional> manifestResult = Mockito.mock(Optional.class); + when(manifestResult.isPresent()).thenReturn(true); + System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); + + Assert.assertFalse(stepsRunner.isImagePushed(manifestResult)); + } + + @Test + public void testIsImagePushed_skipExistingImageDisabledAndManifestPresent() { + Optional> manifestResult = Mockito.mock(Optional.class); + System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "false"); + + Assert.assertTrue(stepsRunner.isImagePushed(manifestResult)); + } + + @Test + public void testIsImagePushed_skipExistingImageEnabledAndManifestNotPresent() { + Optional> manifestResult = Mockito.mock(Optional.class); + System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); + when(manifestResult.isPresent()).thenReturn(false); + + Assert.assertTrue(stepsRunner.isImagePushed(manifestResult)); + } + + @Test + public void testFetchBuildImageForLocalBuild_matchingOsAndArch() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(builtAmd64AndWindowsImage.getOs()).thenReturn("windows"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Image expectedImage = + stepsRunner.fetchBuiltImageForLocalBuild("windows", "amd64", eventHandlers); + + assertThat(expectedImage.getOs()).isEqualTo("windows"); + assertThat(expectedImage.getArchitecture()).isEqualTo("amd64"); + } + + @Test + public void testFetchBuildImageForLocalBuild_differentOs_buildImageForFirstPlatform() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtArm64AndLinuxImage.getOs()).thenReturn("linux"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Image expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("os", "arm64", eventHandlers); + + assertThat(expectedImage.getOs()).isEqualTo("linux"); + assertThat(expectedImage.getArchitecture()).isEqualTo("arm64"); + } + + @Test + public void testFetchBuildImageForLocalBuild_differentArch_buildImageForFirstPlatform() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Image expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("linux", "arch", eventHandlers); + + assertThat(expectedImage.getArchitecture()).isEqualTo("arm64"); + } + + @Test + public void testFetchBuildImageForLocalBuild_singleImage_imagePlatformDifferentFromDockerEnv() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtArm64AndLinuxImage.getOs()).thenReturn("linux"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of(baseImage1, Futures.immediateFuture(builtArm64AndLinuxImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Image expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("linux", "amd64", eventHandlers); + + assertThat(expectedImage.getOs()).isEqualTo("linux"); + assertThat(expectedImage.getArchitecture()).isEqualTo("arm64"); + } + + @Test + public void testNormalizeArchitecture_aarch64() { + assertThat(stepsRunner.normalizeArchitecture("aarch64")).isEqualTo("arm64"); + } + + @Test + public void testNormalizeArchitecture_x86_64() { + assertThat(stepsRunner.normalizeArchitecture("x86_64")).isEqualTo("amd64"); + } + + @Test + public void testNormalizeArchitecture_arm() { + assertThat(stepsRunner.normalizeArchitecture("arm")).isEqualTo("arm"); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java deleted file mode 100644 index 8d39def005..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.nio.file.Path; -import java.security.DigestException; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheFiles}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheFilesTest { - - @Mock private Path mockPath; - - @Test - public void testGetMetadataFile() { - ArgumentCaptor fileNameCaptor = ArgumentCaptor.forClass(String.class); - - Mockito.when(mockPath.resolve(fileNameCaptor.capture())).thenReturn(mockPath); - - Path metadataFile = CacheFiles.getMetadataFile(mockPath); - - Assert.assertEquals("metadata-v2.json", fileNameCaptor.getValue()); - Assert.assertEquals(mockPath, metadataFile); - } - - @Test - public void testGetLayerFile() throws DigestException { - DescriptorDigest layerDigest = - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - - ArgumentCaptor fileNameCaptor = ArgumentCaptor.forClass(String.class); - - Mockito.when(mockPath.resolve(fileNameCaptor.capture())).thenReturn(mockPath); - - Path layerFile = CacheFiles.getLayerFile(mockPath, layerDigest); - - Assert.assertEquals( - "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad.tar.gz", - fileNameCaptor.getValue()); - Assert.assertEquals(mockPath, layerFile); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java deleted file mode 100644 index ea66f3a641..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheMetadata}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheMetadataTest { - - private static CachedLayer mockCachedLayer() { - CachedLayer mockCachedLayer = Mockito.mock(CachedLayer.class); - BlobDescriptor mockBlobDescriptor = Mockito.mock(BlobDescriptor.class); - DescriptorDigest mockDescriptorDigest = Mockito.mock(DescriptorDigest.class); - Mockito.when(mockCachedLayer.getBlobDescriptor()).thenReturn(mockBlobDescriptor); - Mockito.when(mockBlobDescriptor.getDigest()).thenReturn(mockDescriptorDigest); - return mockCachedLayer; - } - - @Mock private LayerEntry mockLayerEntry; - - @Test - public void testAddLayer() { - CachedLayerWithMetadata testCachedLayerWithMetadata = - new CachedLayerWithMetadata(mockCachedLayer(), Mockito.mock(LayerMetadata.class)); - - CacheMetadata cacheMetadata = - CacheMetadata.builder().addLayer(testCachedLayerWithMetadata).build(); - - Assert.assertEquals( - Collections.singletonList(testCachedLayerWithMetadata), - cacheMetadata.getLayers().getLayers()); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_mismatchSize() { - Assert.assertFalse( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of(mockLayerEntry), ImmutableList.of())); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_extractionPath() { - ImmutableList layerEntries = - ImmutableList.of(new LayerEntry(ImmutableList.of(), "extractionPath")); - ImmutableList metadataEntries = - LayerMetadata.from( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "anotherExtractionPath")), - FileTime.fromMillis(0)) - .getEntries(); - - Assert.assertFalse( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - layerEntries, metadataEntries)); - - Assert.assertTrue( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "anotherExtractionPath")), - metadataEntries)); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_pass() { - LayerEntry layerEntry1 = - new LayerEntry( - ImmutableList.of(Paths.get("sourceFile1"), Paths.get("sourceFile2")), "extractionPath"); - LayerEntry layerEntry2 = - new LayerEntry( - ImmutableList.of(Paths.get("sourceFile3"), Paths.get("sourceFile4")), - "anotherExtractionPath"); - - ImmutableList layerEntries = ImmutableList.of(layerEntry1, layerEntry2); - ImmutableList metadataEntries = - LayerMetadata.from(ImmutableList.of(layerEntry1, layerEntry2), FileTime.fromMillis(0)) - .getEntries(); - - Assert.assertTrue( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - layerEntries, metadataEntries)); - } - - @Test - public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(6).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry = - new LayerEntry( - ImmutableList.of(Paths.get("some/source/file"), Paths.get("some/source/directory")), - "extractionPath"); - - LayerMetadata fakeExpectedSourceFilesClassesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeExpectedSourceFilesResourcesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeOtherSourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of( - new LayerEntry( - ImmutableList.of(Paths.get("not/the/same/source/file")), "extractionPath")), - FileTime.fromMillis(0)); - LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "extractionPath")), - FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeOtherSourceFilesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(1), fakeExpectedSourceFilesResourcesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(2), fakeOtherSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(3), fakeEmptySourceFilesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(4), fakeExpectedSourceFilesClassesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(5), fakeExpectedSourceFilesResourcesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of(fakeLayerEntry)).filter(); - - Assert.assertEquals(3, filteredLayers.size()); - Assert.assertEquals( - fakeExpectedSourceFilesResourcesLayerMetadata, filteredLayers.get(0).getMetadata()); - Assert.assertEquals( - fakeExpectedSourceFilesClassesLayerMetadata, filteredLayers.get(1).getMetadata()); - Assert.assertEquals( - fakeExpectedSourceFilesResourcesLayerMetadata, filteredLayers.get(2).getMetadata()); - } - - @Test - public void testFilter_byNoEntries() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry = - new LayerEntry( - ImmutableList.of(Paths.get("some/source/file", "some/source/directory")), - "extractionPath"); - - LayerMetadata fakeSourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeNoEntriesLayerMetadata = - new LayerMetadata(ImmutableList.of(), FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(1), fakeNoEntriesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of()).filter(); - - Assert.assertEquals(1, filteredLayers.size()); - Assert.assertEquals(fakeNoEntriesLayerMetadata, filteredLayers.get(0).getMetadata()); - } - - @Test - public void testFilter_byEmptySourceFiles() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry = - new LayerEntry( - ImmutableList.of(Paths.get("some/source/file", "some/source/directory")), - "extractionPath"); - LayerEntry fakeLayerEntryWithNoSourceFiles = - new LayerEntry(ImmutableList.of(), "extractionPath"); - - LayerMetadata fakeSourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(fakeLayerEntryWithNoSourceFiles), FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(1), fakeEmptySourceFilesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata - .filterLayers() - .byLayerEntries(ImmutableList.of(fakeLayerEntryWithNoSourceFiles)) - .filter(); - - Assert.assertEquals(1, filteredLayers.size()); - Assert.assertEquals(fakeEmptySourceFilesLayerMetadata, filteredLayers.get(0).getMetadata()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java deleted file mode 100644 index f99b52f5b1..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.collect.ImmutableList; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.Collections; -import java.util.List; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheMetadataTranslator}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheMetadataTranslatorTest { - - private static final List CLASSES_LAYER_SOURCE_FILES = - Collections.singletonList(Paths.get("some", "source", "path").toString()); - private static final String CLASSES_LAYER_EXTRACTION_PATH = "some/extraction/path"; - private static final FileTime CLASSES_LAYER_LAST_MODIFIED_TIME = - FileTime.fromMillis(255073580723571L); - - @Mock private Path mockPath; - - private BlobDescriptor baseLayerBlobDescriptor; - private DescriptorDigest baseLayerDiffId; - private BlobDescriptor classesLayerBlobDescriptor; - private DescriptorDigest classesLayerDiffId; - - @Before - public void setUp() throws DigestException { - baseLayerBlobDescriptor = - new BlobDescriptor( - 631, - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef")); - baseLayerDiffId = - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"); - classesLayerBlobDescriptor = - new BlobDescriptor( - 223, - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); - classesLayerDiffId = - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"); - } - - @Test - public void testFromTemplate() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - Path fakePath = Paths.get("fake/path"); - - // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); - - // Deserializes into a metadata JSON object. - CacheMetadataTemplate metadataTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, CacheMetadataTemplate.class); - - CacheMetadata cacheMetadata = CacheMetadataTranslator.fromTemplate(metadataTemplate, fakePath); - - List layers = cacheMetadata.getLayers().getLayers(); - - // Checks that the base layer was translated correctly. - CachedLayerWithMetadata baseLayer = layers.get(0); - Assert.assertEquals( - CacheFiles.getLayerFile(fakePath, baseLayerBlobDescriptor.getDigest()), - baseLayer.getContentFile()); - Assert.assertEquals(baseLayerBlobDescriptor, baseLayer.getBlobDescriptor()); - Assert.assertEquals(baseLayerDiffId, baseLayer.getDiffId()); - - // Checks that the classses layer was translated correctly. - CachedLayerWithMetadata classesLayer = layers.get(1); - Assert.assertEquals( - CacheFiles.getLayerFile(fakePath, classesLayerBlobDescriptor.getDigest()), - classesLayer.getContentFile()); - Assert.assertEquals(classesLayerBlobDescriptor, classesLayer.getBlobDescriptor()); - Assert.assertEquals(classesLayerDiffId, classesLayer.getDiffId()); - Assert.assertNotNull(classesLayer.getMetadata()); - Assert.assertEquals( - CLASSES_LAYER_SOURCE_FILES, - classesLayer.getMetadata().getEntries().get(0).getSourceFilesStrings()); - Assert.assertEquals( - CLASSES_LAYER_EXTRACTION_PATH, - classesLayer.getMetadata().getEntries().get(0).getExtractionPath()); - Assert.assertEquals( - CLASSES_LAYER_LAST_MODIFIED_TIME, classesLayer.getMetadata().getLastModifiedTime()); - } - - @Test - public void testToTemplate() throws URISyntaxException, IOException { - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); - String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - - CachedLayer baseCachedLayer = - new CachedLayer(mockPath, baseLayerBlobDescriptor, baseLayerDiffId); - CachedLayerWithMetadata baseLayer = new CachedLayerWithMetadata(baseCachedLayer, null); - - CachedLayer classesCachedLayer = - new CachedLayer(mockPath, classesLayerBlobDescriptor, classesLayerDiffId); - LayerMetadata classesLayerMetadata = - LayerMetadata.from( - ImmutableList.of( - new LayerEntry( - CLASSES_LAYER_SOURCE_FILES - .stream() - .map(Paths::get) - .collect(ImmutableList.toImmutableList()), - CLASSES_LAYER_EXTRACTION_PATH)), - CLASSES_LAYER_LAST_MODIFIED_TIME); - CachedLayerWithMetadata classesLayer = - new CachedLayerWithMetadata(classesCachedLayer, classesLayerMetadata); - - CacheMetadata cacheMetadata = - CacheMetadata.builder().addLayer(baseLayer).addLayer(classesLayer).build(); - - CacheMetadataTemplate cacheMetadataTemplate = CacheMetadataTranslator.toTemplate(cacheMetadata); - - // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(cacheMetadataTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java deleted file mode 100644 index c66041d657..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.Comparator; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link CacheReader}. */ -public class CacheReaderTest { - - // TODO: Replace with filesystem.DirectoryWalker. - private static void copyDirectory(Path source, Path destination) throws IOException { - new DirectoryWalker(source) - .filter(path -> !path.equals(source)) - .walk( - path -> { - Path newPath = destination.resolve(source.relativize(path)); - Files.copy(path, newPath); - }); - } - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private Path testCacheFolder; - - @Before - public void setUp() throws IOException, URISyntaxException { - testCacheFolder = temporaryFolder.newFolder().toPath(); - - // Copies the test resource cache to the temporary test cache folder. - Path resourceCache = Paths.get(Resources.getResource("cache").toURI()); - copyDirectory(resourceCache, testCacheFolder); - } - - @Test - public void testAreBaseImageLayersCached() - throws DigestException, CacheMetadataCorruptedException, IOException { - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - Assert.assertNotNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"))); - Assert.assertNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"))); - Assert.assertNotNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"))); - } - } - - @Test - public void testGetLayerFile() throws CacheMetadataCorruptedException, IOException { - Path expectedFile = - testCacheFolder.resolve( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz"); - - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - - Assert.assertEquals( - expectedFile, - cacheReader.getLayerFile( - ImmutableList.of( - new LayerEntry( - ImmutableList.of(Paths.get("some", "source", "directory")), - "some/extraction/path")))); - Assert.assertNull(cacheReader.getLayerFile(ImmutableList.of())); - } - } - - @Test - public void testGetUpToDateLayerBySourceFiles() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - // The two last modified times to use. Must be in thousands as most file time granularity is in - // seconds. - FileTime olderLastModifiedTime = FileTime.fromMillis(1000); - FileTime newerLastModifiedTime = FileTime.fromMillis(2000); - - // Copies test files to a modifiable temporary folder. - Path resourceSourceFiles = Paths.get(Resources.getResource("layer").toURI()); - Path testSourceFiles = temporaryFolder.newFolder().toPath(); - copyDirectory(resourceSourceFiles, testSourceFiles); - - // Walk the files in reverse order so that the subfiles are changed before the parent - // directories are. - ImmutableList paths = new DirectoryWalker(testSourceFiles).walk(); - paths = ImmutableList.sortedCopyOf(Comparator.reverseOrder(), paths); - for (Path path : paths) { - Files.setLastModifiedTime(path, olderLastModifiedTime); - } - - // Sets the metadata source file to the new temporary folder. - CachedLayerWithMetadata classesCachedLayer; - try (Cache cache = Cache.init(testCacheFolder)) { - ImageLayers cachedLayers = - cache.getMetadata().filterLayers().filter(); - - Assert.assertEquals(3, cachedLayers.size()); - classesCachedLayer = cachedLayers.get(2); - - Assert.assertNotNull(classesCachedLayer.getMetadata()); - classesCachedLayer - .getMetadata() - .setEntry(ImmutableList.of(testSourceFiles.toString()), "/some/extraction/path"); - } - - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - - ImmutableList upToDateLayerEntries = - ImmutableList.of( - new LayerEntry(ImmutableList.of(testSourceFiles), "/some/extraction/path")); - - CachedLayerWithMetadata upToDateLayer = - cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries); - Assert.assertNotNull(upToDateLayer); - Assert.assertEquals( - classesCachedLayer.getBlobDescriptor(), upToDateLayer.getBlobDescriptor()); - - // Changes a file and checks that the change is detected. - Files.setLastModifiedTime( - testSourceFiles.resolve("a").resolve("b").resolve("bar"), newerLastModifiedTime); - Assert.assertNull(cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries)); - Assert.assertNull( - cacheReader.getUpToDateLayerByLayerEntries( - ImmutableList.of( - new LayerEntry(ImmutableList.of(testSourceFiles), "extractionPath")))); - - // Any non-cached directory should be deemed modified. - Assert.assertNull( - cacheReader.getUpToDateLayerByLayerEntries( - ImmutableList.of( - new LayerEntry(ImmutableList.of(resourceSourceFiles), "/some/extraction/path")))); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java new file mode 100644 index 0000000000..c81a80c8ef --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link CacheStorageFiles}. */ +public class CacheStorageFilesTest { + + private static final CacheStorageFiles TEST_CACHE_STORAGE_FILES = + new CacheStorageFiles(Paths.get("cache/directory")); + + @Test + public void testIsLayerFile() { + Assert.assertTrue( + CacheStorageFiles.isLayerFile( + Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + Assert.assertTrue( + CacheStorageFiles.isLayerFile( + Paths.get("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); + Assert.assertFalse(CacheStorageFiles.isLayerFile(Paths.get("is.not.layer.file"))); + } + + @Test + public void testGetDiffId() throws DigestException, CacheCorruptedException { + Assert.assertEquals( + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( + Paths.get( + "layer", + "file", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); + Assert.assertEquals( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( + Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + } + + @Test + public void testGetDiffId_corrupted() { + try { + TEST_CACHE_STORAGE_FILES.getDigestFromFilename(Paths.get("not long enough")); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Layer file did not include valid hash: not long enough. " + + "You may need to clear the cache by deleting the '" + + TEST_CACHE_STORAGE_FILES.getCacheDirectory() + + "' directory", + ex.getMessage()); + MatcherAssert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); + } + + try { + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( + Paths.get( + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + Assert.fail("Should have thrown CacheCorruptedException"); + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Layer file did not include valid hash: " + + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb. " + + "You may need to clear the cache by deleting the '" + + TEST_CACHE_STORAGE_FILES.getCacheDirectory() + + "' directory", + ex.getMessage()); + MatcherAssert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); + } + } + + @Test + public void testGetLayerFile() throws DigestException { + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + DescriptorDigest diffId = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "layers", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TEST_CACHE_STORAGE_FILES.getLayerFile(layerDigest, diffId)); + } + + @Test + public void testGetLayerFilename() throws DigestException { + DescriptorDigest diffId = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + Assert.assertEquals( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TEST_CACHE_STORAGE_FILES.getLayerFilename(diffId)); + } + + @Test + public void testGetSelectorFile() throws DigestException { + DescriptorDigest selector = + DescriptorDigest.fromHash( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "selectors", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + TEST_CACHE_STORAGE_FILES.getSelectorFile(selector)); + } + + @Test + public void testGetLayersDirectory() { + Assert.assertEquals( + Paths.get("cache", "directory", "layers"), TEST_CACHE_STORAGE_FILES.getLayersDirectory()); + } + + @Test + public void testGetLayerDirectory() throws DigestException { + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "layers", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + TEST_CACHE_STORAGE_FILES.getLayerDirectory(layerDigest)); + } + + @Test + public void testGetTemporaryDirectory() { + Assert.assertEquals( + Paths.get("cache/directory/tmp"), TEST_CACHE_STORAGE_FILES.getTemporaryDirectory()); + } + + @Test + public void testGetImagesDirectory() { + Assert.assertEquals( + Paths.get("cache/directory/images"), TEST_CACHE_STORAGE_FILES.getImagesDirectory()); + } + + @Test + public void testGetImageDirectory() throws InvalidImageReferenceException { + Path imagesDirectory = Paths.get("cache", "directory", "images"); + Assert.assertEquals(imagesDirectory, TEST_CACHE_STORAGE_FILES.getImagesDirectory()); + + Assert.assertEquals( + imagesDirectory.resolve("reg.istry/repo/sitory!tag"), + TEST_CACHE_STORAGE_FILES.getImageDirectory( + ImageReference.parse("reg.istry/repo/sitory:tag"))); + Assert.assertEquals( + imagesDirectory.resolve( + "reg.istry/repo!sha256!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + TEST_CACHE_STORAGE_FILES.getImageDirectory( + ImageReference.parse( + "reg.istry/repo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + Assert.assertEquals( + imagesDirectory.resolve("reg.istry!5000/repo/sitory!tag"), + TEST_CACHE_STORAGE_FILES.getImageDirectory( + ImageReference.parse("reg.istry:5000/repo/sitory:tag"))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java new file mode 100644 index 0000000000..47f7ff696f --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -0,0 +1,684 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate.ContentDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +/** Tests for {@link CacheStorageReader}. */ +public class CacheStorageReaderTest { + + private static void setupCachedMetadataV21(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + loadJsonResource("core/json/v21manifest.json", V21ManifestTemplate.class), null); + try (OutputStream out = + Files.newOutputStream(imageDirectory.resolve("manifests_configs.json"))) { + JsonTemplateMapper.writeTo( + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)), out); + } + } + + private static void setupCachedMetadataV22(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + loadJsonResource("core/json/v22manifest.json", V22ManifestTemplate.class), + loadJsonResource( + "core/json/containerconfig.json", ContainerConfigurationTemplate.class), + "sha256:digest"); + try (OutputStream out = + Files.newOutputStream(imageDirectory.resolve("manifests_configs.json"))) { + JsonTemplateMapper.writeTo( + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)), out); + } + } + + private static void setupCachedMetadataV22ManifestList(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + + ManifestTemplate v22ManifestList = + loadJsonResource("core/json/v22manifest_list.json", V22ManifestListTemplate.class); + ManifestTemplate v22Manifest1 = + loadJsonResource("core/json/v22manifest.json", V22ManifestTemplate.class); + ManifestTemplate v22Manifest2 = + loadJsonResource("core/json/translated_v22manifest.json", V22ManifestTemplate.class); + ContainerConfigurationTemplate containerConfig = + loadJsonResource("core/json/containerconfig.json", ContainerConfigurationTemplate.class); + List manifestsAndConfigs = + Arrays.asList( + new ManifestAndConfigTemplate(v22Manifest1, containerConfig, "sha256:digest"), + new ManifestAndConfigTemplate(v22Manifest2, containerConfig, "sha256:digest")); + try (OutputStream out = + Files.newOutputStream(imageDirectory.resolve("manifests_configs.json"))) { + JsonTemplateMapper.writeTo( + new ImageMetadataTemplate(v22ManifestList, manifestsAndConfigs), out); + } + } + + private static void setupCachedMetadataOci(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + loadJsonResource("core/json/ocimanifest.json", OciManifestTemplate.class), + loadJsonResource( + "core/json/containerconfig.json", ContainerConfigurationTemplate.class), + "sha256:digest"); + try (OutputStream out = + Files.newOutputStream(imageDirectory.resolve("manifests_configs.json"))) { + JsonTemplateMapper.writeTo( + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)), out); + } + } + + private static void setupCachedMetadataOciImageIndex(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + + ManifestTemplate ociIndex = loadJsonResource("core/json/ociindex.json", OciIndexTemplate.class); + ManifestTemplate ociManifest = + loadJsonResource("core/json/ocimanifest.json", OciManifestTemplate.class); + ContainerConfigurationTemplate containerConfig = + loadJsonResource("core/json/containerconfig.json", ContainerConfigurationTemplate.class); + List manifestsAndConfigs = + Arrays.asList(new ManifestAndConfigTemplate(ociManifest, containerConfig, "sha256:digest")); + try (OutputStream out = + Files.newOutputStream(imageDirectory.resolve("manifests_configs.json"))) { + JsonTemplateMapper.writeTo(new ImageMetadataTemplate(ociIndex, manifestsAndConfigs), out); + } + } + + private static T loadJsonResource(String path, Class jsonClass) + throws URISyntaxException, IOException { + return JsonTemplateMapper.readJsonFromFile( + Paths.get(Resources.getResource(path).toURI()), jsonClass); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Path cacheDirectory; + private DescriptorDigest layerDigest1; + private DescriptorDigest layerDigest2; + private CacheStorageFiles cacheStorageFiles; + private CacheStorageReader cacheStorageReader; + + @Before + public void setUp() throws DigestException, IOException { + cacheDirectory = temporaryFolder.newFolder().toPath(); + cacheStorageFiles = new CacheStorageFiles(cacheDirectory); + cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + layerDigest1 = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + layerDigest2 = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + } + + @Test + public void testRetrieveMetadata_v21SingleManifest() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataV21(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + Assert.assertNull(metadata.getManifestList()); + Assert.assertEquals(1, metadata.getManifestsAndConfigs().size()); + Assert.assertNull(metadata.getManifestsAndConfigs().get(0).getConfig()); + + V21ManifestTemplate manifestTemplate = + (V21ManifestTemplate) metadata.getManifestsAndConfigs().get(0).getManifest(); + Assert.assertEquals(1, manifestTemplate.getSchemaVersion()); + Assert.assertEquals(2, manifestTemplate.getLayerDigests().size()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifestTemplate.getLayerDigests().get(0).getHash()); + Assert.assertEquals( + "5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3", + manifestTemplate.getLayerDigests().get(1).getHash()); + } + + @Test + public void testRetrieveMetadata_v22SingleManifest() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataV22(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + Assert.assertNull(metadata.getManifestList()); + Assert.assertEquals(1, metadata.getManifestsAndConfigs().size()); + + V22ManifestTemplate manifestTemplate = + (V22ManifestTemplate) metadata.getManifestsAndConfigs().get(0).getManifest(); + Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifestTemplate.getContainerConfiguration().getDigest().getHash()); + } + + @Test + public void testRetrieveMetadata_v22ManifestList() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataV22ManifestList(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + + MatcherAssert.assertThat( + metadata.getManifestList(), CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + List manifestDescriptors = + ((V22ManifestListTemplate) metadata.getManifestList()).getManifests(); + + Assert.assertEquals(3, manifestDescriptors.size()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + manifestDescriptors.get(0).getDigest()); + Assert.assertEquals( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + manifestDescriptors.get(1).getDigest()); + Assert.assertEquals( + "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + manifestDescriptors.get(2).getDigest()); + + Assert.assertEquals(2, metadata.getManifestsAndConfigs().size()); + ManifestAndConfigTemplate manifestAndConfig1 = metadata.getManifestsAndConfigs().get(0); + ManifestAndConfigTemplate manifestAndConfig2 = metadata.getManifestsAndConfigs().get(1); + + V22ManifestTemplate manifest1 = (V22ManifestTemplate) manifestAndConfig1.getManifest(); + V22ManifestTemplate manifest2 = (V22ManifestTemplate) manifestAndConfig2.getManifest(); + Assert.assertEquals(2, manifest1.getSchemaVersion()); + Assert.assertEquals(2, manifest2.getSchemaVersion()); + Assert.assertEquals(1, manifest1.getLayers().size()); + Assert.assertEquals(1, manifest2.getLayers().size()); + Assert.assertEquals( + "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236", + manifest1.getLayers().get(0).getDigest().getHash()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifest2.getLayers().get(0).getDigest().getHash()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifest1.getContainerConfiguration().getDigest().getHash()); + Assert.assertEquals( + "2000a70a1ce8bba401c493376fdb9eb81bcf7155212f4ce4c6ff96e577718a49", + manifest2.getContainerConfiguration().getDigest().getHash()); + + Assert.assertEquals("wasm", manifestAndConfig1.getConfig().getArchitecture()); + Assert.assertEquals("wasm", manifestAndConfig2.getConfig().getArchitecture()); + } + + @Test + public void testRetrieveMetadata_ociSingleManifest() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataOci(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + Assert.assertNull(metadata.getManifestList()); + Assert.assertEquals(1, metadata.getManifestsAndConfigs().size()); + + OciManifestTemplate manifestTemplate = + (OciManifestTemplate) metadata.getManifestsAndConfigs().get(0).getManifest(); + Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifestTemplate.getContainerConfiguration().getDigest().getHash()); + } + + @Test + public void testRetrieveMetadata_ociImageIndex() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataOciImageIndex(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + + MatcherAssert.assertThat( + metadata.getManifestList(), CoreMatchers.instanceOf(OciIndexTemplate.class)); + List manifestDescriptors = + ((OciIndexTemplate) metadata.getManifestList()).getManifests(); + + Assert.assertEquals(1, manifestDescriptors.size()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifestDescriptors.get(0).getDigest().getHash()); + + Assert.assertEquals(1, metadata.getManifestsAndConfigs().size()); + ManifestAndConfigTemplate manifestAndConfig = metadata.getManifestsAndConfigs().get(0); + + OciManifestTemplate manifestTemplate = (OciManifestTemplate) manifestAndConfig.getManifest(); + Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + manifestTemplate.getContainerConfiguration().getDigest().getHash()); + + Assert.assertEquals("wasm", manifestAndConfig.getConfig().getArchitecture()); + } + + @Test + public void testRetrieveMetadata_containerConfiguration() + throws IOException, URISyntaxException, CacheCorruptedException { + setupCachedMetadataV22(cacheDirectory); + + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + Assert.assertNull(metadata.getManifestList()); + Assert.assertEquals(1, metadata.getManifestsAndConfigs().size()); + + ContainerConfigurationTemplate configurationTemplate = + metadata.getManifestsAndConfigs().get(0).getConfig(); + Assert.assertNotNull(configurationTemplate); + Assert.assertEquals("wasm", configurationTemplate.getArchitecture()); + Assert.assertEquals("js", configurationTemplate.getOs()); + } + + @Test + public void testRetrieve() throws IOException, CacheCorruptedException { + // Creates the test layer directory. + DescriptorDigest layerDigest = layerDigest1; + DescriptorDigest layerDiffId = layerDigest2; + Files.createDirectories(cacheStorageFiles.getLayerDirectory(layerDigest)); + try (OutputStream out = + Files.newOutputStream(cacheStorageFiles.getLayerFile(layerDigest, layerDiffId))) { + out.write("layerBlob".getBytes(StandardCharsets.UTF_8)); + } + + // Checks that the CachedLayer is retrieved correctly. + Optional optionalCachedLayer = cacheStorageReader.retrieve(layerDigest); + Assert.assertTrue(optionalCachedLayer.isPresent()); + Assert.assertEquals(layerDigest, optionalCachedLayer.get().getDigest()); + Assert.assertEquals(layerDiffId, optionalCachedLayer.get().getDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCachedLayer.get().getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCachedLayer.get().getBlob())); + + // Checks that multiple .layer files means the cache is corrupted. + Files.createFile(cacheStorageFiles.getLayerFile(layerDigest, layerDigest)); + try { + cacheStorageReader.retrieve(layerDigest); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "No or multiple layer files found for layer hash " + + layerDigest.getHash() + + " in directory: " + + cacheStorageFiles.getLayerDirectory(layerDigest))); + } + } + + @Test + public void testRetrieveTarLayer() throws IOException, CacheCorruptedException { + // Creates the test layer directory. + Path localDirectory = cacheStorageFiles.getLocalDirectory(); + DescriptorDigest layerDigest = layerDigest1; + DescriptorDigest layerDiffId = layerDigest2; + Files.createDirectories(localDirectory.resolve(layerDiffId.getHash())); + try (OutputStream out = + Files.newOutputStream( + localDirectory.resolve(layerDiffId.getHash()).resolve(layerDigest.getHash()))) { + out.write("layerBlob".getBytes(StandardCharsets.UTF_8)); + } + + // Checks that the CachedLayer is retrieved correctly. + Optional optionalCachedLayer = cacheStorageReader.retrieveTarLayer(layerDiffId); + Assert.assertTrue(optionalCachedLayer.isPresent()); + Assert.assertEquals(layerDigest, optionalCachedLayer.get().getDigest()); + Assert.assertEquals(layerDiffId, optionalCachedLayer.get().getDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCachedLayer.get().getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCachedLayer.get().getBlob())); + + // Checks that multiple layer files means the cache is corrupted. + Files.createFile(localDirectory.resolve(layerDiffId.getHash()).resolve(layerDiffId.getHash())); + try { + cacheStorageReader.retrieveTarLayer(layerDiffId); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "No or multiple layer files found for layer hash " + + layerDiffId.getHash() + + " in directory: " + + localDirectory.resolve(layerDiffId.getHash()))); + } + } + + @Test + public void testRetrieveLocalConfig() throws IOException, URISyntaxException, DigestException { + Path configDirectory = cacheDirectory.resolve("local").resolve("config"); + Files.createDirectories(configDirectory); + Files.copy( + Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()), + configDirectory.resolve( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + + ContainerConfigurationTemplate configurationTemplate = + cacheStorageReader + .retrieveLocalConfig( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .get(); + Assert.assertEquals("wasm", configurationTemplate.getArchitecture()); + Assert.assertEquals("js", configurationTemplate.getOs()); + + Optional missingConfigurationTemplate = + cacheStorageReader.retrieveLocalConfig( + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + Assert.assertFalse(missingConfigurationTemplate.isPresent()); + } + + @Test + public void testSelect_invalidLayerDigest() throws IOException { + DescriptorDigest selector = layerDigest1; + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); + Files.createDirectories(selectorFile.getParent()); + Files.write(selectorFile, "not a valid layer digest".getBytes(StandardCharsets.UTF_8)); + + try { + cacheStorageReader.select(selector); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "Expected valid layer digest as contents of selector file `" + + selectorFile + + "` for selector `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, but got: not a valid layer digest")); + } + } + + @Test + public void testSelect() throws IOException, CacheCorruptedException { + DescriptorDigest selector = layerDigest1; + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); + Files.createDirectories(selectorFile.getParent()); + Files.write(selectorFile, layerDigest2.getHash().getBytes(StandardCharsets.UTF_8)); + + Optional selectedLayerDigest = cacheStorageReader.select(selector); + Assert.assertTrue(selectedLayerDigest.isPresent()); + Assert.assertEquals(layerDigest2, selectedLayerDigest.get()); + } + + @Test + public void testVerifyImageMetadata_manifestCacheEmpty() { + ImageMetadataTemplate metadata = new ImageMetadataTemplate(null, Collections.emptyList()); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Manifest cache empty")); + } + } + + @Test + public void testVerifyImageMetadata_manifestListMissing() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestListTemplate(), new ContainerConfigurationTemplate()); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig, manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Manifest list missing")); + } + } + + @Test + public void testVerifyImageMetadata_manifestsMissing() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate(null, new ContainerConfigurationTemplate()); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Manifest(s) missing")); + } + } + + @Test + public void testVerifyImageMetadata_schema1ManifestsCorrupted_manifestListExists() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate(new V21ManifestTemplate(), null); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(new V22ManifestListTemplate(), Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("Schema 1 manifests corrupted")); + } + } + + @Test + public void testVerifyImageMetadata_schema1ManifestsCorrupted_containerConfigExists() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V21ManifestTemplate(), new ContainerConfigurationTemplate()); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("Schema 1 manifests corrupted")); + } + } + + @Test + public void testVerifyImageMetadata_schema2ManifestsCorrupted_nullContainerConfig() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate(new V22ManifestTemplate(), null, "sha256:digest"); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("Schema 2 manifests corrupted")); + } + } + + @Test + public void testVerifyImageMetadata_schema2ManifestsCorrupted_nullManifestDigest() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestTemplate(), new ContainerConfigurationTemplate(), null); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(new V22ManifestListTemplate(), Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("Schema 2 manifests corrupted")); + } + } + + @Test + public void testVerifyImageMetadata_unknownManifestType() { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + Mockito.mock(ManifestTemplate.class), new ContainerConfigurationTemplate()); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + try { + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + Assert.fail(); + } catch (CacheCorruptedException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Unknown manifest type:")); + } + } + + @Test + public void testVerifyImageMetadata_validV21() throws CacheCorruptedException { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate(new V21ManifestTemplate(), null); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + // should pass without CacheCorruptedException + } + + @Test + public void testVerifyImageMetadata_validV22() throws CacheCorruptedException { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestTemplate(), new ContainerConfigurationTemplate()); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + // should pass without CacheCorruptedException + } + + @Test + public void testVerifyImageMetadata_validV22ManifestList() throws CacheCorruptedException { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new V22ManifestTemplate(), new ContainerConfigurationTemplate(), "sha256:digest"); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate( + new V22ManifestListTemplate(), Arrays.asList(manifestAndConfig, manifestAndConfig)); + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + // should pass without CacheCorruptedException + } + + @Test + public void testVerifyImageMetadata_validOci() throws CacheCorruptedException { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new OciManifestTemplate(), new ContainerConfigurationTemplate(), "sha256:digest"); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig)); + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + // should pass without CacheCorruptedException + } + + @Test + public void testVerifyImageMetadata_validOciImageIndex() throws CacheCorruptedException { + ManifestAndConfigTemplate manifestAndConfig = + new ManifestAndConfigTemplate( + new OciManifestTemplate(), new ContainerConfigurationTemplate(), "sha256:digest"); + ImageMetadataTemplate metadata = + new ImageMetadataTemplate( + new OciIndexTemplate(), Arrays.asList(manifestAndConfig, manifestAndConfig)); + CacheStorageReader.verifyImageMetadata(metadata, Paths.get("/cache/dir")); + // should pass without CacheCorruptedException + } + + @Test + public void testAllLayersCached_v21SingleManifest() + throws IOException, CacheCorruptedException, DigestException, URISyntaxException { + setupCachedMetadataV21(cacheDirectory); + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + V21ManifestTemplate manifest = + (V21ManifestTemplate) metadata.getManifestsAndConfigs().get(0).getManifest(); + DescriptorDigest firstLayerDigest = manifest.getLayerDigests().get(0); + DescriptorDigest secondLayerDigest = manifest.getLayerDigests().get(1); + + // Create only one of the layer directories so that layers are only partially cached. + Files.createDirectories(cacheStorageFiles.getLayerDirectory(firstLayerDigest)); + boolean checkWithPartialLayersCached = cacheStorageReader.areAllLayersCached(manifest); + // Create the other layer directory so that all layers are cached. + Files.createDirectories(cacheStorageFiles.getLayerDirectory(secondLayerDigest)); + boolean checkWithAllLayersCached = cacheStorageReader.areAllLayersCached(manifest); + + assertThat(checkWithPartialLayersCached).isFalse(); + assertThat(checkWithAllLayersCached).isTrue(); + } + + @Test + public void testAllLayersCached_v22SingleManifest() + throws IOException, CacheCorruptedException, DigestException, URISyntaxException { + setupCachedMetadataV22(cacheDirectory); + ImageMetadataTemplate metadata = + cacheStorageReader.retrieveMetadata(ImageReference.of("test", "image", "tag")).get(); + V22ManifestTemplate manifest = + (V22ManifestTemplate) metadata.getManifestsAndConfigs().get(0).getManifest(); + DescriptorDigest layerDigest = manifest.getLayers().get(0).getDigest(); + + boolean checkBeforeLayerCached = cacheStorageReader.areAllLayersCached(manifest); + // Create the single layer directory so that all layers are cached. + Files.createDirectories(cacheStorageFiles.getLayerDirectory(layerDigest)); + boolean checkAfterLayerCached = cacheStorageReader.areAllLayersCached(manifest); + + assertThat(checkBeforeLayerCached).isFalse(); + assertThat(checkAfterLayerCached).isTrue(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java new file mode 100644 index 0000000000..63fa3e29e1 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -0,0 +1,416 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate.ContentDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ImageMetadataTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfigTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; +import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link CacheStorageWriter}. */ +public class CacheStorageWriterTest { + + private static BlobDescriptor getDigest(Blob blob) throws IOException { + return blob.writeTo(ByteStreams.nullOutputStream()); + } + + private static Blob compress(Blob blob) { + // Don't use GzipCompressorOutputStream, which has different defaults than GZIPOutputStream + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }, + false); + } + + private static Blob compress(Blob blob, String compressorName) { + return Blobs.from( + outputStream -> { + try (OutputStream compressorStream = + CompressorStreamFactory.getSingleton() + .createCompressorOutputStream(compressorName, outputStream)) { + blob.writeTo(compressorStream); + } catch (CompressorException e) { + throw new RuntimeException(e); + } + }, + false); + } + + private static Blob decompress(Blob blob) throws IOException { + try { + return Blobs.from( + CompressorStreamFactory.getSingleton() + .createCompressorInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); + } catch (CompressorException e) { + throw new IOException(e); + } + } + + private static T loadJsonResource(String path, Class jsonClass) + throws URISyntaxException, IOException { + return JsonTemplateMapper.readJsonFromFile( + Paths.get(Resources.getResource(path).toURI()), jsonClass); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private CacheStorageFiles cacheStorageFiles; + private CacheStorageWriter cacheStorageWriter; + private Path cacheRoot; + + @Before + public void setUp() throws IOException { + cacheRoot = temporaryFolder.newFolder().toPath(); + cacheStorageFiles = new CacheStorageFiles(cacheRoot); + cacheStorageWriter = new CacheStorageWriter(cacheStorageFiles); + } + + @Test + public void testWriteCompressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + Blob compressedLayerBlob = compress(uncompressedLayerBlob); + CachedLayer cachedLayer = cacheStorageWriter.writeCompressed(compressedLayerBlob); + + verifyCachedLayer(cachedLayer, uncompressedLayerBlob, compressedLayerBlob); + } + + @Test + public void testWriteZstdCompressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + Blob compressedLayerBlob = compress(uncompressedLayerBlob, CompressorStreamFactory.ZSTANDARD); + + CachedLayer cachedLayer = cacheStorageWriter.writeCompressed(compressedLayerBlob); + + verifyCachedLayer(cachedLayer, uncompressedLayerBlob, compressedLayerBlob); + } + + @Test(expected = IOException.class) + public void testWriteCompressWhenUncompressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + // The detection of compression algorithm will fail + cacheStorageWriter.writeCompressed(uncompressedLayerBlob); + } + + @Test + public void testWriteUncompressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + DescriptorDigest layerDigest = getDigest(compress(uncompressedLayerBlob)).getDigest(); + DescriptorDigest selector = getDigest(Blobs.from("selector")).getDigest(); + + CachedLayer cachedLayer = cacheStorageWriter.writeUncompressed(uncompressedLayerBlob, selector); + + verifyCachedLayer(cachedLayer, uncompressedLayerBlob, compress(uncompressedLayerBlob)); + + // Verifies that the files are present. + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); + Assert.assertTrue(Files.exists(selectorFile)); + Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); + } + + @Test + public void testWriteTarLayer() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + DescriptorDigest diffId = getDigest(uncompressedLayerBlob).getDigest(); + + CachedLayer cachedLayer = + cacheStorageWriter.writeTarLayer(diffId, compress(uncompressedLayerBlob)); + + BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); + + // Verifies cachedLayer is correct. + Assert.assertEquals(layerBlobDescriptor.getDigest(), cachedLayer.getDigest()); + Assert.assertEquals(diffId, cachedLayer.getDiffId()); + Assert.assertEquals(layerBlobDescriptor.getSize(), cachedLayer.getSize()); + Assert.assertArrayEquals( + Blobs.writeToByteArray(uncompressedLayerBlob), + Blobs.writeToByteArray(decompress(cachedLayer.getBlob()))); + + // Verifies that the files are present. + Assert.assertTrue( + Files.exists( + cacheStorageFiles + .getLocalDirectory() + .resolve(cachedLayer.getDiffId().getHash()) + .resolve(cachedLayer.getDigest().getHash()))); + } + + @Test + public void testWriteMetadata_v21() + throws IOException, URISyntaxException, InvalidImageReferenceException { + V21ManifestTemplate v21Manifest = + loadJsonResource("core/json/v21manifest.json", V21ManifestTemplate.class); + ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); + + ManifestAndConfigTemplate manifestAndConfig = new ManifestAndConfigTemplate(v21Manifest, null); + cacheStorageWriter.writeMetadata( + imageReference, new ImageMetadataTemplate(null, Arrays.asList(manifestAndConfig))); + + Path savedMetadataPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/manifests_configs.json"); + Assert.assertTrue(Files.exists(savedMetadataPath)); + + ImageMetadataTemplate savedMetadata = + JsonTemplateMapper.readJsonFromFile(savedMetadataPath, ImageMetadataTemplate.class); + Assert.assertNull(savedMetadata.getManifestList()); + Assert.assertEquals(1, savedMetadata.getManifestsAndConfigs().size()); + + ManifestAndConfigTemplate savedManifestAndConfig = + savedMetadata.getManifestsAndConfigs().get(0); + Assert.assertNull(savedManifestAndConfig.getConfig()); + + V21ManifestTemplate savedManifest = (V21ManifestTemplate) savedManifestAndConfig.getManifest(); + Assert.assertEquals( + "ppc64le", savedManifest.getContainerConfiguration().get().getArchitecture()); + } + + @Test + public void testWriteMetadata_v22() + throws IOException, URISyntaxException, InvalidImageReferenceException { + ContainerConfigurationTemplate containerConfig = + loadJsonResource("core/json/containerconfig.json", ContainerConfigurationTemplate.class); + V22ManifestTemplate manifest1 = + loadJsonResource("core/json/v22manifest.json", V22ManifestTemplate.class); + V22ManifestTemplate manifest2 = + loadJsonResource( + "core/json/v22manifest_optional_properties.json", V22ManifestTemplate.class); + V22ManifestListTemplate manifestList = + loadJsonResource("core/json/v22manifest_list.json", V22ManifestListTemplate.class); + + ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); + + List manifestsAndConfigs = + Arrays.asList( + new ManifestAndConfigTemplate(manifest1, containerConfig, "sha256:digest"), + new ManifestAndConfigTemplate(manifest2, containerConfig, "sha256:digest")); + cacheStorageWriter.writeMetadata( + imageReference, new ImageMetadataTemplate(manifestList, manifestsAndConfigs)); + + Path savedMetadataPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/manifests_configs.json"); + Assert.assertTrue(Files.exists(savedMetadataPath)); + + ImageMetadataTemplate savedMetadata = + JsonTemplateMapper.readJsonFromFile(savedMetadataPath, ImageMetadataTemplate.class); + + MatcherAssert.assertThat( + savedMetadata.getManifestList(), CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + List savedManifestDescriptors = + ((V22ManifestListTemplate) savedMetadata.getManifestList()).getManifests(); + + Assert.assertEquals(3, savedManifestDescriptors.size()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + savedManifestDescriptors.get(0).getDigest()); + Assert.assertEquals( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + savedManifestDescriptors.get(1).getDigest()); + Assert.assertEquals( + "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + savedManifestDescriptors.get(2).getDigest()); + + Assert.assertEquals(2, savedMetadata.getManifestsAndConfigs().size()); + ManifestAndConfigTemplate savedManifestAndConfig1 = + savedMetadata.getManifestsAndConfigs().get(0); + ManifestAndConfigTemplate savedManifestAndConfig2 = + savedMetadata.getManifestsAndConfigs().get(1); + + V22ManifestTemplate savedManifest1 = + (V22ManifestTemplate) savedManifestAndConfig1.getManifest(); + V22ManifestTemplate savedManifest2 = + (V22ManifestTemplate) savedManifestAndConfig2.getManifest(); + Assert.assertEquals(2, savedManifest1.getSchemaVersion()); + Assert.assertEquals(2, savedManifest2.getSchemaVersion()); + + Assert.assertEquals(1, savedManifest1.getLayers().size()); + Assert.assertEquals( + "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236", + savedManifest1.getLayers().get(0).getDigest().getHash()); + Assert.assertEquals( + Arrays.asList( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + savedManifest2.getLayers().stream() + .map(layer -> layer.getDigest().getHash()) + .collect(Collectors.toList())); + + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + savedManifest1.getContainerConfiguration().getDigest().getHash()); + Assert.assertEquals( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + savedManifest2.getContainerConfiguration().getDigest().getHash()); + + Assert.assertEquals("wasm", savedManifestAndConfig1.getConfig().getArchitecture()); + Assert.assertEquals("wasm", savedManifestAndConfig2.getConfig().getArchitecture()); + } + + @Test + public void testWriteMetadata_oci() + throws URISyntaxException, IOException, InvalidImageReferenceException { + ContainerConfigurationTemplate containerConfig = + loadJsonResource("core/json/containerconfig.json", ContainerConfigurationTemplate.class); + OciManifestTemplate manifest = + loadJsonResource("core/json/ocimanifest.json", OciManifestTemplate.class); + OciIndexTemplate ociIndex = loadJsonResource("core/json/ociindex.json", OciIndexTemplate.class); + + ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); + + cacheStorageWriter.writeMetadata( + imageReference, + new ImageMetadataTemplate( + ociIndex, + Arrays.asList( + new ManifestAndConfigTemplate(manifest, containerConfig, "sha256:digest")))); + + Path savedMetadataPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/manifests_configs.json"); + Assert.assertTrue(Files.exists(savedMetadataPath)); + + ImageMetadataTemplate savedMetadata = + JsonTemplateMapper.readJsonFromFile(savedMetadataPath, ImageMetadataTemplate.class); + + MatcherAssert.assertThat( + savedMetadata.getManifestList(), CoreMatchers.instanceOf(OciIndexTemplate.class)); + List savedManifestDescriptors = + ((OciIndexTemplate) savedMetadata.getManifestList()).getManifests(); + + Assert.assertEquals(1, savedManifestDescriptors.size()); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + savedManifestDescriptors.get(0).getDigest().getHash()); + + Assert.assertEquals(1, savedMetadata.getManifestsAndConfigs().size()); + ManifestAndConfigTemplate savedManifestAndConfig = + savedMetadata.getManifestsAndConfigs().get(0); + + OciManifestTemplate savedManifest1 = (OciManifestTemplate) savedManifestAndConfig.getManifest(); + Assert.assertEquals(2, savedManifest1.getSchemaVersion()); + + Assert.assertEquals(1, savedManifest1.getLayers().size()); + Assert.assertEquals( + "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236", + savedManifest1.getLayers().get(0).getDigest().getHash()); + + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + savedManifest1.getContainerConfiguration().getDigest().getHash()); + + Assert.assertEquals("wasm", savedManifestAndConfig.getConfig().getArchitecture()); + } + + @Test + public void testWriteLocalConfig() throws IOException, URISyntaxException, DigestException { + ContainerConfigurationTemplate containerConfigurationTemplate = + loadJsonResource("core/json/containerconfig.json", ContainerConfigurationTemplate.class); + + cacheStorageWriter.writeLocalConfig( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + containerConfigurationTemplate); + + Path savedConfigPath = + cacheStorageFiles + .getLocalDirectory() + .resolve("config") + .resolve("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + Assert.assertTrue(Files.exists(savedConfigPath)); + ContainerConfigurationTemplate savedContainerConfig = + JsonTemplateMapper.readJsonFromFile(savedConfigPath, ContainerConfigurationTemplate.class); + Assert.assertEquals("wasm", savedContainerConfig.getArchitecture()); + } + + @Test + public void testMoveIfDoesNotExist_exceptionAfterFailure() { + Exception exception = + assertThrows( + IOException.class, + () -> CacheStorageWriter.moveIfDoesNotExist(Paths.get("foo"), Paths.get("bar"))); + assertThat(exception) + .hasMessageThat() + .contains( + "unable to move: foo to bar; such failures are often caused by interference from " + + "antivirus"); + assertThat(exception).hasCauseThat().isInstanceOf(NoSuchFileException.class); + assertThat(exception.getCause()).hasMessageThat().isEqualTo("foo"); + } + + private void verifyCachedLayer( + CachedLayer cachedLayer, Blob uncompressedLayerBlob, Blob compressedLayerBlob) + throws IOException { + BlobDescriptor layerBlobDescriptor = getDigest(compressedLayerBlob); + DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob).getDigest(); + + // Verifies cachedLayer is correct. + Assert.assertEquals(layerBlobDescriptor.getDigest(), cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId, cachedLayer.getDiffId()); + Assert.assertEquals(layerBlobDescriptor.getSize(), cachedLayer.getSize()); + Assert.assertArrayEquals( + Blobs.writeToByteArray(uncompressedLayerBlob), + Blobs.writeToByteArray(decompress(cachedLayer.getBlob()))); + + // Verifies that the files are present. + Assert.assertTrue( + Files.exists( + cacheStorageFiles.getLayerFile(cachedLayer.getDigest(), cachedLayer.getDiffId()))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 975e8d386a..abf00f67a5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,28 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.net.URISyntaxException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -29,43 +45,190 @@ /** Tests for {@link Cache}. */ public class CacheTest { - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); + /** + * Gets a {@link Blob} that is {@code blob} compressed. Note that the output stream is closed when + * the blob is written. + * + * @param blob the {@link Blob} to compress + * @return the compressed {@link Blob} + */ + private static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }, + false); + } - @Test - public void testInit_empty() throws IOException, CacheMetadataCorruptedException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + /** + * Gets a {@link Blob} that is {@code blob} decompressed. + * + * @param blob the {@link Blob} to decompress + * @return the decompressed {@link Blob} + * @throws IOException if an I/O exception occurs + */ + private static Blob decompress(Blob blob) throws IOException { + return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); + } + + /** + * Gets the digest of {@code blob}. + * + * @param blob the {@link Blob} + * @return the {@link DescriptorDigest} of {@code blob} + * @throws IOException if an I/O exception occurs + */ + private static DescriptorDigest digestOf(Blob blob) throws IOException { + return blob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + } + + /** + * Gets the size of {@code blob}. + * + * @param blob the {@link Blob} + * @return the size (in bytes) of {@code blob} + * @throws IOException if an I/O exception occurs + */ + private static long sizeOf(Blob blob) throws IOException { + return blob.writeTo(ByteStreams.nullOutputStream()).getSize(); + } - Cache cache = Cache.init(cacheDirectory); - Assert.assertEquals(0, cache.getMetadata().getLayers().getLayers().size()); + private static FileEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new FileEntry( + source, + destination, + FileEntriesLayer.DEFAULT_FILE_PERMISSIONS_PROVIDER.get(source, destination), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Blob layerBlob1; + private DescriptorDigest layerDigest1; + private DescriptorDigest layerDiffId1; + private long layerSize1; + private ImmutableList layerEntries1; + + private Blob layerBlob2; + private DescriptorDigest layerDigest2; + private DescriptorDigest layerDiffId2; + private long layerSize2; + private ImmutableList layerEntries2; + + @Before + public void setUp() throws IOException { + Path directory = temporaryFolder.newFolder().toPath(); + Files.createDirectory(directory.resolve("source")); + Files.createFile(directory.resolve("source/file")); + Files.createDirectories(directory.resolve("another/source")); + Files.createFile(directory.resolve("another/source/file")); + + layerBlob1 = Blobs.from("layerBlob1"); + layerDigest1 = digestOf(compress(layerBlob1)); + layerDiffId1 = digestOf(layerBlob1); + layerSize1 = sizeOf(compress(layerBlob1)); + layerEntries1 = + ImmutableList.of( + defaultLayerEntry( + directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")), + defaultLayerEntry( + directory.resolve("another/source/file"), + AbsoluteUnixPath.get("/another/extraction/path"))); + + layerBlob2 = Blobs.from("layerBlob2"); + layerDigest2 = digestOf(compress(layerBlob2)); + layerDiffId2 = digestOf(layerBlob2); + layerSize2 = sizeOf(compress(layerBlob2)); + layerEntries2 = ImmutableList.of(); } @Test - public void testInit_notDirectory() throws CacheMetadataCorruptedException, IOException { - Path tempFile = temporaryCacheDirectory.newFile().toPath(); + public void testWithDirectory_existsButNotDirectory() throws IOException { + Path file = temporaryFolder.newFile().toPath(); try { - Cache.init(tempFile); - Assert.fail("Cache should not be able to initialize on non-directory"); + Cache.withDirectory(file); + Assert.fail(); - } catch (NotDirectoryException ex) { - Assert.assertEquals("The cache can only write to a directory", ex.getMessage()); + } catch (CacheDirectoryCreationException ex) { + MatcherAssert.assertThat( + ex.getCause(), CoreMatchers.instanceOf(FileAlreadyExistsException.class)); } } @Test - public void testInit_withMetadata() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + public void testWriteCompressed_retrieveByLayerDigest() + throws IOException, CacheDirectoryCreationException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1(cache.writeCompressedLayer(compress(layerBlob1))); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + } + + @Test + public void testWriteUncompressedWithLayerEntries_retrieveByLayerDigest() + throws IOException, CacheDirectoryCreationException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + } + + @Test + public void testWriteUncompressedWithLayerEntries_retrieveByLayerEntries() + throws IOException, CacheDirectoryCreationException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - Path resourceMetadataJsonPath = PlatformSpecificMetadataJson.getMetadataJsonFile(); - Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); - Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer1(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); - try (Cache cache = Cache.init(cacheDirectory)) { - Assert.assertEquals(2, cache.getMetadata().getLayers().getLayers().size()); - } + // A source file modification results in the cached layer to be out-of-date and not retrieved. + Files.setLastModifiedTime( + layerEntries1.get(0).getSourceFile(), FileTime.from(Instant.now().plusSeconds(1))); + Assert.assertFalse(cache.retrieve(layerEntries1).isPresent()); + } + + @Test + public void testRetrieveWithTwoEntriesInCache() + throws IOException, CacheDirectoryCreationException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer2(cache.writeUncompressedLayer(layerBlob2, layerEntries2)); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + verifyIsLayer2(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + verifyIsLayer2(cache.retrieve(layerEntries2).orElseThrow(AssertionError::new)); + } + + /** + * Verifies that {@code cachedLayer} corresponds to the first fake layer in {@link #setUp}. + * + * @param cachedLayer the {@link CachedLayer} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer1(CachedLayer cachedLayer) throws IOException { + Assert.assertEquals("layerBlob1", Blobs.writeToString(decompress(cachedLayer.getBlob()))); + Assert.assertEquals(layerDigest1, cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId1, cachedLayer.getDiffId()); + Assert.assertEquals(layerSize1, cachedLayer.getSize()); + } - Assert.assertArrayEquals( - Files.readAllBytes(resourceMetadataJsonPath), Files.readAllBytes(testMetadataJsonPath)); + /** + * Verifies that {@code cachedLayer} corresponds to the second fake layer in {@link #setUp}. + * + * @param cachedLayer the {@link CachedLayer} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer2(CachedLayer cachedLayer) throws IOException { + Assert.assertEquals("layerBlob2", Blobs.writeToString(decompress(cachedLayer.getBlob()))); + Assert.assertEquals(layerDigest2, cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId2, cachedLayer.getDiffId()); + Assert.assertEquals(layerSize2, cachedLayer.getSize()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java deleted file mode 100644 index 2de09e2fd1..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; -import com.google.cloud.tools.jib.image.UnwrittenLayer; -import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; -import com.google.common.io.CountingOutputStream; -import com.google.common.io.Resources; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -/** Tests for {@link CacheWriter}. */ -public class CacheWriterTest { - - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); - - private Cache testCache; - - private Path resourceBlob; - - private static class ExpectedLayer { - - private final BlobDescriptor blobDescriptor; - private final DescriptorDigest diffId; - private final Blob blob; - - private ExpectedLayer(BlobDescriptor blobDescriptor, DescriptorDigest diffId, Blob blob) { - this.blobDescriptor = blobDescriptor; - this.diffId = diffId; - this.blob = blob; - } - } - - @Before - public void setUp() throws CacheMetadataCorruptedException, IOException, URISyntaxException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - - testCache = Cache.init(cacheDirectory); - - resourceBlob = Paths.get(Resources.getResource("blobA").toURI()); - } - - @Test - public void testWriteLayer_unwritten() throws IOException { - ExpectedLayer expectedLayer = getExpectedLayer(); - - // Writes resourceBlob as a layer to the cache. - CacheWriter cacheWriter = new CacheWriter(testCache); - - UnwrittenLayer unwrittenLayer = new UnwrittenLayer(Blobs.from(resourceBlob)); - - ReproducibleLayerBuilder mockReproducibleLayerBuilder = - Mockito.mock(ReproducibleLayerBuilder.class); - Mockito.when(mockReproducibleLayerBuilder.build()).thenReturn(unwrittenLayer); - Mockito.when(mockReproducibleLayerBuilder.getLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry( - ImmutableList.of(Paths.get("some", "source", "file")), - "/some/extraction/path"))); - - CachedLayerWithMetadata cachedLayerWithMetadata = - cacheWriter.writeLayer(mockReproducibleLayerBuilder); - testCache.addCachedLayersWithMetadataToMetadata( - Collections.singletonList(cachedLayerWithMetadata)); - - CachedLayerWithMetadata layerInMetadata = testCache.getUpdatedMetadata().getLayers().get(0); - Assert.assertNotNull(layerInMetadata.getMetadata()); - Assert.assertEquals(1, layerInMetadata.getMetadata().getEntries().size()); - Assert.assertEquals( - Collections.singletonList(Paths.get("some", "source", "file").toString()), - layerInMetadata.getMetadata().getEntries().get(0).getSourceFilesStrings()); - Assert.assertEquals( - "/some/extraction/path", - layerInMetadata.getMetadata().getEntries().get(0).getExtractionPath()); - - verifyCachedLayerIsExpected(expectedLayer, cachedLayerWithMetadata); - } - - @Test - public void testGetLayerOutputStream() throws IOException { - ExpectedLayer expectedLayer = getExpectedLayer(); - - // Writes resourceBlob as a layer to the cache. - CacheWriter cacheWriter = new CacheWriter(testCache); - - CountingOutputStream layerOutputStream = - cacheWriter.getLayerOutputStream(expectedLayer.blobDescriptor.getDigest()); - expectedLayer.blob.writeTo(layerOutputStream); - layerOutputStream.close(); - CachedLayer cachedLayer = - cacheWriter.getCachedLayer( - layerOutputStream.getCount(), expectedLayer.blobDescriptor.getDigest()); - testCache.addCachedLayersToMetadata(Collections.singletonList(cachedLayer)); - - CachedLayerWithMetadata layerInMetadata = testCache.getUpdatedMetadata().getLayers().get(0); - Assert.assertNull(layerInMetadata.getMetadata()); - - verifyCachedLayerIsExpected(expectedLayer, cachedLayer); - } - - /** - * @return the expected layer to test against, represented by the {@code resourceBlob} resource - * file - */ - private ExpectedLayer getExpectedLayer() throws IOException { - String expectedBlobAString = - new String(Files.readAllBytes(resourceBlob), StandardCharsets.UTF_8); - - // Gets the expected content descriptor, diff ID, and compressed BLOB. - ByteArrayOutputStream compressedBlobOutputStream = new ByteArrayOutputStream(); - CountingDigestOutputStream compressedDigestOutputStream = - new CountingDigestOutputStream(compressedBlobOutputStream); - CountingDigestOutputStream uncompressedDigestOutputStream; - try (GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream)) { - uncompressedDigestOutputStream = new CountingDigestOutputStream(compressorStream); - uncompressedDigestOutputStream.write(expectedBlobAString.getBytes(StandardCharsets.UTF_8)); - } - - BlobDescriptor expectedBlobADescriptor = compressedDigestOutputStream.toBlobDescriptor(); - DescriptorDigest expectedBlobADiffId = - uncompressedDigestOutputStream.toBlobDescriptor().getDigest(); - - ByteArrayInputStream compressedBlobInputStream = - new ByteArrayInputStream(compressedBlobOutputStream.toByteArray()); - Blob blob = Blobs.from(compressedBlobInputStream); - - return new ExpectedLayer(expectedBlobADescriptor, expectedBlobADiffId, blob); - } - - private void verifyCachedLayerIsExpected(ExpectedLayer expectedLayer, CachedLayer cachedLayer) - throws IOException { - // Reads the cached layer back. - Path compressedBlobFile = cachedLayer.getContentFile(); - - try (InputStreamReader fileReader = - new InputStreamReader( - new GZIPInputStream(Files.newInputStream(compressedBlobFile)), - StandardCharsets.UTF_8)) { - String decompressedString = CharStreams.toString(fileReader); - - String expectedBlobAString = - new String(Files.readAllBytes(resourceBlob), StandardCharsets.UTF_8); - Assert.assertEquals(expectedBlobAString, decompressedString); - Assert.assertEquals( - expectedLayer.blobDescriptor.getSize(), cachedLayer.getBlobDescriptor().getSize()); - Assert.assertEquals( - expectedLayer.blobDescriptor.getDigest(), cachedLayer.getBlobDescriptor().getDigest()); - Assert.assertEquals(expectedLayer.blobDescriptor, cachedLayer.getBlobDescriptor()); - Assert.assertEquals(expectedLayer.diffId, cachedLayer.getDiffId()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java index dd3fc1429d..16262f1fed 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,17 +16,11 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,33 +31,50 @@ @RunWith(MockitoJUnitRunner.class) public class CachedLayerTest { - @Mock private Path mockPath; - @Mock private BlobDescriptor mockBlobDescriptor; - @Mock private DescriptorDigest mockDiffId; + @Mock private DescriptorDigest mockLayerDigest; + @Mock private DescriptorDigest mockLayerDiffId; @Test - public void testNew() { - CachedLayer layer = new CachedLayer(mockPath, mockBlobDescriptor, mockDiffId); + public void testBuilder_fail() { + try { + CachedLayer.builder().build(); + Assert.fail("missing required"); - Assert.assertEquals(mockPath, layer.getContentFile()); - Assert.assertEquals(mockBlobDescriptor, layer.getBlobDescriptor()); - Assert.assertEquals(mockDiffId, layer.getDiffId()); - } + } catch (NullPointerException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDigest")); + } - @Test - public void testGetBlob() throws URISyntaxException, IOException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); - String expectedFileAString = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); + try { + CachedLayer.builder().setLayerDigest(mockLayerDigest).build(); + Assert.fail("missing required"); - CachedLayer cachedLayer = new CachedLayer(fileA, mockBlobDescriptor, mockDiffId); + } catch (NullPointerException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDiffId")); + } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Blob fileBlob = cachedLayer.getBlob(); - fileBlob.writeTo(outputStream); + try { + CachedLayer.builder().setLayerDigest(mockLayerDigest).setLayerDiffId(mockLayerDiffId).build(); + Assert.fail("missing required"); - Assert.assertEquals( - expectedFileAString, new String(outputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals(mockBlobDescriptor, cachedLayer.getBlobDescriptor()); - Assert.assertEquals(mockDiffId, cachedLayer.getDiffId()); + } catch (NullPointerException ex) { + MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerBlob")); + } + } + + @Test + public void testBuilder_pass() throws IOException { + CachedLayer.Builder cachedLayerBuilder = + CachedLayer.builder() + .setLayerDigest(mockLayerDigest) + .setLayerDiffId(mockLayerDiffId) + .setLayerSize(1337); + Assert.assertFalse(cachedLayerBuilder.hasLayerBlob()); + cachedLayerBuilder.setLayerBlob(Blobs.from("layerBlob")); + Assert.assertTrue(cachedLayerBuilder.hasLayerBlob()); + CachedLayer cachedLayer = cachedLayerBuilder.build(); + Assert.assertEquals(mockLayerDigest, cachedLayer.getDigest()); + Assert.assertEquals(mockLayerDiffId, cachedLayer.getDiffId()); + Assert.assertEquals(1337, cachedLayer.getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cachedLayer.getBlob())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java deleted file mode 100644 index d79ec3fcf8..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link Caches}. */ -public class CachesTest { - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void testInitializer() - throws CacheMetadataCorruptedException, IOException, CacheDirectoryNotOwnedException, - CacheDirectoryCreationException { - Path tempBaseCacheDirectory = temporaryFolder.newFolder().toPath(); - Path tempApplicationCacheDirectory = temporaryFolder.newFolder().toPath(); - - try (Caches caches = - new Caches.Initializer(tempBaseCacheDirectory, false, tempApplicationCacheDirectory, false) - .init()) { - Assert.assertEquals(tempBaseCacheDirectory, caches.getBaseCache().getCacheDirectory()); - Assert.assertEquals( - tempApplicationCacheDirectory, caches.getApplicationCache().getCacheDirectory()); - } - - // Checks that the caches were closed (metadata.json saved). - Assert.assertTrue(Files.exists(tempBaseCacheDirectory.resolve(CacheFiles.METADATA_FILENAME))); - Assert.assertTrue( - Files.exists(tempApplicationCacheDirectory.resolve(CacheFiles.METADATA_FILENAME))); - } - - @Test - public void testEnsureOwnership_notOwned() throws IOException, CacheDirectoryCreationException { - Path cacheDirectory = temporaryFolder.newFolder().toPath(); - - try { - Caches.Initializer.ensureOwnership(cacheDirectory); - Assert.fail("Expected CacheDirectoryNotOwnedException to be thrown"); - - } catch (CacheDirectoryNotOwnedException ex) { - Assert.assertEquals(cacheDirectory, ex.getCacheDirectory()); - } - } - - @Test - public void testEnsureOwnership_create() - throws IOException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Path cacheDirectory = temporaryFolder.newFolder().toPath(); - Path nonexistentDirectory = cacheDirectory.resolve("somefolder"); - - Caches.Initializer.ensureOwnership(nonexistentDirectory); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java new file mode 100644 index 0000000000..dbab5b386a --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link LayerEntriesSelector}. */ +public class LayerEntriesSelectorTest { + + private static FileEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new FileEntry( + source, + destination, + FileEntriesLayer.DEFAULT_FILE_PERMISSIONS_PROVIDER.get(source, destination), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private ImmutableList outOfOrderLayerEntries; + private ImmutableList inOrderLayerEntries; + + private static ImmutableList toLayerEntryTemplates( + ImmutableList layerEntries) throws IOException { + ImmutableList.Builder builder = ImmutableList.builder(); + for (FileEntry layerEntry : layerEntries) { + builder.add(new LayerEntryTemplate(layerEntry)); + } + return builder.build(); + } + + @Before + public void setUp() throws IOException { + Path folder = temporaryFolder.newFolder().toPath(); + Path file1 = Files.createDirectory(folder.resolve("files")); + Path file2 = Files.createFile(folder.resolve("files").resolve("two")); + Path file3 = Files.createFile(folder.resolve("gile")); + + FileEntry testLayerEntry1 = defaultLayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); + FileEntry testLayerEntry2 = defaultLayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); + FileEntry testLayerEntry3 = defaultLayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); + FileEntry testLayerEntry4 = + new FileEntry( + file3, + AbsoluteUnixPath.get("/extraction/path"), + FilePermissions.fromOctalString("755"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + FileEntry testLayerEntry5 = defaultLayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); + FileEntry testLayerEntry6 = + new FileEntry( + file3, + AbsoluteUnixPath.get("/extraction/patha"), + FilePermissions.fromOctalString("755"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + + outOfOrderLayerEntries = + ImmutableList.of( + testLayerEntry4, + testLayerEntry2, + testLayerEntry6, + testLayerEntry3, + testLayerEntry1, + testLayerEntry5); + inOrderLayerEntries = + ImmutableList.of( + testLayerEntry1, + testLayerEntry2, + testLayerEntry3, + testLayerEntry4, + testLayerEntry5, + testLayerEntry6); + } + + @Test + public void testLayerEntryTemplate_compareTo() throws IOException { + Assert.assertEquals( + toLayerEntryTemplates(inOrderLayerEntries), + ImmutableList.sortedCopyOf(toLayerEntryTemplates(outOfOrderLayerEntries))); + } + + @Test + public void testToSortedJsonTemplates() throws IOException { + Assert.assertEquals( + toLayerEntryTemplates(inOrderLayerEntries), + LayerEntriesSelector.toSortedJsonTemplates(outOfOrderLayerEntries)); + } + + @Test + public void testGenerateSelector_empty() throws IOException { + DescriptorDigest expectedSelector = Digests.computeJsonDigest(ImmutableList.of()); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of())); + } + + @Test + public void testGenerateSelector() throws IOException { + DescriptorDigest expectedSelector = + Digests.computeJsonDigest(toLayerEntryTemplates(inOrderLayerEntries)); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(outOfOrderLayerEntries)); + } + + @Test + public void testGenerateSelector_sourceModificationTimeChanged() throws IOException { + Path layerFile = temporaryFolder.newFile().toPath(); + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); + FileEntry layerEntry = defaultLayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); + DescriptorDigest expectedSelector = + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry)); + + // Verify that changing source modification time generates a different selector + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.ofEpochSecond(1))); + Assert.assertNotEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry))); + + // Verify that changing source modification time back generates same selector + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry))); + } + + @Test + public void testGenerateSelector_targetModificationTimeChanged() throws IOException { + Path layerFile = temporaryFolder.newFile().toPath(); + AbsoluteUnixPath pathInContainer = AbsoluteUnixPath.get("/bar"); + FilePermissions permissions = FilePermissions.fromOctalString("111"); + + FileEntry layerEntry1 = new FileEntry(layerFile, pathInContainer, permissions, Instant.now()); + FileEntry layerEntry2 = new FileEntry(layerFile, pathInContainer, permissions, Instant.EPOCH); + + // Verify that different target modification times generate different selectors + Assert.assertNotEquals( + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry1)), + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry2))); + } + + @Test + public void testGenerateSelector_permissionsModified() throws IOException { + Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); + Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); + FileEntry layerEntry111 = + new FileEntry( + layerFile, + AbsoluteUnixPath.get("/extraction/path"), + FilePermissions.fromOctalString("111"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + FileEntry layerEntry222 = + new FileEntry( + layerFile, + AbsoluteUnixPath.get("/extraction/path"), + FilePermissions.fromOctalString("222"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); + + // Verify that changing permissions generates a different selector + Assert.assertNotEquals( + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry111)), + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry222))); + } + + @Test + public void testGenerateSelector_ownersModified() throws IOException { + Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); + Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); + FileEntry layerEntry111 = + new FileEntry( + layerFile, + AbsoluteUnixPath.get("/extraction/path"), + FilePermissions.fromOctalString("111"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME, + "0:0"); + FileEntry layerEntry222 = + new FileEntry( + layerFile, + AbsoluteUnixPath.get("/extraction/path"), + FilePermissions.fromOctalString("222"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME, + "foouser"); + + // Verify that changing ownership generates a different selector + Assert.assertNotEquals( + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry111)), + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry222))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java deleted file mode 100644 index d90c6a7da1..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.common.io.Resources; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** Test utility for using platform-specific test resources. */ -public class PlatformSpecificMetadataJson { - - /** - * Retrieves a sample metadata JSON file. A separate version for Windows is used to account for - * the different path separator. - */ - public static Path getMetadataJsonFile() throws URISyntaxException { - String metadataResourceFilename = "json/metadata-v2.json"; - if (System.getProperty("os.name").toLowerCase().contains("win")) { - metadataResourceFilename = "json/metadata_windows-v2.json"; - } - // Loads the expected JSON string. - return Paths.get(Resources.getResource(metadataResourceFilename).toURI()); - } - - private PlatformSpecificMetadataJson() {} -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java new file mode 100644 index 0000000000..6ea83e21f9 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.cache; + +import java.util.concurrent.TimeUnit; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Retry}. */ +public class RetryTest { + private int actionCount = 0; + + private boolean successfulAction() { + ++actionCount; + return true; + } + + private boolean unsuccessfulAction() { + ++actionCount; + return false; + } + + private boolean exceptionAction() throws Exception { + ++actionCount; + throw new Exception("whee"); + } + + @Test + public void testSuccessfulAction() throws Exception { + boolean result = Retry.action(this::successfulAction).run(); + Assert.assertTrue(result); + Assert.assertEquals(1, actionCount); + } + + @Test + public void testMaximumRetries_default() throws Exception { + boolean result = Retry.action(this::unsuccessfulAction).run(); + Assert.assertFalse(result); + Assert.assertEquals(5, actionCount); + } + + @Test + public void testMaximumRetries_specified() throws Exception { + boolean result = Retry.action(this::unsuccessfulAction).maximumRetries(2).run(); + Assert.assertFalse(result); + Assert.assertEquals(2, actionCount); + } + + @Test + public void testRetryableException() { + // all exceptions are retryable by default, so should retry 5 times + try { + Retry.action(this::exceptionAction).run(); + Assert.fail("should have thrown exception"); + } catch (Exception ex) { + Assert.assertEquals("whee", ex.getMessage()); + Assert.assertEquals(5, actionCount); + } + } + + @Test + public void testNonRetryableException() { + // the exception is not ok and so should only try 1 time + try { + Retry.action(this::exceptionAction).retryOnException(ex -> false).run(); + Assert.fail("should have thrown exception"); + } catch (Exception ex) { + Assert.assertEquals("whee", ex.getMessage()); + Assert.assertEquals(1, actionCount); + } + } + + @Test + public void testInterruptSleep() throws Exception { + // interrupt the current thread so as to cause the retry's sleep() to throw + // an InterruptedException + Thread.currentThread().interrupt(); + try { + boolean result = Retry.action(this::unsuccessfulAction).sleep(10, TimeUnit.SECONDS).run(); + Assert.assertFalse(result); + Assert.assertEquals(1, actionCount); + } finally { + // This thread should be marked as interrupted (plus clear the flag for the test) + Assert.assertTrue(Thread.interrupted()); + } + } + + @Test + public void testInvalid_maximumRetries() { + try { + Retry.action(this::successfulAction).maximumRetries(0); + Assert.fail(); + } catch (IllegalArgumentException ex) { + /* maximumRetries() ensures the retry value is at least 1. */ + } + } + + @Test + public void testInvalid_sleep() { + try { + Retry.action(this::successfulAction).sleep(-1, TimeUnit.DAYS); + Assert.fail(); + } catch (IllegalArgumentException ex) { + /* sleep() ensures the sleep value is non-negative. */ + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java deleted file mode 100644 index 37574db6da..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.cache.PlatformSpecificMetadataJson; -import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.Collections; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link CacheMetadataTemplate}. */ -public class CacheMetadataTemplateTest { - - @Test - public void testToJson() throws URISyntaxException, IOException, DigestException { - // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); - String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - - CacheMetadataTemplate cacheMetadataTemplate = new CacheMetadataTemplate(); - - // Adds a base layer. - CacheMetadataLayerObjectTemplate baseLayerTemplate = - new CacheMetadataLayerObjectTemplate() - .setSize(631) - .setDigest( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef")) - .setDiffId( - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647")); - - // Adds an application layer. - CacheMetadataLayerPropertiesObjectTemplate propertiesTemplate = - new CacheMetadataLayerPropertiesObjectTemplate() - .setLayerEntries( - Collections.singletonList( - new LayerEntryTemplate( - Collections.singletonList(Paths.get("some", "source", "path").toString()), - "some/extraction/path"))) - .setLastModifiedTime(FileTime.fromMillis(255073580723571L)); - CacheMetadataLayerObjectTemplate classesLayerTemplate = - new CacheMetadataLayerObjectTemplate() - .setSize(223) - .setDigest( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")) - .setDiffId( - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372")) - .setProperties(propertiesTemplate); - - cacheMetadataTemplate.addLayer(baseLayerTemplate).addLayer(classesLayerTemplate); - - // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(cacheMetadataTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); - } - - @Test - public void testFromJson() throws URISyntaxException, IOException, DigestException { - // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); - - // Deserializes into a metadata JSON object. - CacheMetadataTemplate metadataTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, CacheMetadataTemplate.class); - - List layers = metadataTemplate.getLayers(); - - Assert.assertEquals(2, layers.size()); - - // Checks the first layer is correct. - CacheMetadataLayerObjectTemplate baseLayerTemplate = layers.get(0); - Assert.assertEquals(631, baseLayerTemplate.getSize()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"), - baseLayerTemplate.getDigest()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"), - baseLayerTemplate.getDiffId()); - Assert.assertNull(baseLayerTemplate.getProperties()); - - // Checks the second layer is correct. - CacheMetadataLayerObjectTemplate classesLayerTemplate = layers.get(1); - Assert.assertEquals(223, classesLayerTemplate.getSize()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), - classesLayerTemplate.getDigest()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"), - classesLayerTemplate.getDiffId()); - Assert.assertNotNull(classesLayerTemplate.getProperties()); - Assert.assertEquals(1, classesLayerTemplate.getProperties().getLayerEntries().size()); - Assert.assertEquals( - Collections.singletonList(Paths.get("some", "source", "path").toString()), - classesLayerTemplate.getProperties().getLayerEntries().get(0).getSourceFiles()); - Assert.assertEquals( - "some/extraction/path", - classesLayerTemplate.getProperties().getLayerEntries().get(0).getExtractionPath()); - Assert.assertEquals( - FileTime.fromMillis(255073580723571L), - classesLayerTemplate.getProperties().getLastModifiedTime()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java new file mode 100644 index 0000000000..4c052da192 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java @@ -0,0 +1,350 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.mockito.Mockito; + +/** Tests for {@link BuildContext}. */ +public class BuildContextTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + + private static BuildContext.Builder createBasicTestBuilder() { + return BuildContext.builder() + .setBaseImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setTargetImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setContainerConfiguration(ContainerConfiguration.builder().build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")); + } + + @Test + public void testBuilder() throws Exception { + String expectedBaseImageServerUrl = "someserver"; + String expectedBaseImageName = "baseimage"; + String expectedBaseImageTag = "baseimagetag"; + String expectedTargetServerUrl = "someotherserver"; + String expectedTargetImageName = "targetimage"; + String expectedTargetTag = "targettag"; + Set additionalTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); + Set expectedTargetImageTags = ImmutableSet.of("targettag", "tag1", "tag2", "tag3"); + List credentialRetrievers = + Collections.singletonList(() -> Optional.of(Credential.from("username", "password"))); + Instant expectedCreationTime = Instant.ofEpochSecond(10000); + List expectedEntrypoint = Arrays.asList("some", "entrypoint"); + List expectedProgramArguments = Arrays.asList("arg1", "arg2"); + Map expectedEnvironment = ImmutableMap.of("key", "value"); + Set expectedExposedPorts = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)); + Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); + Class expectedTargetFormat = OciManifestTemplate.class; + Path expectedApplicationLayersCacheDirectory = Paths.get("application/layers"); + Path expectedBaseImageLayersCacheDirectory = Paths.get("base/image/layers"); + List expectedLayerConfigurations = + Collections.singletonList( + FileEntriesLayer.builder() + .addEntry(Paths.get("sourceFile"), AbsoluteUnixPath.get("/path/in/container")) + .build()); + String expectedCreatedBy = "createdBy"; + ListMultimap expectedRegistryMirrors = + ImmutableListMultimap.of("some.registry", "mirror1", "some.registry", "mirror2"); + + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder( + ImageReference.of( + expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) + .build(); + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder( + ImageReference.of( + expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) + .setCredentialRetrievers(credentialRetrievers) + .build(); + ContainerConfiguration containerConfiguration = + ContainerConfiguration.builder() + .setCreationTime(expectedCreationTime) + .setEntrypoint(expectedEntrypoint) + .setProgramArguments(expectedProgramArguments) + .setEnvironment(expectedEnvironment) + .setExposedPorts(expectedExposedPorts) + .setLabels(expectedLabels) + .build(); + BuildContext.Builder buildContextBuilder = + BuildContext.builder() + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setAdditionalTargetImageTags(additionalTargetImageTags) + .setContainerConfiguration(containerConfiguration) + .setApplicationLayersCacheDirectory(expectedApplicationLayersCacheDirectory) + .setBaseImageLayersCacheDirectory(expectedBaseImageLayersCacheDirectory) + .setTargetFormat(ImageFormat.OCI) + .setEnablePlatformTags(true) + .setAllowInsecureRegistries(true) + .setLayerConfigurations(expectedLayerConfigurations) + .setToolName(expectedCreatedBy) + .setRegistryMirrors(expectedRegistryMirrors); + BuildContext buildContext = buildContextBuilder.build(); + + Assert.assertEquals( + expectedCreationTime, buildContext.getContainerConfiguration().getCreationTime()); + Assert.assertEquals( + expectedBaseImageServerUrl, buildContext.getBaseImageConfiguration().getImageRegistry()); + Assert.assertEquals( + expectedBaseImageName, buildContext.getBaseImageConfiguration().getImageRepository()); + Assert.assertEquals( + expectedBaseImageTag, buildContext.getBaseImageConfiguration().getImageQualifier()); + Assert.assertEquals( + expectedTargetServerUrl, buildContext.getTargetImageConfiguration().getImageRegistry()); + Assert.assertEquals( + expectedTargetImageName, buildContext.getTargetImageConfiguration().getImageRepository()); + Assert.assertEquals( + expectedTargetTag, buildContext.getTargetImageConfiguration().getImageQualifier()); + Assert.assertEquals(expectedTargetImageTags, buildContext.getAllTargetImageTags()); + Assert.assertEquals( + Credential.from("username", "password"), + buildContext + .getTargetImageConfiguration() + .getCredentialRetrievers() + .get(0) + .retrieve() + .orElseThrow(AssertionError::new)); + Assert.assertEquals( + expectedProgramArguments, buildContext.getContainerConfiguration().getProgramArguments()); + Assert.assertEquals( + expectedEnvironment, buildContext.getContainerConfiguration().getEnvironmentMap()); + Assert.assertEquals( + expectedExposedPorts, buildContext.getContainerConfiguration().getExposedPorts()); + Assert.assertEquals(expectedLabels, buildContext.getContainerConfiguration().getLabels()); + Assert.assertEquals(expectedTargetFormat, buildContext.getTargetFormat()); + Assert.assertEquals( + expectedApplicationLayersCacheDirectory, + buildContextBuilder.getApplicationLayersCacheDirectory()); + Assert.assertEquals( + expectedBaseImageLayersCacheDirectory, + buildContextBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertEquals(expectedLayerConfigurations, buildContext.getLayerConfigurations()); + Assert.assertEquals( + expectedEntrypoint, buildContext.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals(expectedCreatedBy, buildContext.getToolName()); + Assert.assertEquals(expectedRegistryMirrors, buildContext.getRegistryMirrors()); + Assert.assertNotNull(buildContext.getExecutorService()); + Assert.assertTrue(buildContext.getEnablePlatformTags()); + } + + @Test + public void testBuilder_default() throws CacheDirectoryCreationException { + // These are required and don't have defaults. + String expectedBaseImageServerUrl = "someserver"; + String expectedBaseImageName = "baseimage"; + String expectedBaseImageTag = "baseimagetag"; + String expectedTargetServerUrl = "someotherserver"; + String expectedTargetImageName = "targetimage"; + String expectedTargetTag = "targettag"; + + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder( + ImageReference.of( + expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) + .build(); + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder( + ImageReference.of( + expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) + .build(); + BuildContext.Builder buildContextBuilder = + BuildContext.builder() + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(ContainerConfiguration.builder().setUser("12345").build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")); + BuildContext buildContext = buildContextBuilder.build(); + + Assert.assertEquals(ImmutableSet.of("targettag"), buildContext.getAllTargetImageTags()); + Assert.assertEquals(V22ManifestTemplate.class, buildContext.getTargetFormat()); + Assert.assertNotNull(buildContextBuilder.getApplicationLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("ignored"), buildContextBuilder.getApplicationLayersCacheDirectory()); + Assert.assertNotNull(buildContextBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("ignored"), buildContextBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertEquals("12345", buildContext.getContainerConfiguration().getUser()); + Assert.assertEquals(Collections.emptyList(), buildContext.getLayerConfigurations()); + Assert.assertEquals("jib", buildContext.getToolName()); + Assert.assertEquals(0, buildContext.getRegistryMirrors().size()); + Assert.assertFalse(buildContext.getEnablePlatformTags()); + } + + @Test + public void testBuilder_missingValues() throws CacheDirectoryCreationException { + // Target image is missing + try { + BuildContext.builder() + .setBaseImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setContainerConfiguration(ContainerConfiguration.builder().build()) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .build(); + Assert.fail("BuildContext should not be built with missing values"); + + } catch (IllegalStateException ex) { + Assert.assertEquals("target image configuration is required but not set", ex.getMessage()); + } + + // Two required fields missing + try { + BuildContext.builder() + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setContainerConfiguration(ContainerConfiguration.builder().build()) + .build(); + Assert.fail("BuildContext should not be built with missing values"); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "base image configuration and target image configuration are required but not set", + ex.getMessage()); + } + + // All required fields missing + try { + BuildContext.builder().build(); + Assert.fail("BuildContext should not be built with missing values"); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "base image configuration, target image configuration, container configuration, base " + + "image layers cache directory, and application layers cache directory are required " + + "but not set", + ex.getMessage()); + } + } + + @Test + public void testBuilder_digestWarning() + throws CacheDirectoryCreationException, InvalidImageReferenceException { + EventHandlers mockEventHandlers = Mockito.mock(EventHandlers.class); + BuildContext.Builder builder = createBasicTestBuilder().setEventHandlers(mockEventHandlers); + + builder + .setBaseImageConfiguration( + ImageConfiguration.builder( + ImageReference.parse( + "image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .build()) + .build(); + Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.warn(Mockito.anyString())); + + builder + .setBaseImageConfiguration( + ImageConfiguration.builder(ImageReference.parse("image:tag")).build()) + .build(); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.warn( + "Base image 'image:tag' does not use a specific image digest - build may not be reproducible")); + } + + @Test + public void testClose_shutDownInternalExecutorService() + throws IOException, CacheDirectoryCreationException { + + BuildContext buildContext = createBasicTestBuilder().build(); + buildContext.close(); + + Assert.assertTrue(buildContext.getExecutorService().isShutdown()); + } + + @Test + public void testClose_doNotShutDownProvidedExecutorService() + throws IOException, CacheDirectoryCreationException { + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); + BuildContext buildContext = + createBasicTestBuilder().setExecutorService(executorService).build(); + buildContext.close(); + + Assert.assertSame(executorService, buildContext.getExecutorService()); + Assert.assertFalse(buildContext.getExecutorService().isShutdown()); + } + + @Test + public void testGetUserAgent_unset() throws CacheDirectoryCreationException { + BuildContext buildContext = createBasicTestBuilder().build(); + + String generatedUserAgent = buildContext.makeUserAgent(); + + Assert.assertEquals("jib null jib", generatedUserAgent); + } + + @Test + public void testGetUserAgent_withValues() throws CacheDirectoryCreationException { + BuildContext buildContext = + createBasicTestBuilder().setToolName("test-name").setToolVersion("test-version").build(); + + String generatedUserAgent = buildContext.makeUserAgent(); + + Assert.assertEquals("jib test-version test-name", generatedUserAgent); + } + + @Test + public void testGetUserAgentWithUpstreamClient() throws CacheDirectoryCreationException { + System.setProperty(JibSystemProperties.UPSTREAM_CLIENT, "skaffold/0.34.0"); + BuildContext buildContext = + createBasicTestBuilder().setToolName("test-name").setToolVersion("test-version").build(); + + String generatedUserAgent = buildContext.makeUserAgent(); + + Assert.assertEquals("jib test-version test-name skaffold/0.34.0", generatedUserAgent); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java deleted file mode 100644 index 614bb5326d..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import java.nio.file.Files; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link CacheConfiguration}. */ -public class CacheConfigurationTest { - - @Test - public void testAtPath() { - CacheConfiguration cacheConfiguration = CacheConfiguration.forPath(Paths.get("/path/to/cache")); - Assert.assertEquals(Paths.get("/path/to/cache"), cacheConfiguration.getCacheDirectory()); - Assert.assertTrue(cacheConfiguration.shouldEnsureOwnership()); - } - - @Test - public void testMakeTemporary() throws CacheDirectoryCreationException { - CacheConfiguration cacheConfiguration = CacheConfiguration.makeTemporary(); - Assert.assertTrue(Files.exists(cacheConfiguration.getCacheDirectory())); - Assert.assertFalse(cacheConfiguration.shouldEnsureOwnership()); - } - - @Test - public void testAtDefaultUserLevelCacheDirectory() { - CacheConfiguration cacheConfiguration = CacheConfiguration.forDefaultUserLevelCacheDirectory(); - Assert.assertEquals( - CacheConfiguration.DEFAULT_BASE_CACHE_DIRECTORY, cacheConfiguration.getCacheDirectory()); - Assert.assertTrue(cacheConfiguration.shouldEnsureOwnership()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java new file mode 100644 index 0000000000..aeeb6ba49d --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Platform; +import com.google.cloud.tools.jib.api.buildplan.Port; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ContainerConfiguration}. */ +public class ContainerConfigurationTest { + + @Test + public void testBuilder_nullValues() { + // Java arguments element should not be null. + try { + ContainerConfiguration.builder().setProgramArguments(Arrays.asList("first", null)); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("program arguments list contains null elements", ex.getMessage()); + } + + // Entrypoint element should not be null. + try { + ContainerConfiguration.builder().setEntrypoint(Arrays.asList("first", null)); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("entrypoint contains null elements", ex.getMessage()); + } + + // Exposed ports element should not be null. + Set badPorts = new HashSet<>(Arrays.asList(Port.tcp(1000), null)); + try { + ContainerConfiguration.builder().setExposedPorts(badPorts); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("ports list contains null elements", ex.getMessage()); + } + + // Volume element should not be null. + Set badVolumes = + new HashSet<>(Arrays.asList(AbsoluteUnixPath.get("/"), null)); + try { + ContainerConfiguration.builder().setVolumes(badVolumes); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("volumes list contains null elements", ex.getMessage()); + } + + Map nullKeyMap = new HashMap<>(); + nullKeyMap.put(null, "value"); + Map nullValueMap = new HashMap<>(); + nullValueMap.put("key", null); + + // Label keys should not be null. + try { + ContainerConfiguration.builder().setLabels(nullKeyMap); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("labels map contains null keys", ex.getMessage()); + } + + // Labels values should not be null. + try { + ContainerConfiguration.builder().setLabels(nullValueMap); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("labels map contains null values", ex.getMessage()); + } + + // Environment keys should not be null. + try { + ContainerConfiguration.builder().setEnvironment(nullKeyMap); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("environment map contains null keys", ex.getMessage()); + } + + // Environment values should not be null. + try { + ContainerConfiguration.builder().setEnvironment(nullValueMap); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("environment map contains null values for key(s): key", ex.getMessage()); + } + } + + @Test + @SuppressWarnings("JdkObsolete") // for hashtable + public void testBuilder_environmentMapTypes() { + // Can accept empty environment. + Assert.assertNotNull( + ContainerConfiguration.builder().setEnvironment(ImmutableMap.of()).build()); + // Can handle other map types (https://github.com/GoogleContainerTools/jib/issues/632) + Assert.assertNotNull(ContainerConfiguration.builder().setEnvironment(new TreeMap<>())); + Assert.assertNotNull(ContainerConfiguration.builder().setEnvironment(new Hashtable<>())); + } + + @Test + public void testBuilder_user() { + ContainerConfiguration configuration = ContainerConfiguration.builder().setUser("john").build(); + Assert.assertEquals("john", configuration.getUser()); + } + + @Test + public void testBuilder_workingDirectory() { + ContainerConfiguration configuration = + ContainerConfiguration.builder().setWorkingDirectory(AbsoluteUnixPath.get("/path")).build(); + Assert.assertEquals(AbsoluteUnixPath.get("/path"), configuration.getWorkingDirectory()); + } + + @Test + public void testSetPlatforms_emptySet() { + try { + ContainerConfiguration.builder().setPlatforms(Collections.emptySet()).build(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("platforms set cannot be empty", ex.getMessage()); + } + } + + @Test + public void testAddPlatform_duplicatePlatforms() { + ContainerConfiguration configuration = + ContainerConfiguration.builder() + .addPlatform("testArchitecture", "testOS") + .addPlatform("testArchitecture", "testOS") + .build(); + Assert.assertEquals( + ImmutableSet.of(new Platform("amd64", "linux"), new Platform("testArchitecture", "testOS")), + configuration.getPlatforms()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java new file mode 100644 index 0000000000..9d0a90d592 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerHealthCheck}. */ +public class DockerHealthCheckTest { + + @Test + public void testBuild() { + DockerHealthCheck healthCheck = + DockerHealthCheck.fromCommand(ImmutableList.of("echo", "hi")) + .setInterval(Duration.ofNanos(123)) + .setTimeout(Duration.ofNanos(456)) + .setStartPeriod(Duration.ofNanos(789)) + .setRetries(10) + .build(); + + Assert.assertTrue(healthCheck.getInterval().isPresent()); + Assert.assertEquals(Duration.ofNanos(123), healthCheck.getInterval().get()); + Assert.assertTrue(healthCheck.getTimeout().isPresent()); + Assert.assertEquals(Duration.ofNanos(456), healthCheck.getTimeout().get()); + Assert.assertTrue(healthCheck.getStartPeriod().isPresent()); + Assert.assertEquals(Duration.ofNanos(789), healthCheck.getStartPeriod().get()); + Assert.assertTrue(healthCheck.getRetries().isPresent()); + Assert.assertEquals(10, (int) healthCheck.getRetries().get()); + } + + @Test + public void testBuild_invalidCommand() { + try { + DockerHealthCheck.fromCommand(ImmutableList.of()); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("command must not be empty", ex.getMessage()); + } + + try { + DockerHealthCheck.fromCommand(Arrays.asList("CMD", null)); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("command must not contain null elements", ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/AnotherDockerClient.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/AnotherDockerClient.java new file mode 100644 index 0000000000..64926306ec --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/AnotherDockerClient.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.docker; + +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.ImageDetails; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.image.ImageTarball; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +public class AnotherDockerClient implements DockerClient { + @Override + public boolean supported(Map parameters) { + return parameters.containsKey("test"); + } + + @Override + public String load(ImageTarball imageTarball, Consumer writtenByteCountListener) + throws InterruptedException, IOException { + return null; + } + + @Override + public void save( + ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) + throws InterruptedException, IOException {} + + @Override + public ImageDetails inspect(ImageReference imageReference) + throws IOException, InterruptedException { + return null; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java new file mode 100644 index 0000000000..2b451ebe05 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java @@ -0,0 +1,461 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.docker; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.docker.CliDockerClient.DockerImageDetails; +import com.google.cloud.tools.jib.image.ImageTarball; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.VoidAnswer1; + +/** Tests for {@link CliDockerClient}. */ +@RunWith(MockitoJUnitRunner.class) +public class CliDockerClientTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private ProcessBuilder mockProcessBuilder; + @Mock private Process mockProcess; + @Mock private ImageTarball imageTarball; + + @Before + public void setUp() throws IOException { + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + Mockito.doAnswer( + AdditionalAnswers.answerVoid( + (VoidAnswer1) + out -> out.write("jib".getBytes(StandardCharsets.UTF_8)))) + .when(imageTarball) + .writeTo(Mockito.any(OutputStream.class)); + } + + @Test + public void testIsDockerInstalled_fail() { + Assert.assertFalse(CliDockerClient.isDockerInstalled(Paths.get("path/to/nonexistent/file"))); + } + + @Test + public void testIsDockerInstalled_pass() throws URISyntaxException { + Assert.assertTrue( + CliDockerClient.isDockerInstalled( + Paths.get(Resources.getResource("core/docker/emptyFile").toURI()))); + } + + @Test + public void testInfo() throws InterruptedException, IOException { + String dockerInfoJson = "{ \"OSType\": \"windows\"," + "\"Architecture\": \"arm64\"}"; + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + // Simulates stdout. + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream(dockerInfoJson.getBytes())); + + DockerInfoDetails infoDetails = testDockerClient.info(); + assertThat(infoDetails.getArchitecture()).isEqualTo("arm64"); + assertThat(infoDetails.getOsType()).isEqualTo("windows"); + } + + @Test + public void testInfo_fail() throws InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + IOException exception = assertThrows(IOException.class, testDockerClient::info); + assertThat(exception) + .hasMessageThat() + .contains("'docker info' command failed with error: error"); + } + + @Test + public void testInfo_stdinFail() throws InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.getInputStream()) + .thenReturn( + new InputStream() { + + @Override + public int read() throws IOException { + throw new IOException(); + } + }); + + assertThrows(IOException.class, testDockerClient::info); + + Mockito.verify(mockProcess, Mockito.never()).waitFor(); + } + + @Test + public void testInfo_returnsUnknownKeys() throws InterruptedException, IOException { + String dockerInfoJson = "{ \"unknownOS\": \"windows\"," + "\"unknownArchitecture\": \"arm64\"}"; + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream(dockerInfoJson.getBytes())); + + DockerInfoDetails infoDetails = testDockerClient.info(); + assertThat(infoDetails.getArchitecture()).isEmpty(); + assertThat(infoDetails.getOsType()).isEmpty(); + } + + @Test + public void testLoad() throws IOException, InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + Assert.assertEquals(Collections.singletonList("load"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + + // Captures stdin. + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Mockito.when(mockProcess.getOutputStream()).thenReturn(byteArrayOutputStream); + + // Simulates stdout. + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("output".getBytes(StandardCharsets.UTF_8))); + + String output = testDockerClient.load(imageTarball, ignored -> {}); + + Assert.assertEquals( + "jib", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals("output", output); + } + + @Test + public void testLoad_stdinFail() throws InterruptedException { + DockerClient testDockerClient = new CliDockerClient(ignored -> mockProcessBuilder); + + Mockito.when(mockProcess.getOutputStream()) + .thenReturn( + new OutputStream() { + + @Override + public void write(int b) throws IOException { + throw new IOException(); + } + }); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.load(imageTarball, ignored -> {}); + Assert.fail("Write should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker load' command failed with error: error", ex.getMessage()); + } + } + + @Test + public void testLoad_stdinFail_stderrFail() throws InterruptedException { + DockerClient testDockerClient = new CliDockerClient(ignored -> mockProcessBuilder); + + Mockito.when(mockProcess.getOutputStream()) + .thenReturn( + new OutputStream() { + + @Override + public void write(int b) throws IOException { + throw new IOException("I/O failed"); + } + }); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn( + new InputStream() { + + @Override + public int read() throws IOException { + throw new IOException(); + } + }); + + try { + testDockerClient.load(imageTarball, ignored -> {}); + Assert.fail("Write should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker load' command failed with error: I/O failed", ex.getMessage()); + } + } + + @Test + public void testLoad_stdoutFail() throws InterruptedException { + DockerClient testDockerClient = new CliDockerClient(ignored -> mockProcessBuilder); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getOutputStream()).thenReturn(ByteStreams.nullOutputStream()); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("ignored".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.load(imageTarball, ignored -> {}); + Assert.fail("Process should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker load' command failed with error: error", ex.getMessage()); + } + } + + @Test + public void testSave() throws InterruptedException, IOException { + DockerClient testDockerClient = makeDockerSaveClient(); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + + long[] counter = new long[1]; + testDockerClient.save( + ImageReference.of(null, "testimage", null), + temporaryFolder.getRoot().toPath().resolve("out.tar"), + bytes -> counter[0] += bytes); + + // InputStream writes "jib", so 3 bytes of progress should have been counted. + Assert.assertEquals(3, counter[0]); + } + + @Test + public void testSave_fail() throws InterruptedException { + DockerClient testDockerClient = makeDockerSaveClient(); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.save( + ImageReference.of(null, "testimage", null), + temporaryFolder.getRoot().toPath().resolve("out.tar"), + ignored -> {}); + Assert.fail("docker save should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker save' command failed with error: error", ex.getMessage()); + } + } + + @Test + public void testDefaultProcessorBuilderFactory_customExecutable() { + ProcessBuilder processBuilder = + CliDockerClient.defaultProcessBuilderFactory("docker-executable", ImmutableMap.of()) + .apply(Arrays.asList("sub", "command")); + + Assert.assertEquals( + Arrays.asList("docker-executable", "sub", "command"), processBuilder.command()); + Assert.assertEquals(System.getenv(), processBuilder.environment()); + } + + @Test + public void testDefaultProcessorBuilderFactory_customEnvironment() { + ImmutableMap environment = ImmutableMap.of("Key1", "Value1"); + + Map expectedEnvironment = new HashMap<>(System.getenv()); + expectedEnvironment.putAll(environment); + + ProcessBuilder processBuilder = + CliDockerClient.defaultProcessBuilderFactory("docker", environment) + .apply(Collections.emptyList()); + + Assert.assertEquals(expectedEnvironment, processBuilder.environment()); + } + + @Test + public void testSize_fail() throws InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + Assert.assertEquals("inspect", subcommand.get(0)); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.inspect(ImageReference.of(null, "image", null)); + Assert.fail("docker inspect should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker inspect' command failed with error: error", ex.getMessage()); + } + } + + @Test + public void testSize_stdinFail() throws InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + Assert.assertEquals("inspect", subcommand.get(0)); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.getInputStream()) + .thenReturn( + new InputStream() { + + @Override + public int read() throws IOException { + throw new IOException(); + } + }); + + assertThrows( + IOException.class, () -> testDockerClient.inspect(ImageReference.of(null, "image", null))); + + Mockito.verify(mockProcess, Mockito.never()).waitFor(); + } + + @Test + public void testDockerImageDetails() throws DigestException, IOException { + String json = + "{\"Size\":488118507," + + "\"Id\":\"sha256:e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd\"," + + "\"RootFS\": { \"Layers\" : [" + + " \"sha256:55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1\"," + + " \"sha256:26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b\"," + + " \"sha256:8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a\"]}}\n"; + DockerImageDetails results = JsonTemplateMapper.readJson(json, DockerImageDetails.class); + Assert.assertEquals(488118507, results.getSize()); + Assert.assertEquals( + DescriptorDigest.fromHash( + "e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd"), + results.getImageId()); + Assert.assertEquals( + Arrays.asList( + DescriptorDigest.fromHash( + "55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1"), + DescriptorDigest.fromHash( + "26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b"), + DescriptorDigest.fromHash( + "8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a")), + results.getDiffIds()); + } + + @Test + public void testDockerImageDetails_unknownProperties() throws IOException, DigestException { + String json = + "{\"Unknown\": [ ], \"Structure\": [ { \"Test\": 0 } ], \"Size\": 1234," + + "\"Id\": \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"," + + "\"RootFS\": { \"Someting\": \"unrelated\", \"Layers\": [" + + " \"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\" ] } }"; + DockerImageDetails results = JsonTemplateMapper.readJson(json, DockerImageDetails.class); + Assert.assertEquals(1234, results.getSize()); + Assert.assertEquals( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + results.getImageId()); + Assert.assertEquals( + Arrays.asList( + DescriptorDigest.fromHash( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")), + results.getDiffIds()); + } + + @Test + public void testDockerImageDetails_emptyJson() throws IOException, DigestException { + DockerImageDetails details = JsonTemplateMapper.readJson("{}", DockerImageDetails.class); + Assert.assertEquals(0, details.getSize()); + Assert.assertEquals(Collections.emptyList(), details.getDiffIds()); + try { + details.getImageId(); + Assert.fail(); + } catch (DigestException ex) { + Assert.assertEquals("Invalid digest: ", ex.getMessage()); + } + } + + private DockerClient makeDockerSaveClient() { + return new CliDockerClient( + subcommand -> { + try { + if (subcommand.contains("{{.Size}}")) { + // It doesn't matter what size is actually returned by 'docker inspect' here, so just + // use 150000 as a placeholder. + Process mockSizeProcess = Mockito.mock(Process.class); + Mockito.when(mockSizeProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("150000".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcessBuilder.start()).thenReturn(mockSizeProcess); + } else { + Assert.assertEquals(Arrays.asList("save", "testimage"), subcommand); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("jib".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + } + } catch (IOException ignored) { + // ignored + } + return mockProcessBuilder; + }); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java deleted file mode 100644 index 8eecd61854..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.common.io.ByteStreams; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link DockerClient}. */ -@RunWith(MockitoJUnitRunner.class) -public class DockerClientTest { - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private ProcessBuilder mockProcessBuilder; - @Mock private Process mockProcess; - - @Before - public void setUp() throws IOException { - Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); - } - - @Test - public void testIsDockerInstalled_pass() { - Assert.assertTrue(new DockerClient(ignored -> mockProcessBuilder).isDockerInstalled()); - } - - @Test - public void testIsDockerInstalled_fail() { - ProcessBuilder nonexistentProcessBuilder = - new ProcessBuilder(Paths.get("path/to/nonexistent/file").toString()); - Assert.assertFalse(new DockerClient(ignored -> nonexistentProcessBuilder).isDockerInstalled()); - } - - @Test - public void testLoad() throws IOException, InterruptedException { - DockerClient testDockerClient = - new DockerClient( - subcommand -> { - Assert.assertEquals("load", subcommand.get(0)); - return mockProcessBuilder; - }); - Mockito.when(mockProcess.waitFor()).thenReturn(0); - - // Captures stdin. - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - Mockito.when(mockProcess.getOutputStream()).thenReturn(byteArrayOutputStream); - - // Simulates stdout. - Mockito.when(mockProcess.getInputStream()) - .thenReturn(new ByteArrayInputStream("output".getBytes(StandardCharsets.UTF_8))); - - String output = testDockerClient.load(Blobs.from("jib")); - - Assert.assertEquals( - "jib", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals("output", output); - } - - @Test - public void testLoad_stdinFail() throws InterruptedException { - DockerClient testDockerClient = new DockerClient(ignored -> mockProcessBuilder); - - Mockito.when(mockProcess.getOutputStream()) - .thenReturn( - new OutputStream() { - - @Override - public void write(int b) throws IOException { - throw new IOException(); - } - }); - Mockito.when(mockProcess.getErrorStream()) - .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); - - try { - testDockerClient.load(Blobs.from("jib")); - Assert.fail("Write should have failed"); - - } catch (IOException ex) { - Assert.assertEquals("'docker load' command failed with error: error", ex.getMessage()); - } - } - - @Test - public void testLoad_stdinFail_stderrFail() throws InterruptedException { - DockerClient testDockerClient = new DockerClient(ignored -> mockProcessBuilder); - IOException expectedIOException = new IOException(); - - Mockito.when(mockProcess.getOutputStream()) - .thenReturn( - new OutputStream() { - - @Override - public void write(int b) throws IOException { - throw expectedIOException; - } - }); - Mockito.when(mockProcess.getErrorStream()) - .thenReturn( - new InputStream() { - - @Override - public int read() throws IOException { - throw new IOException(); - } - }); - - try { - testDockerClient.load(Blobs.from("jib")); - Assert.fail("Write should have failed"); - - } catch (IOException ex) { - Assert.assertTrue(expectedIOException == ex); - } - } - - @Test - public void testLoad_stdoutFail() throws InterruptedException { - DockerClient testDockerClient = new DockerClient(ignored -> mockProcessBuilder); - Mockito.when(mockProcess.waitFor()).thenReturn(1); - - Mockito.when(mockProcess.getOutputStream()).thenReturn(ByteStreams.nullOutputStream()); - Mockito.when(mockProcess.getInputStream()) - .thenReturn(new ByteArrayInputStream("failed".getBytes(StandardCharsets.UTF_8))); - - try { - testDockerClient.load(Blobs.from("jib")); - Assert.fail("Process should have failed"); - - } catch (IOException ex) { - Assert.assertEquals("'docker load' command failed with output: failed", ex.getMessage()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java deleted file mode 100644 index 89ac1936eb..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Deque; -import java.util.List; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link DockerContextGenerator}. */ -@RunWith(MockitoJUnitRunner.class) -public class DockerContextGeneratorTest { - - private static void assertSameFiles(Path directory1, Path directory2) throws IOException { - Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); - - new DirectoryWalker(directory2) - .walk( - directory2Path -> { - Assert.assertEquals( - directory1.relativize(directory1Paths.pop()), - directory2.relativize(directory2Path)); - }); - - Assert.assertEquals(0, directory1Paths.size()); - } - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; - - @Before - public void setUpMocks() { - String expectedDependenciesPath = "/app/libs/"; - String expectedResourcesPath = "/app/resources/"; - String expectedClassesPath = "/app/classes/"; - - Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) - .thenReturn(expectedDependenciesPath); - Mockito.when(mockSourceFilesConfiguration.getResourcesPathOnImage()) - .thenReturn(expectedResourcesPath); - Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) - .thenReturn(expectedClassesPath); - } - - @Test - public void testGenerate() throws IOException, URISyntaxException { - Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); - Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); - Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); - - ImmutableList expectedDependenciesFiles = - new DirectoryWalker(testDependencies).filterRoot().walk(); - ImmutableList expectedResourcesFiles = - new DirectoryWalker(testResources).filterRoot().walk(); - ImmutableList expectedClassesFiles = new DirectoryWalker(testClasses).filterRoot().walk(); - Mockito.when(mockSourceFilesConfiguration.getDependenciesFiles()) - .thenReturn(expectedDependenciesFiles); - Mockito.when(mockSourceFilesConfiguration.getResourcesFiles()) - .thenReturn(expectedResourcesFiles); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(expectedClassesFiles); - - Path targetDirectory = temporaryFolder.newFolder().toPath(); - - /* - * Deletes the directory so that DockerContextGenerator#generate does not throw - * InsecureRecursiveDeleteException. - */ - Files.delete(targetDirectory); - - new DockerContextGenerator(mockSourceFilesConfiguration) - .setBaseImage("somebaseimage") - .generate(targetDirectory); - - Assert.assertTrue(Files.exists(targetDirectory.resolve("Dockerfile"))); - assertSameFiles(targetDirectory.resolve("libs"), testDependencies); - assertSameFiles(targetDirectory.resolve("resources"), testResources); - assertSameFiles(targetDirectory.resolve("classes"), testClasses); - } - - @Test - public void testMakeDockerfile() throws IOException { - String expectedBaseImage = "somebaseimage"; - List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); - String expectedMainClass = "SomeMainClass"; - List expectedJavaArguments = Arrays.asList("arg1", "arg2"); - List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); - - String dockerfile = - new DockerContextGenerator(mockSourceFilesConfiguration) - .setBaseImage(expectedBaseImage) - .setJvmFlags(expectedJvmFlags) - .setMainClass(expectedMainClass) - .setJavaArguments(expectedJavaArguments) - .setExposedPorts(exposedPorts) - .makeDockerfile(); - - // Need to split/rejoin the string here to avoid cross-platform troubles - List sampleDockerfile = - Resources.readLines(Resources.getResource("sampleDockerfile"), StandardCharsets.UTF_8); - Assert.assertArrayEquals( - String.join("\n", sampleDockerfile).getBytes(StandardCharsets.UTF_8), - dockerfile.getBytes(StandardCharsets.UTF_8)); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java deleted file mode 100644 index 5716bba928..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestTemplate; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; -import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.CharStreams; -import com.google.common.io.Resources; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.DigestException; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link ImageToTarballTranslator}. */ -public class ImageToTarballTranslatorTest { - - @Test - public void testToTarballBlob() - throws InvalidImageReferenceException, IOException, URISyntaxException, - LayerPropertyNotFoundException, DigestException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); - Path fileB = Paths.get(Resources.getResource("fileB").toURI()); - - DescriptorDigest fakeDigestA = - DescriptorDigest.fromHash( - "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); - DescriptorDigest fakeDigestB = - DescriptorDigest.fromHash( - "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc6"); - - Image testImage = - Image.builder() - .addLayer(new CachedLayer(fileA, new BlobDescriptor(fakeDigestA), fakeDigestA)) - .addLayer(new CachedLayer(fileB, new BlobDescriptor(fakeDigestB), fakeDigestB)) - .build(); - - Blob tarballBlob = - new ImageToTarballTranslator(testImage).toTarballBlob(ImageReference.parse("my/image:tag")); - - try (ByteArrayInputStream tarballBytesStream = - new ByteArrayInputStream(Blobs.writeToByteArray(tarballBlob)); - TarArchiveInputStream tarArchiveInputStream = - new TarArchiveInputStream(tarballBytesStream)) { - // Verifies layer with fileA was added. - TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("fileA", headerFileALayer.getName()); - String fileAString = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(Blobs.writeToString(Blobs.from(fileA)), fileAString); - - // Verifies layer with fileB was added. - TarArchiveEntry headerFileBLayer = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("fileB", headerFileBLayer.getName()); - String fileBString = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(Blobs.writeToString(Blobs.from(fileB)), fileBString); - - // Verifies container configuration was added. - TarArchiveEntry headerContainerConfiguration = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("config.json", headerContainerConfiguration.getName()); - String containerConfigJson = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - JsonTemplateMapper.readJson(containerConfigJson, ContainerConfigurationTemplate.class); - - // Verifies manifest was added. - TarArchiveEntry headerManifest = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("manifest.json", headerManifest.getName()); - String manifestJson = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - JsonTemplateMapper.readListOfJson(manifestJson, DockerLoadManifestTemplate.class); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java deleted file mode 100644 index 9ff48bc797..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker.json; - -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link DockerLoadManifestTemplate}. */ -public class DockerLoadManifestTemplateTest { - - @Test - public void testToJson() throws URISyntaxException, IOException { - // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/loadmanifest.json").toURI()); - String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - - DockerLoadManifestTemplate template = new DockerLoadManifestTemplate(); - template.setRepoTags( - ImageReference.of("testregistry", "testrepo", "testtag").toStringWithTag()); - template.addLayerFile("layer1.tar.gz"); - template.addLayerFile("layer2.tar.gz"); - template.addLayerFile("layer3.tar.gz"); - - Assert.assertEquals(expectedJson, Blobs.writeToString(JsonTemplateMapper.toBlob(template))); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java new file mode 100644 index 0000000000..142522118e --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.docker.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerManifestEntryTemplate}. */ +public class DockerManifestEntryTemplateTest { + + @Test + public void testToJson() throws URISyntaxException, IOException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/loadmanifest.json").toURI()); + String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + + DockerManifestEntryTemplate template = new DockerManifestEntryTemplate(); + template.addRepoTag( + ImageReference.of("testregistry", "testrepo", "testtag").toStringWithQualifier()); + template.addLayerFile("layer1.tar.gz"); + template.addLayerFile("layer2.tar.gz"); + template.addLayerFile("layer3.tar.gz"); + + List loadManifest = Collections.singletonList(template); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(loadManifest)); + } + + @Test + public void testFromJson() throws URISyntaxException, IOException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/loadmanifest2.json").toURI()); + String sourceJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + DockerManifestEntryTemplate template = + new ObjectMapper().readValue(sourceJson, DockerManifestEntryTemplate[].class)[0]; + + Assert.assertEquals( + ImmutableList.of("layer1.tar.gz", "layer2.tar.gz", "layer3.tar.gz"), + template.getLayerFiles()); + Assert.assertEquals("config.json", template.getConfig()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java new file mode 100644 index 0000000000..efcc910b4b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.cloud.tools.jib.api.JibEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** Tests for {@link EventHandlers}. */ +public class EventHandlersTest { + + /** Test {@link JibEvent}. */ + private interface TestJibEvent1 extends JibEvent { + + String getPayload(); + } + + /** Test implementation of {@link JibEvent}. */ + private static class TestJibEvent2 implements JibEvent { + + @Nullable private String message; + + private void assertMessageCorrect(String name) { + Assert.assertEquals("Hello " + name, message); + } + + private void sayHello(String name) { + Assert.assertNull(message); + message = "Hello " + name; + } + } + + /** Test {@link JibEvent}. */ + private static class TestJibEvent3 implements JibEvent {} + + @Test + public void testAdd() { + int[] counter = new int[1]; + EventHandlers eventHandlers = + EventHandlers.builder() + .add( + TestJibEvent1.class, + testJibEvent1 -> Assert.assertEquals("payload", testJibEvent1.getPayload())) + .add(TestJibEvent2.class, testJibEvent2 -> testJibEvent2.sayHello("Jib")) + .add(JibEvent.class, jibEvent -> counter[0]++) + .build(); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(JibEvent.class)); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent1.class)); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent2.class)); + Assert.assertEquals(1, eventHandlers.getHandlers().get(JibEvent.class).size()); + Assert.assertEquals(1, eventHandlers.getHandlers().get(TestJibEvent1.class).size()); + Assert.assertEquals(1, eventHandlers.getHandlers().get(TestJibEvent2.class).size()); + + TestJibEvent1 mockTestJibEvent1 = Mockito.mock(TestJibEvent1.class); + Mockito.when(mockTestJibEvent1.getPayload()).thenReturn("payload"); + TestJibEvent2 testJibEvent2 = new TestJibEvent2(); + + // Checks that the handlers handled their respective event types. + eventHandlers.getHandlers().get(JibEvent.class).asList().get(0).handle(mockTestJibEvent1); + eventHandlers.getHandlers().get(JibEvent.class).asList().get(0).handle(testJibEvent2); + eventHandlers.getHandlers().get(TestJibEvent1.class).asList().get(0).handle(mockTestJibEvent1); + eventHandlers.getHandlers().get(TestJibEvent2.class).asList().get(0).handle(testJibEvent2); + + Assert.assertEquals(2, counter[0]); + Mockito.verify(mockTestJibEvent1).getPayload(); + Mockito.verifyNoMoreInteractions(mockTestJibEvent1); + testJibEvent2.assertMessageCorrect("Jib"); + } + + @Test + public void testDispatch() { + List emissions = new ArrayList<>(); + + EventHandlers eventHandlers = + EventHandlers.builder() + .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 first")) + .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 second")) + .add(TestJibEvent3.class, testJibEvent3 -> emissions.add("handled 3")) + .add(JibEvent.class, jibEvent -> emissions.add("handled generic")) + .build(); + + TestJibEvent2 testJibEvent2 = new TestJibEvent2(); + TestJibEvent3 testJibEvent3 = new TestJibEvent3(); + + eventHandlers.dispatch(testJibEvent2); + eventHandlers.dispatch(testJibEvent3); + + Assert.assertEquals( + Arrays.asList( + "handled generic", + "handled 2 first", + "handled 2 second", + "handled generic", + "handled 3"), + emissions); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java new file mode 100644 index 0000000000..aa3b8737ea --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent.Level; +import com.google.cloud.tools.jib.event.EventHandlers; +import java.util.ArrayDeque; +import java.util.Deque; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LogEvent}. */ +public class LogEventTest { + + private final Deque receivedLogEvents = new ArrayDeque<>(); + + // Note that in actual code, the event handler should NOT perform thread unsafe operations like + // here. + private final EventHandlers eventHandlers = + EventHandlers.builder().add(LogEvent.class, receivedLogEvents::offer).build(); + + @Test + public void testFactories() { + eventHandlers.dispatch(LogEvent.error("error")); + eventHandlers.dispatch(LogEvent.lifecycle("lifecycle")); + eventHandlers.dispatch(LogEvent.progress("progress")); + eventHandlers.dispatch(LogEvent.warn("warn")); + eventHandlers.dispatch(LogEvent.info("info")); + eventHandlers.dispatch(LogEvent.debug("debug")); + + verifyNextLogEvent(Level.ERROR, "error"); + verifyNextLogEvent(Level.LIFECYCLE, "lifecycle"); + verifyNextLogEvent(Level.PROGRESS, "progress"); + verifyNextLogEvent(Level.WARN, "warn"); + verifyNextLogEvent(Level.INFO, "info"); + verifyNextLogEvent(Level.DEBUG, "debug"); + Assert.assertTrue(receivedLogEvents.isEmpty()); + } + + private void verifyNextLogEvent(Level level, String message) { + Assert.assertFalse(receivedLogEvents.isEmpty()); + + LogEvent logEvent = receivedLogEvents.poll(); + + Assert.assertEquals(level, logEvent.getLevel()); + Assert.assertEquals(message, logEvent.getMessage()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java new file mode 100644 index 0000000000..b80b55e5dd --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.progress.Allocation; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ProgressEvent}. */ +public class ProgressEventTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("ignored", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("ignored", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("ignored", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("ignored", 200); + + private AllocationTree() {} + } + + private static EventHandlers makeEventHandlers(Consumer progressEventConsumer) { + return EventHandlers.builder().add(ProgressEvent.class, progressEventConsumer).build(); + } + + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + private final Map allocationCompletionMap = new HashMap<>(); + + private double progress = 0.0; + + @Test + public void testAccumulateProgress() { + Consumer progressEventConsumer = + progressEvent -> { + double fractionOfRoot = progressEvent.getAllocation().getFractionOfRoot(); + long units = progressEvent.getUnits(); + + progress += units * fractionOfRoot; + }; + + EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + Assert.assertEquals(1.0 / 2 / 100 * 50, progress, DOUBLE_ERROR_MARGIN); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + Assert.assertEquals(1.0 / 2, progress, DOUBLE_ERROR_MARGIN); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 10)); + Assert.assertEquals(1.0 / 2 + 1.0 / 2 / 200 * 10, progress, DOUBLE_ERROR_MARGIN); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 190)); + Assert.assertEquals(1.0, progress, DOUBLE_ERROR_MARGIN); + } + + @Test + public void testSmoke() { + Consumer progressEventConsumer = + progressEvent -> { + Allocation allocation = progressEvent.getAllocation(); + long units = progressEvent.getUnits(); + + updateCompletionMap(allocation, units); + }; + + EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + + Assert.assertEquals(1, allocationCompletionMap.size()); + Assert.assertEquals(50, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + + Assert.assertEquals(3, allocationCompletionMap.size()); + Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.root).longValue()); + + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 200)); + + Assert.assertEquals(4, allocationCompletionMap.size()); + Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); + Assert.assertEquals(200, allocationCompletionMap.get(AllocationTree.child2).longValue()); + Assert.assertEquals(2, allocationCompletionMap.get(AllocationTree.root).longValue()); + } + + @Test + public void testType() { + // Used to test whether or not progress event was consumed + boolean[] called = new boolean[] {false}; + Consumer buildImageConsumer = + progressEvent -> { + called[0] = true; + }; + + EventHandlers eventHandlers = makeEventHandlers(buildImageConsumer); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 50)); + Assert.assertTrue(called[0]); + } + + /** + * Updates the {@link #allocationCompletionMap} with {@code units} more progress for {@code + * allocation}. This also updates {@link Allocation} parents if {@code allocation} is complete in + * terms of progress. + * + * @param allocation the allocation the progress is made on + * @param units the progress units + */ + private void updateCompletionMap(Allocation allocation, long units) { + if (allocationCompletionMap.containsKey(allocation)) { + units += allocationCompletionMap.get(allocation); + } + allocationCompletionMap.put(allocation, units); + + if (allocation.getAllocationUnits() == units) { + allocation + .getParent() + .ifPresent(parentAllocation -> updateCompletionMap(parentAllocation, 1)); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java new file mode 100644 index 0000000000..0f13673469 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java @@ -0,0 +1,222 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.cloud.tools.jib.MultithreadedExecutor; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link AllocationCompletionTracker}. */ +public class AllocationCompletionTrackerTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("root", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("child1", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("child1Child", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("child2", 200); + + private AllocationTree() {} + } + + @Test + public void testGetUnfinishedAllocations_singleThread() { + AllocationCompletionTracker allocationCompletionTracker = new AllocationCompletionTracker(); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.root, 0L)); + Assert.assertEquals( + Collections.singletonList(AllocationTree.root), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 0L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 50L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 50L)); + Assert.assertEquals( + Collections.singletonList(AllocationTree.root), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child2, 100L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child2, 100L)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertFalse(allocationCompletionTracker.updateProgress(AllocationTree.child2, 0L)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + + try { + allocationCompletionTracker.updateProgress(AllocationTree.child1, 1L); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals("Progress exceeds max for 'child1': 1 more beyond 1", ex.getMessage()); + } + } + + @Test + public void testGetUnfinishedAllocations_multipleThreads() + throws InterruptedException, ExecutionException, IOException { + try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { + AllocationCompletionTracker allocationCompletionTracker = new AllocationCompletionTracker(); + + // Adds root, child1, and child1Child. + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.root, 0L))); + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L))); + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 0L))); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + // Adds 50 to child1Child and 100 to child2. + List> callables = new ArrayList<>(150); + callables.addAll( + Collections.nCopies( + 50, + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 1L))); + callables.addAll( + Collections.nCopies( + 100, () -> allocationCompletionTracker.updateProgress(AllocationTree.child2, 1L))); + + Assert.assertEquals( + Collections.nCopies(150, true), multithreadedExecutor.invokeAll(callables)); + Assert.assertEquals( + Arrays.asList( + AllocationTree.root, + AllocationTree.child1, + AllocationTree.child1Child, + AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + // 0 progress doesn't do anything. + Assert.assertEquals( + Collections.nCopies(100, false), + multithreadedExecutor.invokeAll( + Collections.nCopies( + 100, + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L)))); + Assert.assertEquals( + Arrays.asList( + AllocationTree.root, + AllocationTree.child1, + AllocationTree.child1Child, + AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + // Adds 50 to child1Child and 100 to child2 to finish it up. + Assert.assertEquals( + Collections.nCopies(150, true), multithreadedExecutor.invokeAll(callables)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + } + } + + @Test + public void testGetUnfinishedLeafTasks() { + AllocationCompletionTracker tracker = new AllocationCompletionTracker(); + tracker.updateProgress(AllocationTree.root, 0); + Assert.assertEquals(Arrays.asList("root"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1, 0); + Assert.assertEquals(Arrays.asList("child1"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 0); + Assert.assertEquals(Arrays.asList("child1", "child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 0); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Collections.emptyList(), tracker.getUnfinishedLeafTasks()); + } + + @Test + public void testGetUnfinishedLeafTasks_differentUpdateOrder() { + AllocationCompletionTracker tracker = new AllocationCompletionTracker(); + tracker.updateProgress(AllocationTree.root, 0); + Assert.assertEquals(Arrays.asList("root"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 0); + Assert.assertEquals(Arrays.asList("child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1, 0); + Assert.assertEquals(Arrays.asList("child2", "child1"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 0); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Collections.emptyList(), tracker.getUnfinishedLeafTasks()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java new file mode 100644 index 0000000000..bfb060dd1e --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Allocation}. */ +public class AllocationTest { + + /** Error margin for checking equality of two doubles. */ + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + @Test + public void testSmoke_linear() { + Allocation root = Allocation.newRoot("root", 1); + Allocation node1 = root.newChild("node1", 2); + Allocation node2 = node1.newChild("node2", 3); + + Assert.assertEquals("node2", node2.getDescription()); + Assert.assertEquals(3, node2.getAllocationUnits()); + Assert.assertEquals(1.0 / 2 / 3, node2.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertTrue(node2.getParent().isPresent()); + Assert.assertEquals(node1, node2.getParent().get()); + + Assert.assertEquals("node1", node1.getDescription()); + Assert.assertEquals(2, node1.getAllocationUnits()); + Assert.assertTrue(node1.getParent().isPresent()); + Assert.assertEquals(root, node1.getParent().get()); + Assert.assertEquals(1.0 / 2, node1.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + + Assert.assertEquals("root", root.getDescription()); + Assert.assertEquals(1, root.getAllocationUnits()); + Assert.assertFalse(root.getParent().isPresent()); + Assert.assertEquals(1.0, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + } + + @Test + public void testFractionOfRoot_tree_partial() { + Allocation root = Allocation.newRoot("ignored", 10); + Allocation left = root.newChild("ignored", 2); + Allocation right = root.newChild("ignored", 4); + Allocation leftDown = left.newChild("ignored", 20); + Allocation rightLeft = right.newChild("ignored", 20); + Allocation rightRight = right.newChild("ignored", 100); + Allocation rightRightDown = rightRight.newChild("ignored", 200); + + Assert.assertEquals(1.0 / 10, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 2, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 2 / 20, leftDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4 / 20, rightLeft.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4 / 100, rightRight.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals( + 1.0 / 10 / 4 / 100 / 200, rightRightDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + } + + @Test + public void testFractionOfRoot_tree_complete() { + Allocation root = Allocation.newRoot("ignored", 2); + + Allocation left = root.newChild("ignored", 3); + Allocation leftLeft = left.newChild("ignored", 1); + Allocation leftLeftDown = leftLeft.newChild("ignored", 100); + Allocation leftMiddle = left.newChild("ignored", 100); + Allocation leftRight = left.newChild("ignored", 100); + + Allocation right = root.newChild("ignored", 1); + Allocation rightDown = right.newChild("ignored", 100); + + // Checks that the leaf allocations add up to a full 1.0. + double total = + leftLeftDown.getFractionOfRoot() * leftLeftDown.getAllocationUnits() + + leftMiddle.getFractionOfRoot() * leftMiddle.getAllocationUnits() + + leftRight.getFractionOfRoot() * leftRight.getAllocationUnits() + + rightDown.getFractionOfRoot() * rightDown.getAllocationUnits(); + Assert.assertEquals(1.0, total, DOUBLE_ERROR_MARGIN); + } + + @Test + public void testNonPositiveAllocationUnits() { + Allocation root = Allocation.newRoot("ignored", 2); + + Allocation left = root.newChild("ignored", -30); + Allocation right = root.newChild("ignored", 0); + + Assert.assertEquals(0.5, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(2, root.getAllocationUnits()); + + Assert.assertEquals(0.5, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1, left.getAllocationUnits()); + + Assert.assertEquals(0.5, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1, right.getAllocationUnits()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java new file mode 100644 index 0000000000..484620dff5 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.cloud.tools.jib.MultithreadedExecutor; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.DoubleAccumulator; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ProgressEventHandler}. */ +public class ProgressEventHandlerTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("root", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("child1", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("child1Child", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("child2", 200); + + private AllocationTree() {} + } + + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + @Test + public void testAccept() throws ExecutionException, InterruptedException, IOException { + try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { + DoubleAccumulator maxProgress = new DoubleAccumulator(Double::max, 0); + + ProgressEventHandler progressEventHandler = + new ProgressEventHandler(update -> maxProgress.accumulate(update.getProgress())); + EventHandlers eventHandlers = + EventHandlers.builder().add(ProgressEvent.class, progressEventHandler).build(); + + // Adds root, child1, and child1Child. + multithreadedExecutor.invoke( + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.root, 0L)); + return null; + }); + multithreadedExecutor.invoke( + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + return null; + }); + multithreadedExecutor.invoke( + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L)); + return null; + }); + Assert.assertEquals(0.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); + + // Adds 50 to child1Child and 100 to child2. + List> callables = new ArrayList<>(150); + callables.addAll( + Collections.nCopies( + 50, + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L)); + return null; + })); + callables.addAll( + Collections.nCopies( + 100, + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 1L)); + return null; + })); + + multithreadedExecutor.invokeAll(callables); + + Assert.assertEquals( + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, maxProgress.get(), DOUBLE_ERROR_MARGIN); + + // 0 progress doesn't do anything. + multithreadedExecutor.invokeAll( + Collections.nCopies( + 100, + () -> { + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + return null; + })); + Assert.assertEquals( + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, maxProgress.get(), DOUBLE_ERROR_MARGIN); + + // Adds 50 to child1Child and 100 to child2 to finish it up. + multithreadedExecutor.invokeAll(callables); + Assert.assertEquals(1.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java index 5f605f258d..565163eab4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -38,7 +38,7 @@ public class DirectoryWalkerTest { @Before public void setUp() throws URISyntaxException { - testDir = Paths.get(Resources.getResource("layer").toURI()); + testDir = Paths.get(Resources.getResource("core/layer").toURI()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java index b1471799dd..abfe2f3e4f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -31,16 +31,16 @@ /** Tests for {@link FileOperations}. */ public class FileOperationsTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testCopy() throws IOException, URISyntaxException { Path destDir = temporaryFolder.newFolder().toPath(); Path libraryA = - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()); + Paths.get(Resources.getResource("core/application/dependencies/libraryA.jar").toURI()); Path libraryB = - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI()); - Path dirLayer = Paths.get(Resources.getResource("layer").toURI()); + Paths.get(Resources.getResource("core/application/dependencies/libraryB.jar").toURI()); + Path dirLayer = Paths.get(Resources.getResource("core/layer").toURI()); FileOperations.copy(ImmutableList.of(libraryA, libraryB, dirLayer), destDir); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java new file mode 100644 index 0000000000..1181157cfe --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link LockFile}. */ +public class LockFileTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testLockAndRelease() throws InterruptedException { + AtomicInteger atomicInt = new AtomicInteger(0); + + // Runnable that would produce a race condition without a lock file + Runnable procedure = + () -> { + try (LockFile ignored = + LockFile.lock(temporaryFolder.getRoot().toPath().resolve("testLock"))) { + Assert.assertTrue(Files.exists(temporaryFolder.getRoot().toPath().resolve("testLock"))); + + int valueBeforeSleep = atomicInt.intValue(); + Thread.sleep(100); + atomicInt.set(valueBeforeSleep + 1); + + } catch (InterruptedException | IOException ex) { + throw new AssertionError(ex); + } + }; + + // Run the runnable once in this thread + once in the main thread + Thread thread = new Thread(procedure); + thread.start(); + procedure.run(); + thread.join(); + + // Assert no overlap while lock was in place + Assert.assertEquals(2, atomicInt.intValue()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java new file mode 100644 index 0000000000..2c2575a508 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link TempDirectoryProvider}. */ +public class TempDirectoryProviderTest { + + private static void createFilesInDirectory(Path directory) + throws IOException, URISyntaxException { + Path testFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + new DirectoryWalker(testFilesDirectory) + .filterRoot() + .walk(path -> Files.copy(path, directory.resolve(testFilesDirectory.relativize(path)))); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testClose_directoriesDeleted() throws IOException, URISyntaxException { + Path parent = temporaryFolder.newFolder().toPath(); + + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path directory1 = tempDirectoryProvider.newDirectory(parent); + createFilesInDirectory(directory1); + Path directory2 = tempDirectoryProvider.newDirectory(parent); + createFilesInDirectory(directory2); + + tempDirectoryProvider.close(); + Assert.assertFalse(Files.exists(directory1)); + Assert.assertFalse(Files.exists(directory2)); + } + } + + @Test + public void testClose_directoryNotDeletedIfMoved() throws IOException, URISyntaxException { + Path destinationParent = temporaryFolder.newFolder().toPath(); + + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path directory = tempDirectoryProvider.newDirectory(destinationParent); + createFilesInDirectory(directory); + + Assert.assertFalse(Files.exists(destinationParent.resolve("destination"))); + Files.move(directory, destinationParent.resolve("destination")); + + tempDirectoryProvider.close(); + Assert.assertFalse(Files.exists(directory)); + Assert.assertTrue(Files.exists(destinationParent.resolve("destination"))); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java deleted file mode 100644 index 36f9c2d0b6..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.filesystem; - -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; -import java.util.Properties; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -/** Tests for {@link UserCacheHome}. */ -public class UserCacheHomeTest { - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private String fakeCacheHome; - - @Before - public void setUp() throws IOException { - fakeCacheHome = temporaryFolder.newFolder().getPath(); - } - - @Test - public void testGetCacheHome_hasXdgCacheHome() { - Map fakeEnvironment = ImmutableMap.of("XDG_CACHE_HOME", fakeCacheHome); - - Assert.assertEquals( - Paths.get(fakeCacheHome), - UserCacheHome.getCacheHome(Mockito.mock(Properties.class), fakeEnvironment)); - } - - @Test - public void testGetCacheHome_linux() { - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", fakeCacheHome); - fakeProperties.setProperty("os.name", "os is LiNuX"); - - Assert.assertEquals( - Paths.get(fakeCacheHome).resolve(".cache"), - UserCacheHome.getCacheHome(fakeProperties, Collections.emptyMap())); - } - - @Test - public void testGetCacheHome_windows() { - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", "nonexistent"); - fakeProperties.setProperty("os.name", "os is WiNdOwS"); - - Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeCacheHome); - - Assert.assertEquals( - Paths.get(fakeCacheHome), UserCacheHome.getCacheHome(fakeProperties, fakeEnvironment)); - } - - @Test - public void testGetCacheHome_mac() throws IOException { - Path libraryApplicationSupport = - Paths.get(fakeCacheHome).resolve("Library").resolve("Application Support"); - Files.createDirectories(libraryApplicationSupport); - - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", fakeCacheHome); - fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); - - Assert.assertEquals( - libraryApplicationSupport, - UserCacheHome.getCacheHome(fakeProperties, Collections.emptyMap())); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java new file mode 100644 index 0000000000..a3e23a5b95 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link XdgDirectories}. */ +public class XdgDirectoriesTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private String fakeCacheHome; + private String fakeConfigHome; + + @Before + public void setUp() throws IOException { + fakeCacheHome = temporaryFolder.newFolder().getPath(); + fakeConfigHome = temporaryFolder.newFolder().getPath(); + } + + @Test + public void testGetCacheHome_hasXdgCacheHome() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + Map fakeEnvironment = ImmutableMap.of("XDG_CACHE_HOME", fakeCacheHome); + + fakeProperties.setProperty("os.name", "linux"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "windows"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib").resolve("Cache"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "mac"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetCacheHome_linux() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + fakeProperties.setProperty("os.name", "os is LiNuX"); + + Assert.assertEquals( + Paths.get(fakeCacheHome, ".cache").resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getCacheHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetCacheHome_windows() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", "nonexistent"); + fakeProperties.setProperty("os.name", "os is WiNdOwS"); + + Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeCacheHome); + + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib").resolve("Cache"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetCacheHome_mac() throws IOException { + Path libraryApplicationSupport = Paths.get(fakeCacheHome, "Library", "Caches"); + Files.createDirectories(libraryApplicationSupport); + + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); + + Assert.assertEquals( + libraryApplicationSupport.resolve("Google").resolve("Jib"), + XdgDirectories.getCacheHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetConfigHome_hasXdgConfigHome() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + Map fakeEnvironment = ImmutableMap.of("XDG_CONFIG_HOME", fakeConfigHome); + + fakeProperties.setProperty("os.name", "linux"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "windows"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib").resolve("Config"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "mac"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetConfigHome_linux() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + fakeProperties.setProperty("os.name", "os is LiNuX"); + + Assert.assertEquals( + Paths.get(fakeConfigHome, ".config").resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getConfigHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetConfigHome_windows() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", "nonexistent"); + fakeProperties.setProperty("os.name", "os is WiNdOwS"); + + Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeConfigHome); + + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib").resolve("Config"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetConfigHome_mac() throws IOException { + Path libraryApplicationSupport = Paths.get(fakeConfigHome, "Library", "Preferences"); + Files.createDirectories(libraryApplicationSupport); + + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); + + Assert.assertEquals( + libraryApplicationSupport.resolve("Google").resolve("Jib"), + XdgDirectories.getConfigHome(fakeProperties, Collections.emptyMap())); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java deleted file mode 100644 index cd1800ddef..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.net.UnknownHostException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.ExecutionException; -import org.apache.http.conn.HttpHostConnectException; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link BuildStepsRunner}. */ -@RunWith(MockitoJUnitRunner.class) -public class BuildStepsRunnerTest { - - private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = - new HelpfulSuggestions( - "messagePrefix", - "clearCacheCommand", - "baseImageCredHelperConfiguration", - registry -> "baseImageAuthConfiguration " + registry, - "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry); - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private BuildSteps mockBuildSteps; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; - @Mock private BuildLogger mockBuildLogger; - @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; - @Mock private HttpResponseException mockHttpResponseException; - @Mock private ExecutionException mockExecutionException; - @Mock private BuildConfiguration mockBuildConfiguration; - - private BuildStepsRunner testBuildImageStepsRunner; - - @Before - public void setUpMocks() { - testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps); - - Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockBuildSteps.getSourceFilesConfiguration()) - .thenReturn(mockSourceFilesConfiguration); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); - Mockito.when(mockSourceFilesConfiguration.getResourcesFiles()).thenReturn(ImmutableList.of()); - Mockito.when(mockSourceFilesConfiguration.getDependenciesFiles()) - .thenReturn(ImmutableList.of()); - } - - @Test - public void testBuildImage_pass() throws BuildStepsExecutionException { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - } - - @Test - public void testBuildImage_cacheMetadataCorruptedException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - CacheMetadataCorruptedException mockCacheMetadataCorruptedException = - Mockito.mock(CacheMetadataCorruptedException.class); - Mockito.doThrow(mockCacheMetadataCorruptedException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean(), ex.getMessage()); - Assert.assertEquals(mockCacheMetadataCorruptedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_httpHostConnectException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - HttpHostConnectException mockHttpHostConnectException = - Mockito.mock(HttpHostConnectException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect(), ex.getMessage()); - Assert.assertEquals(mockHttpHostConnectException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_unknownHostException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forUnknownHost(), ex.getMessage()); - Assert.assertEquals(mockUnknownHostException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_insecureRegistryException() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { - InsecureRegistryException mockInsecureRegistryException = - Mockito.mock(InsecureRegistryException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage()); - Assert.assertEquals(mockInsecureRegistryException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getImageReference()) - .thenReturn("someregistry/somerepository"); - Mockito.when(mockHttpResponseException.getStatusCode()) - .thenReturn(HttpStatusCodes.STATUS_CODE_FORBIDDEN); - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"), - ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getBaseImageRegistry()).thenReturn("someregistry"); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForBaseImage("someregistry"), - ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_other() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getBaseImageRegistry()).thenReturn("someregistry"); - Mockito.when(mockBuildConfiguration.getBaseImageCredentialHelperName()) - .thenReturn("some-credential-helper"); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forCredentialsNotCorrect("someregistry"), ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_other() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Throwable throwable = new Throwable(); - Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); - Assert.assertEquals(throwable, ex.getCause()); - } - } - - @Test - public void testBuildImage_otherException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - IOException ioException = new IOException(); - Mockito.doThrow(ioException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); - Assert.assertEquals(ioException, ex.getCause()); - } - } - - @Test - public void testBuildImage_cacheDirectoryNotOwnedException_needsClean() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { - Path expectedCacheDirectory = Paths.get("some/path"); - - CacheDirectoryNotOwnedException mockCacheDirectoryNotOwnedException = - Mockito.mock(CacheDirectoryNotOwnedException.class); - Mockito.when(mockCacheDirectoryNotOwnedException.getCacheDirectory()) - .thenReturn(expectedCacheDirectory); - Mockito.doThrow(mockCacheDirectoryNotOwnedException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getApplicationLayersCacheConfiguration()) - .thenReturn(CacheConfiguration.forPath(expectedCacheDirectory)); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean(), ex.getMessage()); - Assert.assertEquals(mockCacheDirectoryNotOwnedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_cacheDirectoryNotOwnedException() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { - Path expectedCacheDirectory = Paths.get("some/path"); - - CacheDirectoryNotOwnedException mockCacheDirectoryNotOwnedException = - Mockito.mock(CacheDirectoryNotOwnedException.class); - Mockito.when(mockCacheDirectoryNotOwnedException.getCacheDirectory()) - .thenReturn(expectedCacheDirectory); - Mockito.doThrow(mockCacheDirectoryNotOwnedException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getApplicationLayersCacheConfiguration()) - .thenReturn(CacheConfiguration.forPath(Paths.get("another/path"))); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forCacheDirectoryNotOwned(expectedCacheDirectory), - ex.getMessage()); - Assert.assertEquals(mockCacheDirectoryNotOwnedException, ex.getCause()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java new file mode 100644 index 0000000000..9c05c08a05 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -0,0 +1,305 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link CredentialRetrieverFactory}. */ +@RunWith(MockitoJUnitRunner.class) +public class CredentialRetrieverFactoryTest { + + private static final Credential FAKE_CREDENTIALS = Credential.from("username", "password"); + + @Mock private Consumer mockLogger; + @Mock private DockerCredentialHelper mockDockerCredentialHelper; + @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private GoogleCredentials mockGoogleCredentials; + + @Before + public void setUp() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + Mockito.when( + mockDockerCredentialHelperFactory.create( + Mockito.anyString(), Mockito.any(Path.class), Mockito.anyMap())) + .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); + Mockito.when(mockGoogleCredentials.getAccessToken()) + .thenReturn(new AccessToken("my-token", null)); + } + + @Test + public void testDockerCredentialHelper() throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("registry", "repo"); + + Assert.assertEquals( + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory + .dockerCredentialHelper(Paths.get("docker-credential-foo")) + .retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("registry", Paths.get("docker-credential-foo"), Collections.emptyMap()); + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle("Using credential helper docker-credential-foo for registry/repo")); + } + + @Test + public void testDockerCredentialHelperWithEnvironment() throws CredentialRetrievalException { + Map environment = Collections.singletonMap("ENV_VARIABLE", "Value"); + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("registry", "repo", environment); + + Assert.assertEquals( + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory + .dockerCredentialHelper(Paths.get("docker-credential-foo")) + .retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("registry", Paths.get("docker-credential-foo"), environment); + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle("Using credential helper docker-credential-foo for registry/repo")); + } + + @Test + public void testWellKnownCredentialHelpers() throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("something.gcr.io", "repo"); + + Assert.assertEquals( + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("something.gcr.io", Paths.get("docker-credential-gcr"), Collections.emptyMap()); + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle( + "Using credential helper docker-credential-gcr for something.gcr.io/repo")); + } + + @Test + public void testWellKnownCredentialHelpers_info() + throws CredentialRetrievalException, IOException { + CredentialHelperNotFoundException notFoundException = + Mockito.mock(CredentialHelperNotFoundException.class); + Mockito.when(notFoundException.getMessage()).thenReturn("warning"); + Mockito.when(notFoundException.getCause()).thenReturn(new IOException("the root cause")); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenThrow(notFoundException); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("something.amazonaws.com", "repo"); + + Assert.assertFalse( + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve().isPresent()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create( + "something.amazonaws.com", + Paths.get("docker-credential-ecr-login"), + Collections.emptyMap()); + Mockito.verify(mockLogger).accept(LogEvent.info("warning")); + Mockito.verify(mockLogger).accept(LogEvent.info(" Caused by: the root cause")); + } + + @Test + public void testDockerConfig() throws IOException, CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("registry", "repo"); + + Path dockerConfig = Paths.get("/foo/config.json"); + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + Mockito.mock(DockerConfigCredentialRetriever.class); + Mockito.when(dockerConfigCredentialRetriever.retrieve(mockLogger)) + .thenReturn(Optional.of(FAKE_CREDENTIALS)); + Mockito.when(dockerConfigCredentialRetriever.getDockerConfigFile()).thenReturn(dockerConfig); + + Assert.assertEquals( + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.dockerConfig(dockerConfigCredentialRetriever).retrieve()); + + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle( + "Using credentials from Docker config (" + dockerConfig + ") for registry/repo")); + } + + @Test + public void testGoogleApplicationDefaultCredentials_notGoogleContainerRegistry() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("non.gcr.registry", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verifyNoInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_adcNotPresent() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + new CredentialRetrieverFactory( + ImageReference.of("awesome.gcr.io", "repository", null), + mockLogger, + mockDockerCredentialHelperFactory, + () -> { + throw new IOException("ADC not present"); + }, + Collections.emptyMap()); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: ADC not present")); + } + + @Test + public void testGoogleApplicationDefaultCredentials_refreshFailure() + throws CredentialRetrievalException, IOException { + Mockito.doThrow(new IOException("refresh failed")) + .when(mockGoogleCredentials) + .refreshIfExpired(); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: refresh failed")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_endUserCredentials() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repo"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials, Mockito.never()).createScoped(Mockito.anyString()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle( + "Using Google Application Default Credentials for awesome.gcr.io/repo")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_endUserCredentials_artifactRegistry() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("us-docker.pkg.dev", "my-project/repo/my-app"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials, Mockito.never()).createScoped(Mockito.anyString()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept( + LogEvent.lifecycle( + "Using Google Application Default Credentials for " + + "us-docker.pkg.dev/my-project/repo/my-app")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_serviceAccount() + throws CredentialRetrievalException { + Mockito.when(mockGoogleCredentials.createScopedRequired()).thenReturn(true); + Mockito.when(mockGoogleCredentials.createScoped(Mockito.anyCollection())) + .thenReturn(mockGoogleCredentials); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("gcr.io", "repo"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials) + .createScoped( + Collections.singletonList("https://www.googleapis.com/auth/devstorage.read_write")); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); + Mockito.verify(mockLogger) + .accept(LogEvent.lifecycle("Using Google Application Default Credentials for gcr.io/repo")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + private CredentialRetrieverFactory createCredentialRetrieverFactory( + String registry, String repository) { + return createCredentialRetrieverFactory(registry, repository, Collections.emptyMap()); + } + + private CredentialRetrieverFactory createCredentialRetrieverFactory( + String registry, String repository, Map environment) { + ImageReference imageReference = ImageReference.of(registry, repository, null); + return new CredentialRetrieverFactory( + imageReference, + mockLogger, + mockDockerCredentialHelperFactory, + () -> mockGoogleCredentials, + environment); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java deleted file mode 100644 index 3531af8308..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link HelpfulSuggestions}. */ -public class HelpfulSuggestionsTest { - - private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = - new HelpfulSuggestions( - "messagePrefix", - "clearCacheCommand", - "baseImageCredHelperConfiguration", - registry -> "baseImageAuthConfiguration " + registry, - "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry); - - @Test - public void testSuggestions_smoke() { - Assert.assertEquals( - "messagePrefix, perhaps you should make sure your Internet is up and that the registry you are pushing to exists", - TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect()); - Assert.assertEquals( - "messagePrefix, perhaps you should make sure that the registry you configured exists/is spelled properly", - TEST_HELPFUL_SUGGESTIONS.forUnknownHost()); - Assert.assertEquals( - "messagePrefix, perhaps you should run 'clearCacheCommand' to clear your build cache", - TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean()); - Assert.assertEquals( - "messagePrefix, perhaps you should check that 'cacheDirectory' is not used by another application or set the `useOnlyProjectCache` configuration", - TEST_HELPFUL_SUGGESTIONS.forCacheDirectoryNotOwned(Paths.get("cacheDirectory"))); - Assert.assertEquals( - "messagePrefix, perhaps you should make sure you have permissions for imageReference", - TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("imageReference")); - Assert.assertEquals( - "messagePrefix, perhaps you should set a credential helper name with the configuration 'baseImageCredHelperConfiguration' or baseImageAuthConfiguration registry", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForBaseImage("registry")); - Assert.assertEquals( - "messagePrefix, perhaps you should set a credential helper name with the configuration 'targetImageCredHelperConfiguration' or targetImageAuthConfiguration registry", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForTargetImage("registry")); - Assert.assertEquals( - "messagePrefix, perhaps you should make sure your credentials for 'registry' are set up correctly", - TEST_HELPFUL_SUGGESTIONS.forCredentialsNotCorrect("registry")); - Assert.assertEquals( - "messagePrefix, perhaps you should clear directory manually before creating the Docker context", - TEST_HELPFUL_SUGGESTIONS.forDockerContextInsecureRecursiveDelete("directory")); - Assert.assertEquals( - "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", - TEST_HELPFUL_SUGGESTIONS.forMainClassNotFound("plugin")); - Assert.assertEquals( - "messagePrefix, perhaps you should add a parameter configuration parameter to your buildFile or set the parameter via the commandline (e.g. 'command').", - TEST_HELPFUL_SUGGESTIONS.forToNotConfigured("parameter", "buildFile", "command")); - Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java deleted file mode 100644 index 3b2d5cc7b6..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for MainClassFinder. */ -@RunWith(MockitoJUnitRunner.class) -public class MainClassFinderTest { - - @Mock private BuildLogger mockBuildLogger; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; - @Mock private ProjectProperties mockProjectProperties; - @Mock private HelpfulSuggestions mockHelpfulSuggestions; - - private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); - - @Before - public void setup() { - Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); - Mockito.when(mockProjectProperties.getSourceFilesConfiguration()) - .thenReturn(mockSourceFilesConfiguration); - Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) - .thenReturn(mockHelpfulSuggestions); - Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); - } - - @Test - public void testFindMainClass_simple() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("HelloWorld")); - } - - @Test - public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { - Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("multi.layered.HelloWorld")); - } - - @Test - public void testFindMainClass_noClass() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertTrue(mainClasses.isEmpty()); - } - - @Test - public void testFindMainClass_multiple() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(2, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("multi.layered.HelloMoon")); - Assert.assertTrue(mainClasses.contains("HelloWorld")); - } - - @Test - public void testFindMainClass_extension() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); - } - - @Test - public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { - Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); - } - - @Test - public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { - Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); - } - - @Test - public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { - Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("HelloWorld$InnerClass")); - } - - @Test - public void testResolveMainClass() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("some.main.class"); - Assert.assertEquals( - "some.main.class", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Assert.assertEquals( - "configured", MainClassFinder.resolveMainClass("configured", mockProjectProperties)); - } - - @Test - public void testResolveMainClass_notValid() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(fakeClassesPath); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_multipleInferredWithBackup() - throws MainClassInferenceException, URISyntaxException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()) - .thenReturn( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class").toURI()))); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()) - .thenReturn( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class").toURI()))); - try { - MainClassFinder.resolveMainClass(null, mockProjectProperties); - Assert.fail(); - } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: HelloWorld, multi.layered.HelloMoon"); - } - } - - @Test - public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); - try { - MainClassFinder.resolveMainClass(null, mockProjectProperties); - Assert.fail(); - } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions("Main class was not found"); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java new file mode 100644 index 0000000000..8774de988b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.global; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; + +/** Tests for {@link JibSystemProperties}. */ +public class JibSystemPropertiesTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + + @Test + public void testCheckHttpTimeoutProperty_okWhenUndefined() throws NumberFormatException { + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + JibSystemProperties.checkHttpTimeoutProperty(); + } + + @Test + public void testCheckHttpTimeoutProperty_stringValue() { + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); + try { + JibSystemProperties.checkHttpTimeoutProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); + } + } + + @Test + public void testCheckHttpProxyPortProperty_undefined() throws NumberFormatException { + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyPort"); + JibSystemProperties.checkProxyPortProperty(); + } + + @Test + public void testCheckHttpProxyPortProperty() throws NumberFormatException { + System.setProperty("http.proxyPort", "0"); + System.setProperty("https.proxyPort", "0"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "1"); + System.setProperty("https.proxyPort", "1"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "65535"); + System.setProperty("https.proxyPort", "65535"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "65534"); + System.setProperty("https.proxyPort", "65534"); + JibSystemProperties.checkProxyPortProperty(); + } + + @Test + public void testCheckHttpProxyPortProperty_negativeValue() { + System.setProperty("http.proxyPort", "-1"); + System.clearProperty("https.proxyPort"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort cannot be less than 0: -1", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "-1"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("https.proxyPort cannot be less than 0: -1", ex.getMessage()); + } + } + + @Test + public void testCheckHttpProxyPortProperty_over65535() { + System.setProperty("http.proxyPort", "65536"); + System.clearProperty("https.proxyPort"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort cannot be greater than 65535: 65536", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "65536"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("https.proxyPort cannot be greater than 65535: 65536", ex.getMessage()); + } + } + + @Test + public void testCheckHttpProxyPortProperty_stringValue() { + System.setProperty("http.proxyPort", "some string"); + System.clearProperty("https.proxyPort"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort must be an integer: some string", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "some string"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("https.proxyPort must be an integer: some string", ex.getMessage()); + } + } + + @Test + public void testUseBlobMountsPropertyName() { + Assert.assertEquals("jib.blobMounts", JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + } + + @Test + public void testUseBlobMounts_undefined() { + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_true() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "true"); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_false() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "false"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_other() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "nonbool"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testSkipExistingImages_undefined() { + System.clearProperty(JibSystemProperties.SKIP_EXISTING_IMAGES); + Assert.assertFalse(JibSystemProperties.skipExistingImages()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index 9cece042c2..51a628d1b7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.hash; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -26,36 +27,25 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.DigestException; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; /** Tests for {@link CountingDigestOutputStream}. */ public class CountingDigestOutputStreamTest { - private Map knownSha256Hashes; - - @Before - public void setUp() { - knownSha256Hashes = - Collections.unmodifiableMap( - new HashMap() { - { - put( - "crepecake", - "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); - put("12345", "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); - put("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - } - }); - } + private static final ImmutableMap KNOWN_SHA256_HASHES = + ImmutableMap.of( + "crepecake", + "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c", + "12345", + "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5", + "", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); @Test public void test_smokeTest() throws IOException, DigestException { - for (Map.Entry knownHash : knownSha256Hashes.entrySet()) { + for (Map.Entry knownHash : KNOWN_SHA256_HASHES.entrySet()) { String toHash = knownHash.getKey(); String expectedHash = knownHash.getValue(); @@ -67,10 +57,9 @@ public void test_smokeTest() throws IOException, DigestException { InputStream toHashInputStream = new ByteArrayInputStream(bytesToHash); ByteStreams.copy(toHashInputStream, countingDigestOutputStream); - BlobDescriptor expectedBlobDescriptor = - new BlobDescriptor(bytesToHash.length, DescriptorDigest.fromHash(expectedHash)); - Assert.assertEquals(expectedBlobDescriptor, countingDigestOutputStream.toBlobDescriptor()); - Assert.assertEquals(bytesToHash.length, countingDigestOutputStream.getTotalBytes()); + BlobDescriptor blobDescriptor = countingDigestOutputStream.computeDigest(); + Assert.assertEquals(DescriptorDigest.fromHash(expectedHash), blobDescriptor.getDigest()); + Assert.assertEquals(bytesToHash.length, blobDescriptor.getSize()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java deleted file mode 100644 index e6c5045d5d..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.cloud.tools.jib.blob.Blobs; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link Connection}. */ -@RunWith(MockitoJUnitRunner.class) -public class ConnectionTest { - - @Mock private HttpRequestFactory mockHttpRequestFactory; - @Mock private HttpRequest mockHttpRequest; - @Mock private HttpResponse mockHttpResponse; - - private final ArgumentCaptor httpHeadersArgumentCaptor = - ArgumentCaptor.forClass(HttpHeaders.class); - private final ArgumentCaptor blobHttpContentArgumentCaptor = - ArgumentCaptor.forClass(BlobHttpContent.class); - - private final GenericUrl fakeUrl = new GenericUrl("http://crepecake/fake/url"); - private Request fakeRequest; - - @InjectMocks private final Connection testConnection = new Connection(fakeUrl.toURL()); - - @Before - public void setUpMocksAndFakes() throws IOException { - fakeRequest = - Request.builder() - .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) - .setUserAgent("fake user agent") - .setBody(new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type")) - .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) - .build(); - - Mockito.when( - mockHttpRequestFactory.buildRequest( - Mockito.any(String.class), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) - .thenReturn(mockHttpRequest); - - Mockito.when(mockHttpRequest.setHeaders(Mockito.any(HttpHeaders.class))) - .thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); - } - - @Test - public void testGet() throws IOException { - testSend(HttpMethods.GET, Connection::get); - } - - @Test - public void testPost() throws IOException { - testSend(HttpMethods.POST, Connection::post); - } - - @Test - public void testPut() throws IOException { - testSend(HttpMethods.PUT, Connection::put); - } - - @FunctionalInterface - private interface SendFunction { - - Response send(Connection connection, Request request) throws IOException; - } - - private void testSend(String httpMethod, SendFunction sendFunction) throws IOException { - try (Connection connection = testConnection) { - sendFunction.send(connection, fakeRequest); - } - - Mockito.verify(mockHttpRequest).setHeaders(httpHeadersArgumentCaptor.capture()); - Mockito.verify(mockHttpResponse).disconnect(); - - Assert.assertEquals( - "fake.accept,another.fake.accept", httpHeadersArgumentCaptor.getValue().getAccept()); - Assert.assertEquals("fake user agent", httpHeadersArgumentCaptor.getValue().getUserAgent()); - // Base64 representation of "fake-username:fake-secret" - Assert.assertEquals( - "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", - httpHeadersArgumentCaptor.getValue().getAuthorization()); - - Mockito.verify(mockHttpRequestFactory) - .buildRequest( - Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentArgumentCaptor.capture()); - Assert.assertEquals("fake.content.type", blobHttpContentArgumentCaptor.getValue().getType()); - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - blobHttpContentArgumentCaptor.getValue().writeTo(byteArrayOutputStream); - - Assert.assertEquals( - "crepecake", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java new file mode 100644 index 0000000000..b46267d96d --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -0,0 +1,536 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.blob.Blobs; +import com.sun.net.httpserver.HttpServer; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link FailoverHttpClient}. */ +@RunWith(MockitoJUnitRunner.class) +public class FailoverHttpClientTest { + + @FunctionalInterface + private interface CallFunction { + + Response call(FailoverHttpClient httpClient, URL url, Request request) throws IOException; + } + + @Mock private HttpTransport mockHttpTransport; + @Mock private HttpTransport mockInsecureHttpTransport; + @Mock private HttpRequestFactory mockHttpRequestFactory; + @Mock private HttpRequestFactory mockInsecureHttpRequestFactory; + @Mock private HttpRequest mockHttpRequest; + @Mock private HttpRequest mockInsecureHttpRequest; + @Mock private HttpResponse mockHttpResponse; + @Mock private Consumer logger; + + @Captor private ArgumentCaptor httpHeadersCaptor; + @Captor private ArgumentCaptor blobHttpContentCaptor; + @Captor private ArgumentCaptor urlCaptor; + + private final GenericUrl fakeUrl = new GenericUrl("https://crepecake/fake/url"); + private final LongAdder totalByteCount = new LongAdder(); + + @Before + public void setUp() throws IOException { + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[] {'b', 'o', 'd', 'y'}); + Mockito.when(mockHttpResponse.getContent()).thenReturn(inStream); + } + + @Test + public void testGet() throws IOException { + verifyCall(HttpMethods.GET, FailoverHttpClient::get); + } + + @Test + public void testPost() throws IOException { + verifyCall(HttpMethods.POST, FailoverHttpClient::post); + } + + @Test + public void testPut() throws IOException { + verifyCall(HttpMethods.PUT, FailoverHttpClient::put); + } + + @Test + public void testHttpTimeout_doNotSetByDefault() throws IOException { + try (Response ignored = newHttpClient(false, false).get(fakeUrl.toURL(), fakeRequest(null))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest, Mockito.never()).setConnectTimeout(Mockito.anyInt()); + Mockito.verify(mockHttpRequest, Mockito.never()).setReadTimeout(Mockito.anyInt()); + } + + @Test + public void testHttpTimeout() throws IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + try (Response ignored = httpClient.get(fakeUrl.toURL(), fakeRequest(5982))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest).setConnectTimeout(5982); + Mockito.verify(mockHttpRequest).setReadTimeout(5982); + } + + @Test + public void testGet_nonHttpsServer_insecureConnectionAndFailoverDisabled() + throws MalformedURLException, IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + try (Response response = httpClient.get(new URL("http://plain.http"), fakeRequest(null))) { + Assert.fail("Should disallow non-HTTP attempt"); + } catch (SSLException ex) { + Assert.assertEquals( + "insecure HTTP connection not allowed: http://plain.http", ex.getMessage()); + } + } + + @Test + public void testCall_secureClientOnUnverifiableServer() throws IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("unverified")); + + try (Response response = httpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Secure caller should fail if cannot verify server"); + } catch (SSLException ex) { + Assert.assertEquals("unverified", ex.getMessage()); + Mockito.verifyNoInteractions(logger); + } + } + + @Test + public void testGet_insecureClientOnUnverifiableServer() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("")); + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + verifyCapturedUrls("https://insecure", "https://insecure"); + verifyWarnings( + "Cannot verify server at https://insecure. Attempting again with no TLS verification."); + } + + @Test + public void testGet_insecureClientOnHttpServer() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) // server is not HTTPS + .thenReturn(mockHttpResponse); + Mockito.when(mockInsecureHttpRequest.execute()) + .thenThrow(new SSLException("")); // server is not HTTPS + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); + + verifyCapturedUrls("https://insecure", "https://insecure", "http://insecure"); + verifyWarnings( + "Cannot verify server at https://insecure. Attempting again with no TLS verification.", + "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testGet_insecureClientOnHttpServerAndNoPortSpecified() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) // server is not listening on 443 + .thenReturn(mockHttpResponse); // respond when connected through 80 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest); + + verifyCapturedUrls("https://insecure", "http://insecure"); + verifyWarnings("Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException("my exception")); // server not listening on 443 + + try (Response response = httpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if secure client"); + } catch (ConnectException ex) { + Assert.assertEquals("my exception", ex.getMessage()); + + verifyCapturedUrls("https://insecure"); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_insecureClientOnNonListeningServerAndPortSpecified() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException("my exception")); // server is not listening on 5000 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure:5000"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); + } catch (ConnectException ex) { + Assert.assertEquals("my exception", ex.getMessage()); + + verifyCapturedUrls("https://insecure:5000"); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_timeoutFromConnectException() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new ConnectException("Connection timed out")); + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if timed out even for ConnectionException"); + } catch (ConnectException ex) { + Assert.assertEquals("Connection timed out", ex.getMessage()); + + verifyCapturedUrls("https://insecure"); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_doNotSendCredentialsOverHttp() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + // make it fall back to HTTP + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) // server is not listening on 443 + .thenReturn(mockHttpResponse); // respond when connected through 80 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + // intentionally empty + } + + verifyCapturedUrls("https://insecure", "http://insecure"); + + Assert.assertEquals(2, httpHeadersCaptor.getAllValues().size()); + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getAllValues().get(0).getAuthorization()); + Assert.assertNull(httpHeadersCaptor.getAllValues().get(1).getAuthorization()); + } + + @Test + public void testGet_sendCredentialsOverHttp() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, true); // sendCredentialsOverHttp + + try (Response response = + insecureHttpClient.get(new URL("http://plain.http"), fakeRequest(null))) { + // intentionally empty + } + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getValue().getAuthorization()); + } + + @Test + public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IOException { + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); + + Request request = fakeRequest(null); + try (Response response = insecureHttpClient.get(new URL("http://plain.http"), request)) { + // intentionally empty + } + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + Assert.assertEquals(1, httpHeadersCaptor.getAllValues().size()); + + Assert.assertNull(httpHeadersCaptor.getValue().getAuthorization()); + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", request.getHeaders().getAuthorization()); + } + + @Test + public void testShutDown() throws IOException { + FailoverHttpClient secureHttpClient = newHttpClient(false, false); + + try (Response response = secureHttpClient.get(fakeUrl.toURL(), fakeRequest(null))) { + secureHttpClient.shutDown(); + secureHttpClient.shutDown(); + Mockito.verify(mockHttpTransport, Mockito.times(1)).shutdown(); + Mockito.verify(mockHttpResponse, Mockito.times(1)).disconnect(); + } + } + + @Test + public void testFollowFailoverHistory_insecureHttps() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); + verifyCapturedUrls("https://url", "https://url", "https://url"); + verifyWarnings( + "Cannot verify server at https://url. Attempting again with no TLS verification."); + } + + @Test + public void testFollowFailoverHistory_httpFailoverByConnectionError() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) + .thenReturn(mockHttpResponse); + + try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); + verifyCapturedUrls("https://url", "http://url", "http://url"); + verifyWarnings("Failed to connect to https://url over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testFollowFailoverHistory_httpFailover() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + Mockito.when(mockInsecureHttpRequest.execute()).thenThrow(new SSLException("")); + + try (Response response1 = httpClient.get(new URL("https://url:123"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url:123"), fakeRequest(null))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); + verifyCapturedUrls("https://url:123", "https://url:123", "http://url:123", "http://url:123"); + verifyWarnings( + "Cannot verify server at https://url:123. Attempting again with no TLS verification.", + "Failed to connect to https://url:123 over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testFollowFailoverHistory_portsDifferent() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + + try (Response response1 = httpClient.get(new URL("https://url:1"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url:2"), fakeRequest(null))) { + // intentionally empty + } + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); + verifyCapturedUrls("https://url:1", "https://url:1", "https://url:2", "https://url:2"); + verifyWarnings( + "Cannot verify server at https://url:1. Attempting again with no TLS verification.", + "Cannot verify server at https://url:2. Attempting again with no TLS verification."); + } + + @Test + public void testRetries() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 1); + AtomicBoolean failed = new AtomicBoolean(); + server + .createContext("/") + .setHandler( + exchange -> + exchange.sendResponseHeaders(failed.compareAndSet(false, true) ? 123 : 200, -1)); + try { + server.start(); + int port = server.getAddress().getPort(); + List events = new ArrayList<>(); + int returnCode = + new FailoverHttpClient(true, true, events::add) + .get(new URL("http://localhost:" + port), Request.builder().build()) + .getStatusCode(); + assertThat(returnCode).isEqualTo(200); + assertThat(failed.get()).isTrue(); + assertThat(events) + .containsExactly( + LogEvent.warn("GET http://localhost:" + port + " failed and will be retried")); + } finally { + server.stop(0); + } + } + + private void setUpMocks( + HttpTransport mockHttpTransport, + HttpRequestFactory mockHttpRequestFactory, + HttpRequest mockHttpRequest) + throws IOException { + Mockito.when(mockHttpTransport.createRequestFactory()).thenReturn(mockHttpRequestFactory); + Mockito.when( + mockHttpRequestFactory.buildRequest(Mockito.any(), urlCaptor.capture(), Mockito.any())) + .thenReturn(mockHttpRequest); + + Mockito.when(mockHttpRequest.setIOExceptionHandler(Mockito.any())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setUseRawRedirectUrls(Mockito.anyBoolean())) + .thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setHeaders(httpHeadersCaptor.capture())) + .thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); + } + + private FailoverHttpClient newHttpClient(boolean insecure, boolean authOverHttp) + throws IOException { + setUpMocks(mockHttpTransport, mockHttpRequestFactory, mockHttpRequest); + if (insecure) { + setUpMocks( + mockInsecureHttpTransport, mockInsecureHttpRequestFactory, mockInsecureHttpRequest); + } + return new FailoverHttpClient( + insecure, + authOverHttp, + logger, + () -> mockHttpTransport, + () -> mockInsecureHttpTransport, + true); + } + + private Request fakeRequest(Integer httpTimeout) { + return Request.builder() + .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) + .setUserAgent("fake user agent") + .setBody( + new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) + .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) + .setHttpTimeout(httpTimeout) + .build(); + } + + private void verifyCall(String httpMethod, CallFunction callFunction) throws IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) { + // intentionally empty + } + + Assert.assertEquals( + "fake.accept,another.fake.accept", httpHeadersCaptor.getValue().getAccept()); + Assert.assertEquals("fake user agent", httpHeadersCaptor.getValue().getUserAgent()); + // Base64 representation of "fake-username:fake-secret" + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getValue().getAuthorization()); + + Mockito.verify(mockHttpRequestFactory) + .buildRequest(Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentCaptor.capture()); + Assert.assertEquals("fake.content.type", blobHttpContentCaptor.getValue().getType()); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + blobHttpContentCaptor.getValue().writeTo(byteArrayOutputStream); + + Assert.assertEquals("crepecake", byteArrayOutputStream.toString(StandardCharsets.UTF_8.name())); + Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); + } + + private void verifyWarnings(String... logs) { + for (String log : logs) { + Mockito.verify(logger, Mockito.times(1)).accept(LogEvent.warn(log)); + } + Mockito.verifyNoMoreInteractions(logger); + } + + private void verifyCapturedUrls(String... urls) { + List captured = + urlCaptor.getAllValues().stream().map(GenericUrl::toString).collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList(urls), captured); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java new file mode 100644 index 0000000000..facf1cdd76 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link NotifyingOutputStream}. */ +public class NotifyingOutputStreamTest { + + @Test + public void testCallback_correctSequence() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + List byteCounts = new ArrayList<>(); + + try (NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(byteArrayOutputStream, byteCounts::add)) { + notifyingOutputStream.write(0); + notifyingOutputStream.write(new byte[] {1, 2, 3}); + notifyingOutputStream.write(new byte[] {1, 2, 3, 4, 5}, 3, 2); + } + + Assert.assertEquals(Arrays.asList(1L, 3L, 2L), byteCounts); + Assert.assertArrayEquals(new byte[] {0, 1, 2, 3, 4, 5}, byteArrayOutputStream.toByteArray()); + } + + @Test + public void testDelay() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + List byteCounts = new ArrayList<>(); + + Queue instantQueue = new ArrayDeque<>(); + instantQueue.add(Instant.EPOCH); + + try (ThrottledAccumulatingConsumer byteCounter = + new ThrottledAccumulatingConsumer( + byteCounts::add, Duration.ofSeconds(3), instantQueue::remove); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(byteArrayOutputStream, byteCounter)) { + instantQueue.add(Instant.EPOCH); + notifyingOutputStream.write(100); + instantQueue.add(Instant.EPOCH); + notifyingOutputStream.write(new byte[] {101, 102, 103}); + instantQueue.add(Instant.EPOCH.plusSeconds(4)); + notifyingOutputStream.write(new byte[] {104, 105, 106}); + + instantQueue.add(Instant.EPOCH.plusSeconds(10)); + notifyingOutputStream.write(new byte[] {107, 108}); + + instantQueue.add(Instant.EPOCH.plusSeconds(10)); + notifyingOutputStream.write(new byte[] {109}); + instantQueue.add(Instant.EPOCH.plusSeconds(13)); + notifyingOutputStream.write(new byte[] {0, 110}, 1, 1); + } + + Assert.assertEquals(Arrays.asList(7L, 2L, 2L), byteCounts); + Assert.assertArrayEquals( + new byte[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110}, + byteArrayOutputStream.toByteArray()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java similarity index 53% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java index 750d918ca8..012ee51df6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,20 +14,25 @@ * the License. */ -package com.google.cloud.tools.jib.cache; +package com.google.cloud.tools.jib.http; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; -/** Tests for {@link CachedLayerWithMetadata}. */ -public class CachedLayerWithMetadataTest { +/** Tests for {@link Request}. */ +public class RequestTest { @Test - public void testNew() { - LayerMetadata mockLayerMetadata = Mockito.mock(LayerMetadata.class); - CachedLayerWithMetadata cachedLayerWithMetadata = - new CachedLayerWithMetadata(Mockito.mock(CachedLayer.class), mockLayerMetadata); - Assert.assertEquals(mockLayerMetadata, cachedLayerWithMetadata.getMetadata()); + public void testGetHttpTimeout() { + Request request = Request.builder().build(); + + Assert.assertNull(request.getHttpTimeout()); + } + + @Test + public void testSetHttpTimeout() { + Request request = Request.builder().setHttpTimeout(3000).build(); + + Assert.assertEquals(Integer.valueOf(3000), request.getHttpTimeout()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java new file mode 100644 index 0000000000..3d03d62aff --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +/** Helper to expose package-private methods. */ +public class RequestWrapper { + + private final Request request; + + public RequestWrapper(Request request) { + this.request = request; + } + + public int getHttpTimeout() { + return request.getHttpTimeout(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java index 6a0bb53845..dd27e0bd7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,9 +17,8 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.HttpResponse; -import com.google.cloud.tools.jib.blob.Blob; +import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.junit.Assert; @@ -37,19 +36,13 @@ public class ResponseTest { @Test public void testGetContent() throws IOException { - String expectedResponse = "crepecake\nis\ngood!"; - ByteArrayInputStream responseInputStream = - new ByteArrayInputStream(expectedResponse.getBytes(StandardCharsets.UTF_8)); + byte[] expectedResponse = "crepecake\nis\ngood!".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream responseInputStream = new ByteArrayInputStream(expectedResponse); Mockito.when(httpResponseMock.getContent()).thenReturn(responseInputStream); - Response response = new Response(httpResponseMock); - Blob responseStream = response.getBody(); - - ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream(); - responseStream.writeTo(responseOutputStream); - - Assert.assertEquals( - expectedResponse, new String(responseOutputStream.toByteArray(), StandardCharsets.UTF_8)); + try (Response response = new Response(httpResponseMock)) { + Assert.assertArrayEquals(expectedResponse, ByteStreams.toByteArray(response.getBody())); + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java new file mode 100644 index 0000000000..c5289dfe25 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -0,0 +1,189 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.common.io.Resources; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +/** Simple local web server for testing. */ +public class TestWebServer implements Closeable { + + private final boolean https; + private final int numThreads; + private final List responses; + private final boolean forgetServedResponses; + + private final ServerSocket serverSocket; + private final ExecutorService executorService; + private final Semaphore serverStarted = new Semaphore(1); + private final StringBuilder inputRead = new StringBuilder(); + + private int totalResponsesServed = 0; + private int globalResponseIndex = 0; + + public TestWebServer(boolean https) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"), 1); + } + + public TestWebServer(boolean https, int numThreads) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"), numThreads); + } + + public TestWebServer(boolean https, List responses, int numThreads) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, responses, numThreads, false); + } + + @SuppressWarnings("FutureReturnValueIgnored") + public TestWebServer( + boolean https, List responses, int numThreads, boolean forgetServedResponses) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this.https = https; + this.responses = responses; + this.numThreads = numThreads; + this.forgetServedResponses = forgetServedResponses; + serverSocket = https ? createHttpsServerSocket() : new ServerSocket(0); + executorService = Executors.newFixedThreadPool(numThreads + 1); + executorService.submit(this::listen); + serverStarted.acquire(); + } + + public int getLocalPort() { + return serverSocket.getLocalPort(); + } + + public String getEndpoint() { + return (https ? "https" : "http") + "://localhost:" + serverSocket.getLocalPort(); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + executorService.shutdown(); + } + + private ServerSocket createHttpsServerSocket() + throws IOException, GeneralSecurityException, URISyntaxException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + // generated with: keytool -genkey -keyalg RSA -keystore ./TestWebServer-keystore + Path keyStoreFile = Paths.get(Resources.getResource("core/TestWebServer-keystore").toURI()); + try (InputStream in = Files.newInputStream(keyStoreFile)) { + keyStore.load(in, "password".toCharArray()); + } + + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, "password".toCharArray()); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + return sslContext.getServerSocketFactory().createServerSocket(0); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private Void listen() throws IOException { + serverStarted.release(); + for (int i = 0; i < numThreads; i++) { + Socket socket = serverSocket.accept(); + executorService.submit(() -> serveResponses(socket)); + } + return null; + } + + private Void serveResponses(Socket socket) throws IOException { + try (Socket ignored = socket) { + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + int firstByte = in.read(); + int secondByte = in.read(); + if (!(firstByte == 'G' && secondByte == 'E') + && !(firstByte == 'P' && secondByte == 'O') + && !(firstByte == 'H' && secondByte == 'E')) { // GET, POST, HEAD, ... + out.write( + "HTTP/1.1 400 Bad Request\nContent-Length: 0\n\n".getBytes(StandardCharsets.UTF_8)); + return null; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + for (int i = 0; true; i++) { + for (String line = reader.readLine(); + line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. + line = reader.readLine()) { + synchronized (inputRead) { + if (firstByte != -1) { + inputRead.append((char) firstByte).append((char) secondByte); + firstByte = -1; + } + inputRead.append(line).append('\n'); + } + } + String response = getNextResponse(i); + if (response == null) { + return null; + } + out.write(response.getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + } + } + + private synchronized String getNextResponse(int index) { + if (index >= responses.size() || globalResponseIndex >= responses.size()) { + return null; + } + totalResponsesServed++; + return forgetServedResponses ? responses.get(globalResponseIndex++) : responses.get(index); + } + + /** + * Returns input read. Note if there were concurrent connections, input lines from different + * connections can be intermixed. However, no lines will ever be broken in the middle. + */ + public String getInputRead() { + synchronized (inputRead) { + return inputRead.toString(); + } + } + + public synchronized int getTotalResponsesServed() { + return totalResponsesServed; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java new file mode 100644 index 0000000000..05d9b00058 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import javax.net.ssl.SSLException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link FailoverHttpClient} using an actual local server. */ +@RunWith(MockitoJUnitRunner.class) +public class WithServerFailoverHttpClientTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + + @Mock private Consumer logger; + + private final Request request = new Request.Builder().build(); + + @Test + public void testGet() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false); + Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { + + Assert.assertEquals(200, response.getStatusCode()); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + Mockito.verifyNoInteractions(logger); + } + } + + @Test + public void testSecureConnectionOnInsecureHttpsServer() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient secureHttpClient = + new FailoverHttpClient(false /*secure*/, false, logger, false); + try (TestWebServer server = new TestWebServer(true); + Response ignored = secureHttpClient.get(new URL(server.getEndpoint()), request)) { + Assert.fail("Should fail if cannot verify peer"); + + } catch (SSLException ex) { + Assert.assertNotNull(ex.getMessage()); + } + } + + @Test + public void testInsecureConnection_insecureHttpsFailover() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(true, 2); + Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { + + Assert.assertEquals(200, response.getStatusCode()); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + + String endpoint = server.getEndpoint(); + String expectedLog = + "Cannot verify server at " + endpoint + ". Attempting again with no TLS verification."; + Mockito.verify(logger).accept(LogEvent.warn(expectedLog)); + } + } + + @Test + public void testInsecureConnection_plainHttpFailover() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, 3)) { + String httpsUrl = server.getEndpoint().replace("http://", "https://"); + try (Response response = insecureHttpClient.get(new URL(httpsUrl), request)) { + + Assert.assertEquals(200, response.getStatusCode()); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + + String expectedLog1 = + "Cannot verify server at " + httpsUrl + ". Attempting again with no TLS verification."; + String expectedLog2 = + "Failed to connect to " + httpsUrl + " over HTTPS. Attempting again with HTTP."; + Mockito.verify(logger).accept(LogEvent.warn(expectedLog1)); + Mockito.verify(logger).accept(LogEvent.warn(expectedLog2)); + } + } + } + + @Test + public void testProxyCredentialProperties() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + String proxyResponse = + "HTTP/1.1 407 Proxy Authentication Required\n" + + "Proxy-Authenticate: BASIC realm=\"some-realm\"\n" + + "Cache-Control: no-cache\n" + + "Pragma: no-cache\n" + + "Content-Length: 0\n\n"; + String targetServerResponse = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = + new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse), 1)) { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", String.valueOf(server.getLocalPort())); + System.setProperty("http.proxyUser", "user_sys_prop"); + System.setProperty("http.proxyPassword", "pass_sys_prop"); + + try (Response response = httpClient.get(new URL("http://does.not.matter"), request)) { + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "Proxy-Authorization: Basic dXNlcl9zeXNfcHJvcDpwYXNzX3N5c19wcm9w")); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + } + } + } + + @Test + public void testClosingResourcesMultipleTimes_noErrors() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, 2); + Response ignored1 = httpClient.get(new URL(server.getEndpoint()), request); + Response ignored2 = httpClient.get(new URL(server.getEndpoint()), request)) { + ignored1.close(); + ignored2.close(); + } finally { + + // Validate that calling shutdown() many times completes with no errors + assertThat(httpClient.getTransportsCreated()).hasSize(2); + httpClient.shutDown(); + httpClient.shutDown(); + assertThat(httpClient.getTransportsCreated()).hasSize(0); + } + } + + @Test + public void testRedirectionUrls() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + // Sample query strings from + // https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104 + String url1 = "?id=301&_auth_=exp=1572285389~hmac=f0a387f0"; + String url2 = "?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + String url3 = "?id=303&_auth_=exp=1572285389~hmac=f0a387f0"; + String url4 = "?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + String url5 = "?id=308&_auth_=exp=1572285389~hmac=f0a387f0"; + + String redirect301 = + "HTTP/1.1 301 Moved Permanently\nLocation: " + url1 + "\nContent-Length: 0\n\n"; + String redirect302 = "HTTP/1.1 302 Found\nLocation: " + url2 + "\nContent-Length: 0\n\n"; + String redirect303 = "HTTP/1.1 303 See Other\nLocation: " + url3 + "\nContent-Length: 0\n\n"; + String redirect307 = + "HTTP/1.1 307 Temporary Redirect\nLocation: " + url4 + "\nContent-Length: 0\n\n"; + String redirect308 = + "HTTP/1.1 308 Permanent Redirect\nLocation: " + url5 + "\nContent-Length: 0\n\n"; + String ok200 = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + List responses = + Arrays.asList(redirect301, redirect302, redirect303, redirect307, redirect308, ok200); + + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, responses, 1)) { + httpClient.get(new URL(server.getEndpoint()), request); + + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=301&_auth_=exp=1572285389~hmac=f0a387f0 ")); + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=303&_auth_=exp=1572285389~hmac=f0a387f0 ")); + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + MatcherAssert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=308&_auth_=exp=1572285389~hmac=f0a387f0 ")); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java deleted file mode 100644 index 94fac9b447..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2017 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; -import java.util.Arrays; -import java.util.List; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link ImageLayers}. */ -@RunWith(MockitoJUnitRunner.class) -public class ImageLayersTest { - - @Mock private CachedLayer mockCachedLayer; - @Mock private ReferenceLayer mockReferenceLayer; - @Mock private DigestOnlyLayer mockDigestOnlyLayer; - @Mock private UnwrittenLayer mockUnwrittenLayer; - - @Before - public void setUpFakes() throws LayerPropertyNotFoundException { - DescriptorDigest mockDescriptorDigest1 = Mockito.mock(DescriptorDigest.class); - DescriptorDigest mockDescriptorDigest2 = Mockito.mock(DescriptorDigest.class); - DescriptorDigest mockDescriptorDigest3 = Mockito.mock(DescriptorDigest.class); - - BlobDescriptor cachedLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); - BlobDescriptor referenceLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest2); - BlobDescriptor referenceNoDiffIdLayerBlobDescriptor = - new BlobDescriptor(0, mockDescriptorDigest3); - // Intentionally the same digest as the mockCachedLayer. - BlobDescriptor unwrittenLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); - - Mockito.when(mockCachedLayer.getBlobDescriptor()).thenReturn(cachedLayerBlobDescriptor); - Mockito.when(mockReferenceLayer.getBlobDescriptor()).thenReturn(referenceLayerBlobDescriptor); - Mockito.when(mockDigestOnlyLayer.getBlobDescriptor()) - .thenReturn(referenceNoDiffIdLayerBlobDescriptor); - Mockito.when(mockUnwrittenLayer.getBlobDescriptor()).thenReturn(unwrittenLayerBlobDescriptor); - } - - @Test - public void testAddLayer_success() throws LayerPropertyNotFoundException { - List expectedLayers = - Arrays.asList(mockCachedLayer, mockReferenceLayer, mockDigestOnlyLayer); - - ImageLayers imageLayers = - ImageLayers.builder() - .add(mockCachedLayer) - .add(mockReferenceLayer) - .add(mockDigestOnlyLayer) - .build(); - - Assert.assertThat(imageLayers.getLayers(), CoreMatchers.is(expectedLayers)); - } - - @Test - public void testAddLayer_sameAsLastLayer() throws LayerPropertyNotFoundException { - List expectedLayers = - Arrays.asList(mockCachedLayer, mockReferenceLayer, mockDigestOnlyLayer, mockUnwrittenLayer); - - ImageLayers imageLayers = - ImageLayers.builder() - .add(mockCachedLayer) - .add(mockReferenceLayer) - .add(mockDigestOnlyLayer) - .add(mockUnwrittenLayer) - .add(mockCachedLayer) - .build(); - - Assert.assertEquals(expectedLayers, imageLayers.getLayers()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java new file mode 100644 index 0000000000..cf7d1f72ba --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ImageTarball}. */ +@RunWith(MockitoJUnitRunner.class) +public class ImageTarballTest { + + private Path fileA; + private Path fileB; + private DescriptorDigest fakeDigestA; + private DescriptorDigest fakeDigestB; + + @Mock private Layer mockLayer1; + @Mock private Layer mockLayer2; + + @Before + public void setup() throws URISyntaxException, IOException, DigestException { + fileA = Paths.get(Resources.getResource("core/fileA").toURI()); + fileB = Paths.get(Resources.getResource("core/fileB").toURI()); + long fileASize = Files.size(fileA); + long fileBSize = Files.size(fileB); + + fakeDigestA = + DescriptorDigest.fromHash( + "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); + fakeDigestB = + DescriptorDigest.fromHash( + "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc6"); + + Mockito.when(mockLayer1.getBlob()).thenReturn(Blobs.from(fileA)); + Mockito.when(mockLayer1.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(fileASize, fakeDigestA)); + Mockito.when(mockLayer1.getDiffId()).thenReturn(fakeDigestA); + Mockito.when(mockLayer2.getBlob()).thenReturn(Blobs.from(fileB)); + Mockito.when(mockLayer2.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(fileBSize, fakeDigestB)); + Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); + } + + @Test + public void testWriteTo_docker() + throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException { + Image testImage = + Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); + ImageTarball imageTarball = + new ImageTarball( + testImage, + ImageReference.parse("my/image:tag"), + ImmutableSet.of("tag", "another-tag", "tag3")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + imageTarball.writeTo(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { + + // Verifies layer with fileA was added. + TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals(fakeDigestA.getHash() + ".tar.gz", headerFileALayer.getName()); + String fileAString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileA)), fileAString); + + // Verifies layer with fileB was added. + TarArchiveEntry headerFileBLayer = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals(fakeDigestB.getHash() + ".tar.gz", headerFileBLayer.getName()); + String fileBString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileB)), fileBString); + + // Verifies container configuration was added. + TarArchiveEntry headerContainerConfiguration = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("config.json", headerContainerConfiguration.getName()); + String containerConfigJson = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + JsonTemplateMapper.readJson(containerConfigJson, ContainerConfigurationTemplate.class); + + // Verifies manifest was added. + TarArchiveEntry headerManifest = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("manifest.json", headerManifest.getName()); + String manifestJson = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + DockerManifestEntryTemplate manifest = + JsonTemplateMapper.readListOfJson(manifestJson, DockerManifestEntryTemplate.class).get(0); + Assert.assertEquals( + ImmutableList.of("my/image:tag", "my/image:another-tag", "my/image:tag3"), + manifest.getRepoTags()); + } + } + + @Test + public void testWriteTo_oci() + throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException { + Image testImage = + Image.builder(OciManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); + ImageTarball imageTarball = + new ImageTarball( + testImage, + ImageReference.parse("my/image:tag"), + ImmutableSet.of("tag", "another-tag", "tag3")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + imageTarball.writeTo(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { + + // Verifies layer with fileA was added. + TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("blobs/sha256/" + fakeDigestA.getHash(), headerFileALayer.getName()); + String fileAString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileA)), fileAString); + + // Verifies layer with fileB was added. + TarArchiveEntry headerFileBLayer = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("blobs/sha256/" + fakeDigestB.getHash(), headerFileBLayer.getName()); + String fileBString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileB)), fileBString); + + // Verifies container configuration was added. + TarArchiveEntry headerContainerConfiguration = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals( + "blobs/sha256/011212cff4d5d6b18c7d3a00a7a2701514a1fdd3ec0d250a03756f84f3d955d4", + headerContainerConfiguration.getName()); + JsonTemplateMapper.readJson(tarArchiveInputStream, ContainerConfigurationTemplate.class); + + // Verifies manifest was added. + TarArchiveEntry headerManifest = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals( + "blobs/sha256/1543d061159a8d6877087938bfd62681cdeff873e1fa3e1fcf12dec358c112a4", + headerManifest.getName()); + JsonTemplateMapper.readJson(tarArchiveInputStream, OciManifestTemplate.class); + + // Verifies oci-layout was added. + TarArchiveEntry headerOciLayout = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("oci-layout", headerOciLayout.getName()); + String ociLayoutJson = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals("{\"imageLayoutVersion\": \"1.0.0\"}", ociLayoutJson); + + // Verifies index.json was added. + TarArchiveEntry headerIndex = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("index.json", headerIndex.getName()); + OciIndexTemplate index = + JsonTemplateMapper.readJson(tarArchiveInputStream, OciIndexTemplate.class); + BuildableManifestTemplate.ContentDescriptorTemplate indexManifest = + index.getManifests().get(0); + Assert.assertEquals( + "1543d061159a8d6877087938bfd62681cdeff873e1fa3e1fcf12dec358c112a4", + indexManifest.getDigest().getHash()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index e97e69742a..b2c8e96728 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,11 +16,16 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.time.Instant; import java.util.Arrays; +import java.util.Collections; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -44,27 +49,50 @@ public void setUp() throws LayerPropertyNotFoundException { @Test public void test_smokeTest() throws LayerPropertyNotFoundException { - ImmutableList expectedEnvironment = - ImmutableList.of("crepecake=is great", "VARIABLE=VALUE"); - - Image image = - Image.builder() - .setEnvironmentVariable("crepecake", "is great") - .setEnvironmentVariable("VARIABLE", "VALUE") + Image image = + Image.builder(V22ManifestTemplate.class) + .setCreated(Instant.ofEpochSecond(10000)) + .addEnvironmentVariable("crepecake", "is great") + .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) - .setJavaArguments(Arrays.asList("arg1", "arg2")) - .setExposedPorts( - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP))) + .setProgramArguments(Arrays.asList("arg1", "arg2")) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/a/path"), AbsoluteUnixPath.get("/another/path"))) + .setUser("john") .addLayer(mockLayer) .build(); + Assert.assertEquals(V22ManifestTemplate.class, image.getImageFormat()); Assert.assertEquals( mockDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest()); - Assert.assertEquals(expectedEnvironment, image.getEnvironment()); + Assert.assertEquals(Instant.ofEpochSecond(10000), image.getCreated()); + Assert.assertEquals( + ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); - Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments()); + Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getProgramArguments()); + Assert.assertEquals(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); Assert.assertEquals( - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP)), - image.getExposedPorts()); + ImmutableSet.of(AbsoluteUnixPath.get("/a/path"), AbsoluteUnixPath.get("/another/path")), + image.getVolumes()); + Assert.assertEquals("john", image.getUser()); + } + + @Test + public void testDefaults() { + Image image = Image.builder(V22ManifestTemplate.class).build(); + Assert.assertEquals("amd64", image.getArchitecture()); + Assert.assertEquals("linux", image.getOs()); + Assert.assertEquals(Collections.emptyList(), image.getLayers()); + Assert.assertEquals(Collections.emptyList(), image.getHistory()); + } + + @Test + public void testOsArch() { + Image image = + Image.builder(V22ManifestTemplate.class).setArchitecture("wasm").setOs("js").build(); + Assert.assertEquals("wasm", image.getArchitecture()); + Assert.assertEquals("js", image.getOs()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java index 5dc765c49d..5d062e08f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import org.junit.Assert; import org.junit.Test; @@ -28,32 +28,10 @@ @RunWith(MockitoJUnitRunner.class) public class LayerTest { - @Mock private Blob mockUncompressedBlob; @Mock private DescriptorDigest mockDescriptorDigest; @Mock private BlobDescriptor mockBlobDescriptor; @Mock private DescriptorDigest mockDiffId; - @Test - public void testNew_unwritten() throws LayerPropertyNotFoundException { - Layer layer = new UnwrittenLayer(mockUncompressedBlob); - - Assert.assertEquals(mockUncompressedBlob, layer.getBlob()); - - try { - layer.getBlobDescriptor(); - Assert.fail("Blob descriptor should not be available for unwritten layer"); - } catch (LayerPropertyNotFoundException ex) { - Assert.assertEquals("Blob descriptor not available for unwritten layer", ex.getMessage()); - } - - try { - layer.getDiffId(); - Assert.fail("Diff ID should not be available for unwritten layer"); - } catch (LayerPropertyNotFoundException ex) { - Assert.assertEquals("Diff ID not available for unwritten layer", ex.getMessage()); - } - } - @Test public void testNew_reference() throws LayerPropertyNotFoundException { Layer layer = new ReferenceLayer(mockBlobDescriptor, mockDiffId); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 3d433ce5ac..eca1194405 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,15 +16,19 @@ package com.google.cloud.tools.jib.image; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import java.io.BufferedOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -33,20 +37,14 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; -import java.util.Arrays; -import java.util.Collections; +import java.time.Instant; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link ReproducibleLayerBuilder}. */ -@RunWith(MockitoJUnitRunner.class) public class ReproducibleLayerBuilderTest { /** @@ -61,71 +59,89 @@ public class ReproducibleLayerBuilderTest { private static void verifyNextTarArchiveEntry( TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath, Path expectedFile) throws IOException { - TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals(expectedExtractionPath, header.getName()); + TarArchiveEntry header = tarArchiveInputStream.getNextEntry(); + assertThat(header.getName()).isEqualTo(expectedExtractionPath); - String expectedString = new String(Files.readAllBytes(expectedFile), StandardCharsets.UTF_8); + byte[] expectedBytes = Files.readAllBytes(expectedFile); + byte[] extractedBytes = ByteStreams.toByteArray(tarArchiveInputStream); + assertThat(extractedBytes).isEqualTo(expectedBytes); + } - String extractedString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + /** + * Verifies that the next {@link TarArchiveEntry} in the {@link TarArchiveInputStream} is a + * directory with correct permissions. + * + * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from + * @param expectedExtractionPath the expected extraction path of the next entry + * @throws IOException if an I/O exception occurs + */ + private static void verifyNextTarArchiveEntryIsDirectory( + TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath) + throws IOException { + TarArchiveEntry extractionPathEntry = tarArchiveInputStream.getNextEntry(); + assertThat(extractionPathEntry.getName()).isEqualTo(expectedExtractionPath); + assertThat(extractionPathEntry.isDirectory()).isTrue(); + assertThat(extractionPathEntry.getMode()).isEqualTo(TarArchiveEntry.DEFAULT_DIR_MODE); + } - Assert.assertEquals(expectedString, extractedString); + private static FileEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new FileEntry( + source, + destination, + FileEntriesLayer.DEFAULT_FILE_PERMISSIONS_PROVIDER.get(source, destination), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME); } - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testBuild() throws URISyntaxException, IOException { - Path layerDirectory = Paths.get(Resources.getResource("layer").toURI()); - Path blobA = Paths.get(Resources.getResource("blobA").toURI()); + Path layerDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + Path blobA = Paths.get(Resources.getResource("core/blobA").toURI()); ReproducibleLayerBuilder layerBuilder = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(layerDirectory, blobA), "extract/here/apple") - .addFiles(Collections.singletonList(blobA), "extract/here/banana"); + new ReproducibleLayerBuilder( + ImmutableList.copyOf( + FileEntriesLayer.builder() + .addEntryRecursive( + layerDirectory, AbsoluteUnixPath.get("/extract/here/apple/layer")) + .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/apple/blobA")) + .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/banana/blobA")) + .build() + .getEntries())); // Writes the layer tar to a temporary file. - UnwrittenLayer unwrittenLayer = layerBuilder.build(); + Blob unwrittenBlob = layerBuilder.build(); Path temporaryFile = temporaryFolder.newFile().toPath(); try (OutputStream temporaryFileOutputStream = new BufferedOutputStream(Files.newOutputStream(temporaryFile))) { - unwrittenLayer.getBlob().writeTo(temporaryFileOutputStream); + unwrittenBlob.writeTo(temporaryFileOutputStream); } // Reads the file back. try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(Files.newInputStream(temporaryFile))) { - // Verifies that blobA was added. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); - - // Verifies that all the files have been added to the tarball stream. - ImmutableList layerDirectoryPaths = - new DirectoryWalker(layerDirectory).filter(path -> !path.equals(layerDirectory)).walk(); - for (Path path : layerDirectoryPaths) { - TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); - - StringBuilder expectedExtractionPath = new StringBuilder("extract/here/apple"); - for (Path pathComponent : layerDirectory.getParent().relativize(path)) { - expectedExtractionPath.append("/").append(pathComponent); - } - // Check path-equality because there might be an appended backslash in the header - // filename. - Assert.assertEquals( - Paths.get(expectedExtractionPath.toString()), Paths.get(header.getName())); - - // If is a normal file, checks that the file contents match. - if (Files.isRegularFile(path)) { - String expectedFileString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - - String extractedFileString = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - - Assert.assertEquals(expectedFileString, extractedFileString); - } - } - - // Verifies that blobA was added to the other location. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/b/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/a/b/bar", + Paths.get(Resources.getResource("core/layer/a/b/bar").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/c/cat", + Paths.get(Resources.getResource("core/layer/c/cat").toURI())); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/foo", + Paths.get(Resources.getResource("core/layer/foo").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/banana/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/banana/blobA", blobA); } } @@ -136,9 +152,7 @@ public void testToBlob_reproducibility() throws IOException { Path root1 = Files.createDirectories(testRoot.resolve("files1")); Path root2 = Files.createDirectories(testRoot.resolve("files2")); - String extractionPath = "/somewhere"; - - // TODO: Currently this test only covers variation in order and modified time, even though + // TODO: Currently this test only covers variation in order and modification time, even though // TODO: the code is designed to clean up userid/groupid, this test does not check that yet. String contentA = "abcabc"; Path fileA1 = createFile(root1, "fileA", contentA, 10000); @@ -147,37 +161,342 @@ public void testToBlob_reproducibility() throws IOException { Path fileB1 = createFile(root1, "fileB", contentB, 10000); Path fileB2 = createFile(root2, "fileB", contentB, 20000); - // check if modified times are off - Assert.assertNotEquals(Files.getLastModifiedTime(fileA1), Files.getLastModifiedTime(fileA2)); - Assert.assertNotEquals(Files.getLastModifiedTime(fileB1), Files.getLastModifiedTime(fileB2)); + // check if modification times are off + assertThat(Files.getLastModifiedTime(fileA2)).isNotEqualTo(Files.getLastModifiedTime(fileA1)); + assertThat(Files.getLastModifiedTime(fileB2)).isNotEqualTo(Files.getLastModifiedTime(fileB1)); // create layers of exact same content but ordered differently and with different timestamps Blob layer = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(fileA1, fileB1), extractionPath) - .build() - .getBlob(); + new ReproducibleLayerBuilder( + ImmutableList.of( + defaultLayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), + defaultLayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) + .build(); Blob reproduced = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(fileB2, fileA2), extractionPath) - .build() - .getBlob(); + new ReproducibleLayerBuilder( + ImmutableList.of( + defaultLayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), + defaultLayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) + .build(); byte[] layerContent = Blobs.writeToByteArray(layer); byte[] reproducedLayerContent = Blobs.writeToByteArray(reproduced); - Assert.assertThat(layerContent, CoreMatchers.is(reproducedLayerContent)); + assertThat(layerContent).isEqualTo(reproducedLayerContent); } - private Path createFile(Path root, String filename, String content, long lastModifiedTime) - throws IOException { + @Test + public void testBuild_parentDirBehavior() throws IOException { + Path testRoot = temporaryFolder.getRoot().toPath(); + + // the path doesn't really matter on source files, but these are structured + Path parent = Files.createDirectories(testRoot.resolve("dirA")); + Path fileA = Files.createFile(parent.resolve("fileA")); + Path ignoredParent = Files.createDirectories(testRoot.resolve("dirB-ignored")); + Path fileB = Files.createFile(ignoredParent.resolve("fileB")); + Path fileC = + Files.createFile(Files.createDirectories(testRoot.resolve("dirC-absent")).resolve("fileC")); + + Blob layer = + new ReproducibleLayerBuilder( + ImmutableList.of( + new FileEntry( + parent, + AbsoluteUnixPath.get("/root/dirA"), + FilePermissions.fromOctalString("111"), + Instant.ofEpochSecond(10)), + new FileEntry( + fileA, + AbsoluteUnixPath.get("/root/dirA/fileA"), + FilePermissions.fromOctalString("222"), + Instant.ofEpochSecond(20)), + new FileEntry( + fileB, + AbsoluteUnixPath.get("/root/dirB-ignored/fileB"), + FilePermissions.fromOctalString("333"), + Instant.ofEpochSecond(30)), + new FileEntry( + ignoredParent, + AbsoluteUnixPath.get("/root/dirB-ignored"), + FilePermissions.fromOctalString("444"), + Instant.ofEpochSecond(40)), + new FileEntry( + fileC, + AbsoluteUnixPath.get("/root/dirC-absent/file3"), + FilePermissions.fromOctalString("555"), + Instant.ofEpochSecond(50)))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + layer.writeTo(out); + } + + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + // root (default folder permissions) + TarArchiveEntry root = in.getNextEntry(); + assertThat(root.getMode()).isEqualTo(040755); + assertThat(root.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(1)); + assertThat(root.getLongUserId()).isEqualTo(0); + assertThat(root.getLongGroupId()).isEqualTo(0); + assertThat(root.getUserName()).isEmpty(); + assertThat(root.getGroupName()).isEmpty(); + + // parentAAA (custom permissions, custom timestamp) + TarArchiveEntry rootParentA = in.getNextEntry(); + assertThat(rootParentA.getMode()).isEqualTo(040111); + assertThat(rootParentA.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(10)); + assertThat(rootParentA.getLongUserId()).isEqualTo(0); + assertThat(rootParentA.getLongGroupId()).isEqualTo(0); + assertThat(rootParentA.getUserName()).isEmpty(); + assertThat(rootParentA.getGroupName()).isEmpty(); + + // skip over fileA + in.getNextEntry(); + + // parentBBB (default permissions - ignored custom permissions, since fileB added first) + TarArchiveEntry rootParentB = in.getNextEntry(); + // TODO (#1650): we want 040444 here. + assertThat(rootParentB.getMode()).isEqualTo(040755); + // TODO (#1650): we want Instant.ofEpochSecond(40) here. + assertThat(rootParentB.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(1)); + assertThat(rootParentB.getLongUserId()).isEqualTo(0); + assertThat(rootParentB.getLongGroupId()).isEqualTo(0); + assertThat(rootParentB.getUserName()).isEmpty(); + assertThat(rootParentB.getGroupName()).isEmpty(); + + // skip over fileB + in.getNextEntry(); + + // parentCCC (default permissions - no entry provided) + TarArchiveEntry rootParentC = in.getNextEntry(); + assertThat(rootParentC.getMode()).isEqualTo(040755); + assertThat(rootParentC.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(1)); + assertThat(rootParentC.getLongUserId()).isEqualTo(0); + assertThat(rootParentC.getLongGroupId()).isEqualTo(0); + assertThat(rootParentC.getUserName()).isEmpty(); + assertThat(rootParentC.getGroupName()).isEmpty(); + + // we don't care about fileC + } + } + + @Test + public void testBuild_timestampDefault() throws IOException { + Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of(defaultLayerEntry(file, AbsoluteUnixPath.get("/fileA")))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + // Reads the file back. + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + assertThat(in.getNextEntry().getLastModifiedDate().toInstant()) + .isEqualTo(Instant.EPOCH.plusSeconds(1)); + } + } + + @Test + public void testBuild_timestampNonDefault() throws IOException { + Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of( + new FileEntry( + file, + AbsoluteUnixPath.get("/fileA"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + Instant.ofEpochSecond(123)))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + // Reads the file back. + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + assertThat(in.getNextEntry().getLastModifiedDate().toInstant()) + .isEqualTo(Instant.EPOCH.plusSeconds(123)); + } + } + + @Test + public void testBuild_permissions() throws IOException { + Path testRoot = temporaryFolder.getRoot().toPath(); + Path folder = Files.createDirectories(testRoot.resolve("files1")); + Path fileA = createFile(testRoot, "fileA", "abc", 54321); + Path fileB = createFile(testRoot, "fileB", "def", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of( + defaultLayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA")), + new FileEntry( + fileB, + AbsoluteUnixPath.get("/somewhere/fileB"), + FilePermissions.fromOctalString("123"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME), + new FileEntry( + folder, + AbsoluteUnixPath.get("/somewhere/folder"), + FilePermissions.fromOctalString("456"), + FileEntriesLayer.DEFAULT_MODIFICATION_TIME))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + // Root folder (default folder permissions) + assertThat(in.getNextEntry().getMode()).isEqualTo(040755); + // fileA (default file permissions) + assertThat(in.getNextEntry().getMode()).isEqualTo(0100644); + // fileB (custom file permissions) + assertThat(in.getNextEntry().getMode()).isEqualTo(0100123); + // folder (custom folder permissions) + assertThat(in.getNextEntry().getMode()).isEqualTo(040456); + } + } + @Test + public void testBuild_ownership() throws IOException { + Path testRoot = temporaryFolder.getRoot().toPath(); + Path someFile = createFile(testRoot, "someFile", "content", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of( + defaultLayerEntry(someFile, AbsoluteUnixPath.get("/file1")), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file2"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + ""), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file3"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + ":"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file4"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + "333:"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file5"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + ":555"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file6"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + "333:555"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file7"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + "user:"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file8"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + ":group"), + new FileEntry( + someFile, + AbsoluteUnixPath.get("/file9"), + FilePermissions.fromOctalString("123"), + Instant.EPOCH, + "user:group"))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + TarArchiveEntry entry1 = in.getNextEntry(); + assertThat(entry1.getLongUserId()).isEqualTo(0); + assertThat(entry1.getLongGroupId()).isEqualTo(0); + assertThat(entry1.getUserName()).isEmpty(); + assertThat(entry1.getGroupName()).isEmpty(); + + TarArchiveEntry entry2 = in.getNextEntry(); + assertThat(entry2.getLongUserId()).isEqualTo(0); + assertThat(entry2.getLongGroupId()).isEqualTo(0); + assertThat(entry2.getUserName()).isEmpty(); + assertThat(entry2.getGroupName()).isEmpty(); + + TarArchiveEntry entry3 = in.getNextEntry(); + assertThat(entry3.getLongUserId()).isEqualTo(0); + assertThat(entry3.getLongGroupId()).isEqualTo(0); + assertThat(entry3.getUserName()).isEmpty(); + assertThat(entry3.getGroupName()).isEmpty(); + + TarArchiveEntry entry4 = in.getNextEntry(); + assertThat(entry4.getLongUserId()).isEqualTo(333); + assertThat(entry4.getLongGroupId()).isEqualTo(0); + assertThat(entry4.getUserName()).isEmpty(); + assertThat(entry4.getGroupName()).isEmpty(); + + TarArchiveEntry entry5 = in.getNextEntry(); + assertThat(entry5.getLongUserId()).isEqualTo(0); + assertThat(entry5.getLongGroupId()).isEqualTo(555); + assertThat(entry5.getUserName()).isEmpty(); + assertThat(entry5.getGroupName()).isEmpty(); + + TarArchiveEntry entry6 = in.getNextEntry(); + assertThat(entry6.getLongUserId()).isEqualTo(333); + assertThat(entry6.getLongGroupId()).isEqualTo(555); + assertThat(entry6.getUserName()).isEmpty(); + assertThat(entry6.getGroupName()).isEmpty(); + + TarArchiveEntry entry7 = in.getNextEntry(); + assertThat(entry7.getLongUserId()).isEqualTo(0); + assertThat(entry7.getLongGroupId()).isEqualTo(0); + assertThat(entry7.getUserName()).isEqualTo("user"); + assertThat(entry7.getGroupName()).isEmpty(); + + TarArchiveEntry entry8 = in.getNextEntry(); + assertThat(entry8.getLongUserId()).isEqualTo(0); + assertThat(entry8.getLongGroupId()).isEqualTo(0); + assertThat(entry8.getUserName()).isEmpty(); + assertThat(entry8.getGroupName()).isEqualTo("group"); + + TarArchiveEntry entry9 = in.getNextEntry(); + assertThat(entry9.getLongUserId()).isEqualTo(0); + assertThat(entry9.getLongGroupId()).isEqualTo(0); + assertThat(entry9.getUserName()).isEqualTo("user"); + assertThat(entry9.getGroupName()).isEqualTo("group"); + } + } + + private static Path createFile(Path root, String filename, String content, long modificationTime) + throws IOException { Path newFile = Files.write( root.resolve(filename), content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW); - Files.setLastModifiedTime(newFile, FileTime.fromMillis(lastModifiedTime)); + Files.setLastModifiedTime(newFile, FileTime.fromMillis(modificationTime)); return newFile; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 018f5a39ab..b5de33f79b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -29,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -39,15 +40,23 @@ public class ContainerConfigurationTemplateTest { @Test public void testToJson() throws IOException, URISyntaxException, DigestException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setCreated("1970-01-01T00:00:20Z"); + containerConfigJson.setArchitecture("wasm"); + containerConfigJson.setOs("js"); containerConfigJson.setContainerEnvironment(Arrays.asList("VAR1=VAL1", "VAR2=VAL2")); containerConfigJson.setContainerEntrypoint(Arrays.asList("some", "entrypoint", "command")); containerConfigJson.setContainerCmd(Arrays.asList("arg1", "arg2")); + containerConfigJson.setContainerHealthCheckTest(Arrays.asList("CMD-SHELL", "/checkhealth")); + containerConfigJson.setContainerHealthCheckInterval(3000000000L); + containerConfigJson.setContainerHealthCheckTimeout(1000000000L); + containerConfigJson.setContainerHealthCheckStartPeriod(2000000000L); + containerConfigJson.setContainerHealthCheckRetries(3); containerConfigJson.setContainerExposedPorts( ImmutableSortedMap.of( "1000/tcp", @@ -56,39 +65,86 @@ public void testToJson() throws IOException, URISyntaxException, DigestException ImmutableMap.of(), "3000/udp", ImmutableMap.of())); + containerConfigJson.setContainerLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); + containerConfigJson.setContainerVolumes( + ImmutableMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of())); + containerConfigJson.setContainerWorkingDir("/some/workspace"); + containerConfigJson.setContainerUser("tomcat"); containerConfigJson.addLayerDiffId( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); + containerConfigJson.addHistoryEntry( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build()); + containerConfigJson.addHistoryEntry( + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(containerConfigJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfigJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); // Deserializes into a manifest JSON object. ContainerConfigurationTemplate containerConfigJson = JsonTemplateMapper.readJsonFromFile(jsonFile, ContainerConfigurationTemplate.class); + Assert.assertEquals("1970-01-01T00:00:20Z", containerConfigJson.getCreated()); + Assert.assertEquals("wasm", containerConfigJson.getArchitecture()); + Assert.assertEquals("js", containerConfigJson.getOs()); Assert.assertEquals( Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), containerConfigJson.getContainerEnvironment()); - Assert.assertEquals( Arrays.asList("some", "entrypoint", "command"), containerConfigJson.getContainerEntrypoint()); - Assert.assertEquals(Arrays.asList("arg1", "arg2"), containerConfigJson.getContainerCmd()); + Assert.assertEquals( + Arrays.asList("CMD-SHELL", "/checkhealth"), containerConfigJson.getContainerHealthTest()); + Assert.assertNotNull(containerConfigJson.getContainerHealthInterval()); + Assert.assertEquals(3000000000L, containerConfigJson.getContainerHealthInterval().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthTimeout()); + Assert.assertEquals(1000000000L, containerConfigJson.getContainerHealthTimeout().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthStartPeriod()); + Assert.assertEquals( + 2000000000L, containerConfigJson.getContainerHealthStartPeriod().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthRetries()); + Assert.assertEquals(3, containerConfigJson.getContainerHealthRetries().intValue()); + + Assert.assertEquals( + ImmutableMap.of("key1", "value1", "key2", "value2"), + containerConfigJson.getContainerLabels()); + Assert.assertEquals("/some/workspace", containerConfigJson.getContainerWorkingDir()); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), containerConfigJson.getLayerDiffId(0)); + Assert.assertEquals( + ImmutableList.of( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build(), + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()), + containerConfigJson.getHistory()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index c227f2ce31..7b5e5fb426 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,21 +16,24 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; -import com.google.common.io.ByteStreams; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -38,78 +41,134 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Map; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; /** Tests for {@link ImageToJsonTranslator}. */ public class ImageToJsonTranslatorTest { private ImageToJsonTranslator imageToJsonTranslator; - @Before - public void setUp() throws DigestException, LayerPropertyNotFoundException { - Image.Builder testImageBuilder = Image.builder(); - - testImageBuilder.setEnvironmentVariable("VAR1", "VAL1"); - testImageBuilder.setEnvironmentVariable("VAR2", "VAL2"); - - testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); - - testImageBuilder.setJavaArguments(Arrays.asList("arg1", "arg2")); - - testImageBuilder.setExposedPorts( - ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP))); + private void setUp(Class imageFormat) + throws DigestException, LayerPropertyNotFoundException { + Image.Builder testImageBuilder = + Image.builder(imageFormat) + .setCreated(Instant.ofEpochSecond(20)) + .setArchitecture("wasm") + .setOs("js") + .addEnvironmentVariable("VAR1", "VAL1") + .addEnvironmentVariable("VAR2", "VAL2") + .setEntrypoint(Arrays.asList("some", "entrypoint", "command")) + .setProgramArguments(Arrays.asList("arg1", "arg2")) + .setHealthCheck( + DockerHealthCheck.fromCommand(ImmutableList.of("CMD-SHELL", "/checkhealth")) + .setInterval(Duration.ofSeconds(3)) + .setTimeout(Duration.ofSeconds(1)) + .setStartPeriod(Duration.ofSeconds(2)) + .setRetries(3) + .build()) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs"))) + .addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) + .setWorkingDirectory("/some/workspace") + .setUser("tomcat"); DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - CachedLayer fakeLayer = - new CachedLayer(Mockito.mock(Path.class), new BlobDescriptor(1000, fakeDigest), fakeDigest); - testImageBuilder.addLayer(fakeLayer); - + testImageBuilder.addLayer( + new Layer() { + + @Override + public Blob getBlob() throws LayerPropertyNotFoundException { + return Blobs.from("ignored"); + } + + @Override + public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException { + return new BlobDescriptor(1000, fakeDigest); + } + + @Override + public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { + return fakeDigest; + } + }); + testImageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build()); + testImageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()); imageToJsonTranslator = new ImageToJsonTranslator(testImageBuilder.build()); } @Test - public void testGetContainerConfiguration() throws IOException, URISyntaxException { + public void testGetContainerConfiguration() + throws IOException, URISyntaxException, DigestException { + setUp(V22ManifestTemplate.class); + // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the container configuration and writes the JSON string. - Blob containerConfigurationBlob = imageToJsonTranslator.getContainerConfigurationBlob(); + JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - containerConfigurationBlob.writeTo(byteArrayOutputStream); - - Assert.assertEquals( - expectedJson, new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfiguration)); } @Test - public void testGetManifest_v22() throws URISyntaxException, IOException { - testGetManifest(V22ManifestTemplate.class, "json/translated_v22manifest.json"); + public void testGetManifest_v22() throws URISyntaxException, IOException, DigestException { + setUp(V22ManifestTemplate.class); + testGetManifest(V22ManifestTemplate.class, "core/json/translated_v22manifest.json"); } @Test - public void testGetManifest_oci() throws URISyntaxException, IOException { - testGetManifest(OCIManifestTemplate.class, "json/translated_ocimanifest.json"); + public void testGetManifest_oci() throws URISyntaxException, IOException, DigestException { + setUp(OciManifestTemplate.class); + testGetManifest(OciManifestTemplate.class, "core/json/translated_ocimanifest.json"); } @Test public void testPortListToMap() { - ImmutableList input = - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.UDP)); + ImmutableSet input = ImmutableSet.of(Port.tcp(1000), Port.udp(2000)); ImmutableSortedMap> expected = ImmutableSortedMap.of("1000/tcp", ImmutableMap.of(), "2000/udp", ImmutableMap.of()); - Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input)); + Assert.assertEquals(expected, ImageToJsonTranslator.portSetToMap(input)); + } + + @Test + public void testVolumeListToMap() { + ImmutableSet input = + ImmutableSet.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")); + ImmutableSortedMap> expected = + ImmutableSortedMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); + Assert.assertEquals(expected, ImageToJsonTranslator.volumesSetToMap(input)); + } + + @Test + public void testEnvironmentMapToList() { + ImmutableMap input = ImmutableMap.of("NAME1", "VALUE1", "NAME2", "VALUE2"); + ImmutableList expected = ImmutableList.of("NAME1=VALUE1", "NAME2=VALUE2"); + Assert.assertEquals(expected, ImageToJsonTranslator.environmentMapToList(input)); } /** Tests translation of image to {@link BuildableManifestTemplate}. */ @@ -121,15 +180,11 @@ private void testGetManifest( String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the manifest and writes the JSON string. - Blob containerConfigurationBlob = imageToJsonTranslator.getContainerConfigurationBlob(); - BlobDescriptor blobDescriptor = - containerConfigurationBlob.writeTo(ByteStreams.nullOutputStream()); + JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); + BlobDescriptor blobDescriptor = Digests.computeDigest(containerConfiguration); T manifestTemplate = imageToJsonTranslator.getManifestTemplate(manifestTemplateClass, blobDescriptor); - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, new String(jsonStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestTemplate)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index b731480611..5beabb6b94 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -27,15 +27,18 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; import org.junit.Assert; import org.junit.Test; @@ -44,42 +47,70 @@ public class JsonToImageTranslatorTest { @Test public void testToImage_v21() - throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException { + throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException, + BadContainerConfigurationFormatException { // Loads the JSON string. Path jsonFile = - Paths.get(getClass().getClassLoader().getResource("json/v21manifest.json").toURI()); + Paths.get(getClass().getClassLoader().getResource("core/json/v21manifest.json").toURI()); // Deserializes into a manifest JSON object. V21ManifestTemplate manifestTemplate = JsonTemplateMapper.readJsonFromFile(jsonFile, V21ManifestTemplate.class); - Image image = JsonToImageTranslator.toImage(manifestTemplate); + Image image = JsonToImageTranslator.toImage(manifestTemplate); List layers = image.getLayers(); - Assert.assertEquals(1, layers.size()); + Assert.assertEquals(2, layers.size()); Assert.assertEquals( DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + "sha256:5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3"), layers.get(0).getBlobDescriptor().getDigest()); + Assert.assertEquals( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + layers.get(1).getBlobDescriptor().getDigest()); } @Test public void testToImage_v22() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { - testToImage_buildable("json/v22manifest.json", V22ManifestTemplate.class); + testToImage_buildable("core/json/v22manifest.json", V22ManifestTemplate.class); } @Test public void testToImage_oci() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { - testToImage_buildable("json/ocimanifest.json", OCIManifestTemplate.class); + testToImage_buildable("core/json/ocimanifest.json", OciManifestTemplate.class); + } + + @Test + public void testToImage_canParseTimestampWithOffset() + throws IOException, LayerPropertyNotFoundException, URISyntaxException, + LayerCountMismatchException, BadContainerConfigurationFormatException { + Path containerConfigJson = + Paths.get( + getClass().getClassLoader().getResource("core/json/containerconfig.json").toURI()); + ContainerConfigurationTemplate containerConfig = + JsonTemplateMapper.readJsonFromFile( + containerConfigJson, ContainerConfigurationTemplate.class); + containerConfig.setCreated("2020-04-21T13:22:10.836777828-07:00"); + + Path manifestJson = + Paths.get(getClass().getClassLoader().getResource("core/json/v22manifest.json").toURI()); + V22ManifestTemplate manifest = + JsonTemplateMapper.readJsonFromFile(manifestJson, V22ManifestTemplate.class); + + // Should not throw BadContainerConfigFormatException. + // https://github.com/GoogleContainerTools/jib/issues/2428 + Image image = JsonToImageTranslator.toImage(manifest, containerConfig); + Assert.assertEquals(1587500530L, image.getCreated().getEpochSecond()); } @Test public void testPortMapToList() throws BadContainerConfigurationFormatException { - ImmutableSortedMap> input = + ImmutableSortedMap> input = ImmutableSortedMap.of( "1000", ImmutableMap.of(), @@ -87,35 +118,84 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException ImmutableMap.of(), "3000/udp", ImmutableMap.of()); - ImmutableList expected = - ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP)); - Assert.assertEquals(expected, JsonToImageTranslator.portMapToList(input)); + ImmutableSet expected = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)); + Assert.assertEquals(expected, JsonToImageTranslator.portMapToSet(input)); - ImmutableList>> badInputs = + ImmutableList>> badInputs = ImmutableList.of( ImmutableMap.of("abc", ImmutableMap.of()), ImmutableMap.of("1000-2000", ImmutableMap.of()), ImmutableMap.of("/udp", ImmutableMap.of()), ImmutableMap.of("123/xxx", ImmutableMap.of())); - for (Map> badInput : badInputs) { + for (Map> badInput : badInputs) { try { - JsonToImageTranslator.portMapToList(badInput); + JsonToImageTranslator.portMapToSet(badInput); Assert.fail(); } catch (BadContainerConfigurationFormatException ignored) { + // ignored } } } + @Test + public void testVolumeMapToList() throws BadContainerConfigurationFormatException { + ImmutableSortedMap> input = + ImmutableSortedMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); + ImmutableSet expected = + ImmutableSet.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")); + Assert.assertEquals(expected, JsonToImageTranslator.volumeMapToSet(input)); + + ImmutableList>> badInputs = + ImmutableList.of( + ImmutableMap.of("var/job-result-data", ImmutableMap.of()), + ImmutableMap.of("log", ImmutableMap.of()), + ImmutableMap.of("C:/udp", ImmutableMap.of())); + for (Map> badInput : badInputs) { + try { + JsonToImageTranslator.volumeMapToSet(badInput); + Assert.fail(); + } catch (BadContainerConfigurationFormatException ignored) { + // ignored + } + } + } + + @Test + public void testJsonToImageTranslatorRegex() { + assertGoodEnvironmentPattern("NAME=VALUE", "NAME", "VALUE"); + assertGoodEnvironmentPattern("A1203921=www=ww", "A1203921", "www=ww"); + assertGoodEnvironmentPattern("&*%(&#$(*@(%&@$*$(=", "&*%(&#$(*@(%&@$*$(", ""); + assertGoodEnvironmentPattern("m_a_8943=100", "m_a_8943", "100"); + assertGoodEnvironmentPattern("A_B_C_D=*****", "A_B_C_D", "*****"); + + assertBadEnvironmentPattern("================="); + assertBadEnvironmentPattern("A_B_C"); + } + + private void assertGoodEnvironmentPattern( + String input, String expectedName, String expectedValue) { + Matcher matcher = JsonToImageTranslator.ENVIRONMENT_PATTERN.matcher(input); + Assert.assertTrue(matcher.matches()); + Assert.assertEquals(expectedName, matcher.group("name")); + Assert.assertEquals(expectedValue, matcher.group("value")); + } + + private void assertBadEnvironmentPattern(String input) { + Matcher matcher = JsonToImageTranslator.ENVIRONMENT_PATTERN.matcher(input); + Assert.assertFalse(matcher.matches()); + } + private void testToImage_buildable( String jsonFilename, Class manifestTemplateClass) throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { // Loads the container configuration JSON. Path containerConfigurationJsonFile = - Paths.get(getClass().getClassLoader().getResource("json/containerconfig.json").toURI()); + Paths.get( + getClass().getClassLoader().getResource("core/json/containerconfig.json").toURI()); ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJsonFromFile( containerConfigurationJsonFile, ContainerConfigurationTemplate.class); @@ -126,8 +206,7 @@ private void testToImage_buildable( T manifestTemplate = JsonTemplateMapper.readJsonFromFile(manifestJsonFile, manifestTemplateClass); - Image image = - JsonToImageTranslator.toImage(manifestTemplate, containerConfigurationTemplate); + Image image = JsonToImageTranslator.toImage(manifestTemplate, containerConfigurationTemplate); List layers = image.getLayers(); Assert.assertEquals(1, layers.size()); @@ -141,13 +220,34 @@ private void testToImage_buildable( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), layers.get(0).getDiffId()); - Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); - Assert.assertEquals(Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), image.getEnvironment()); Assert.assertEquals( ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP)), - image.getExposedPorts()); + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build(), + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()), + image.getHistory()); + Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated()); + Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); + Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); + Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); + Assert.assertEquals( + ImmutableSet.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")), + image.getVolumes()); + Assert.assertEquals("tomcat", image.getUser()); + Assert.assertEquals("value1", image.getLabels().get("key1")); + Assert.assertEquals("value2", image.getLabels().get("key2")); + Assert.assertEquals(2, image.getLabels().size()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ManifestListGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ManifestListGeneratorTest.java new file mode 100644 index 0000000000..3c7a15e2ac --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ManifestListGeneratorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.image.Image; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link ManifestListGenerator}. */ +public class ManifestListGeneratorTest { + + private Image image1; + private Image image2; + private ManifestListGenerator manifestListGenerator; + + @Before + public void setUp() { + image1 = + Image.builder(V22ManifestTemplate.class).setArchitecture("amd64").setOs("linux").build(); + image2 = + Image.builder(V22ManifestTemplate.class).setArchitecture("arm64").setOs("windows").build(); + manifestListGenerator = new ManifestListGenerator(Arrays.asList(image1, image2)); + } + + @Test + public void testGetManifestListTemplate() throws IOException { + + // Expected Manifest List JSON + // { + // "schemaVersion":2, + // "mediaType":"application/vnd.docker.distribution.manifest.list.v2+json", + // "manifests":[ + // { + // "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + // "digest":"sha256:1f25787aab4669d252bdae09a72b9c345d2a7b8c64c8dbfba4c82af4834dbccc", + // "size":264, + // "platform":{ + // "architecture":"amd64", + // "os":"linux" + // } + // }, + // { + // "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + // "digest":"sha256:51038a7a91c0e8f747e05dd84c3b0393a7016ec312ce384fc945356778497ae3", + // "size":264, + // "platform":{ + // "architecture":"arm64", + // "os":"windows" + // } + // } + // ] + // } + + ManifestTemplate manifestTemplate = + manifestListGenerator.getManifestListTemplate(V22ManifestTemplate.class); + Assert.assertTrue(manifestTemplate instanceof V22ManifestListTemplate); + V22ManifestListTemplate manifestList = (V22ManifestListTemplate) manifestTemplate; + Assert.assertEquals(2, manifestList.getSchemaVersion()); + Assert.assertEquals( + Arrays.asList("sha256:1f25787aab4669d252bdae09a72b9c345d2a7b8c64c8dbfba4c82af4834dbccc"), + manifestList.getDigestsForPlatform("amd64", "linux")); + Assert.assertEquals( + Arrays.asList("sha256:51038a7a91c0e8f747e05dd84c3b0393a7016ec312ce384fc945356778497ae3"), + manifestList.getDigestsForPlatform("arm64", "windows")); + } + + @Test + public void testGetManifestListTemplate_emptyImagesList() throws IOException { + try { + new ManifestListGenerator(Collections.emptyList()) + .getManifestListTemplate(V22ManifestTemplate.class); + Assert.fail(); + } catch (IllegalStateException ex) { + Assert.assertEquals("no images given", ex.getMessage()); + } + } + + @Test + public void testGetManifestListTemplate_unsupportedImageFormat() throws IOException { + try { + new ManifestListGenerator(Arrays.asList(image1, image2)) + .getManifestListTemplate(OciManifestTemplate.class); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Build an OCI image index is not yet supported", ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java new file mode 100644 index 0000000000..0bfacce149 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link OciIndexTemplate}. */ +public class OciIndexTemplateTest { + + @Test + public void testToJson() throws DigestException, IOException, URISyntaxException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex.json").toURI()); + String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + + // Creates the JSON object to serialize. + OciIndexTemplate ociIndexJson = new OciIndexTemplate(); + ociIndexJson.addManifest( + new BlobDescriptor( + 1000, + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")), + "regis.try/repo:tag"); + + // Serializes the JSON object. + Assert.assertEquals( + expectedJson.replaceAll("[\r\n\t ]", ""), JsonTemplateMapper.toUtf8String(ociIndexJson)); + } + + @Test + public void testFromJson() throws IOException, URISyntaxException, DigestException { + // Loads the JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex.json").toURI()); + + // Deserializes into a manifest JSON object. + OciIndexTemplate ociIndexJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciIndexTemplate.class); + BuildableManifestTemplate.ContentDescriptorTemplate manifest = + ociIndexJson.getManifests().get(0); + + Assert.assertEquals(2, ociIndexJson.getSchemaVersion()); + Assert.assertEquals(OciIndexTemplate.MEDIA_TYPE, ociIndexJson.getManifestMediaType()); + Assert.assertEquals( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + manifest.getDigest()); + Assert.assertEquals( + "regis.try/repo:tag", manifest.getAnnotations().get("org.opencontainers.image.ref.name")); + Assert.assertEquals(1000, manifest.getSize()); + } + + @Test + public void testFromJsonWithUnknownProperties() + throws IOException, URISyntaxException, DigestException { + // Loads a JSON index whose manifest descriptors carry an OCI 1.1 "artifactType" field, which + // Jib does not model. Unknown descriptor properties must be ignored rather than fail parsing. + // https://github.com/GoogleContainerTools/jib/issues/4506 + Path jsonFile = + Paths.get(Resources.getResource("core/json/ociindex_artifacttype.json").toURI()); + + OciIndexTemplate ociIndexJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciIndexTemplate.class); + + Assert.assertEquals(2, ociIndexJson.getManifests().size()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + ociIndexJson.getDigestsForPlatform("amd64", "linux").get(0)); + Assert.assertEquals( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + ociIndexJson.getDigestsForPlatform("arm64", "linux").get(0)); + } + + @Test + public void testToJsonWithPlatform() throws DigestException, IOException, URISyntaxException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex_platforms.json").toURI()); + String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + + // Creates the JSON object to serialize. + OciIndexTemplate ociIndexJson = new OciIndexTemplate(); + + OciIndexTemplate.ManifestDescriptorTemplate ppc64leManifest = + new OciIndexTemplate.ManifestDescriptorTemplate( + OciManifestTemplate.MANIFEST_MEDIA_TYPE, + 7143, + DescriptorDigest.fromDigest( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f")); + ppc64leManifest.setPlatform("ppc64le", "linux"); + ociIndexJson.addManifest(ppc64leManifest); + + OciIndexTemplate.ManifestDescriptorTemplate amd64Manifest = + new OciIndexTemplate.ManifestDescriptorTemplate( + OciManifestTemplate.MANIFEST_MEDIA_TYPE, + 7682, + DescriptorDigest.fromDigest( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270")); + amd64Manifest.setPlatform("amd64", "linux"); + ociIndexJson.addManifest(amd64Manifest); + + // Serializes the JSON object. + Assert.assertEquals( + expectedJson.replaceAll("[\r\n\t ]", ""), JsonTemplateMapper.toUtf8String(ociIndexJson)); + } + + @Test + public void testFromJsonWithPlatform() throws IOException, URISyntaxException, DigestException { + // Loads the JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex_platforms.json").toURI()); + + // Deserializes into a manifest JSON object. + OciIndexTemplate ociIndexJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciIndexTemplate.class); + + Assert.assertEquals(2, ociIndexJson.getManifests().size()); + Assert.assertEquals( + "ppc64le", ociIndexJson.getManifests().get(0).getPlatform().getArchitecture()); + Assert.assertEquals("linux", ociIndexJson.getManifests().get(0).getPlatform().getOs()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + ociIndexJson.getDigestsForPlatform("ppc64le", "linux").get(0)); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java similarity index 75% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java index b7c0bacd0e..6eaac9c4bf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,10 +16,9 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -30,17 +29,17 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link OCIManifestTemplate}. */ -public class OCIManifestTemplateTest { +/** Tests for {@link OciManifestTemplate}. */ +public class OciManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/ocimanifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. - OCIManifestTemplate manifestJson = new OCIManifestTemplate(); + OciManifestTemplate manifestJson = new OciManifestTemplate(); manifestJson.setContainerConfiguration( 1000, @@ -53,20 +52,17 @@ public void testToJson() throws DigestException, IOException, URISyntaxException "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/ocimanifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); // Deserializes into a manifest JSON object. - OCIManifestTemplate manifestJson = - JsonTemplateMapper.readJsonFromFile(jsonFile, OCIManifestTemplate.class); + OciManifestTemplate manifestJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciManifestTemplate.class); Assert.assertEquals( DescriptorDigest.fromDigest( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index 234aa226d6..a2f33c1b5c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import java.io.IOException; @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -33,7 +34,7 @@ public class V21ManifestTemplateTest { @Test public void testFromJson() throws URISyntaxException, IOException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v21manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); // Deserializes into a manifest JSON object. V21ManifestTemplate manifestJson = @@ -44,6 +45,12 @@ public void testFromJson() throws URISyntaxException, IOException, DigestExcepti "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), manifestJson.getFsLayers().get(0).getDigest()); - Assert.assertEquals("some v1-compatible object", manifestJson.getV1Compatibility(0)); + ContainerConfigurationTemplate containerConfiguration = + manifestJson.getContainerConfiguration().orElse(null); + Assert.assertEquals( + Arrays.asList("JAVA_HOME=/opt/openjdk", "PATH=/opt/openjdk/bin"), + containerConfiguration.getContainerEnvironment()); + Assert.assertEquals( + Arrays.asList("/opt/openjdk/bin/java"), containerConfiguration.getContainerEntrypoint()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java new file mode 100644 index 0000000000..c04ab5f291 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class V22ManifestListTemplateTest { + + @Test + public void testFromJson() throws IOException, URISyntaxException { + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + + V22ManifestListTemplate manifestListJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, V22ManifestListTemplate.class); + + Assert.assertEquals(2, manifestListJson.getSchemaVersion()); + List manifests = manifestListJson.getManifests(); + Assert.assertEquals(3, manifests.size()); + + List validPlatformPpc = manifestListJson.getDigestsForPlatform("ppc64le", "linux"); + Assert.assertEquals(1, validPlatformPpc.size()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + validPlatformPpc.get(0)); + + List validPlatformAmd = manifestListJson.getDigestsForPlatform("amd64", "linux"); + Assert.assertEquals(2, validPlatformAmd.size()); + Assert.assertEquals( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + validPlatformAmd.get(0)); + Assert.assertEquals( + "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + validPlatformAmd.get(1)); + + List invalidArch = manifestListJson.getDigestsForPlatform("amd72", "linux"); + Assert.assertEquals(0, invalidArch.size()); + + List invalidOs = manifestListJson.getDigestsForPlatform("amd64", "minix"); + Assert.assertEquals(0, invalidOs.size()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index 2163081509..91955bc901 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,10 +16,11 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate.ContentDescriptorTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -27,6 +28,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.util.Arrays; +import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -36,7 +39,7 @@ public class V22ManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. @@ -53,16 +56,13 @@ public void testToJson() throws DigestException, IOException, URISyntaxException "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); // Deserializes into a manifest JSON object. V22ManifestTemplate manifestJson = @@ -82,4 +82,28 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti Assert.assertEquals(1000_000, manifestJson.getLayers().get(0).getSize()); } + + @Test + public void testFromJson_optionalProperties() throws IOException, URISyntaxException { + Path jsonFile = + Paths.get(Resources.getResource("core/json/v22manifest_optional_properties.json").toURI()); + + V22ManifestTemplate manifestJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, V22ManifestTemplate.class); + + List layers = manifestJson.getLayers(); + Assert.assertEquals(4, layers.size()); + Assert.assertNull(layers.get(0).getUrls()); + Assert.assertNull(layers.get(0).getAnnotations()); + + Assert.assertEquals(Arrays.asList("url-foo", "url-bar"), layers.get(1).getUrls()); + Assert.assertNull(layers.get(1).getAnnotations()); + + Assert.assertNull(layers.get(2).getUrls()); + Assert.assertEquals(ImmutableMap.of("key-foo", "value-foo"), layers.get(2).getAnnotations()); + + Assert.assertEquals(Arrays.asList("cool-url"), layers.get(3).getUrls()); + Assert.assertEquals( + ImmutableMap.of("key1", "value1", "key2", "value2"), layers.get(3).getAnnotations()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 875d3eabae..6723a84c0b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -29,6 +29,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; @@ -53,7 +55,7 @@ private static class InnerObject implements JsonTemplate { @Test public void testWriteJson() throws DigestException, IOException, URISyntaxException { - Path jsonFile = Paths.get(Resources.getResource("json/basic.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/basic.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); TestJson testJson = new TestJson(); @@ -84,9 +86,83 @@ public void testWriteJson() throws DigestException, IOException, URISyntaxExcept "sha256:d38f571aa1c11e3d516e0ef7e513e7308ccbeb869770cb8c4319d63b10a0075e")); testJson.list = Arrays.asList(innerObject1, innerObject2); - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(testJson).writeTo(jsonStream); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(testJson)); + } + + @Test + public void testReadJsonWithLock() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("core/json/basic.json").toURI()); + + // Deserializes into a metadata JSON object. + TestJson testJson = JsonTemplateMapper.readJsonFromFileWithLock(jsonFile, TestJson.class); + + MatcherAssert.assertThat(testJson.number, CoreMatchers.is(54)); + MatcherAssert.assertThat(testJson.text, CoreMatchers.is("crepecake")); + MatcherAssert.assertThat( + testJson.digest, + CoreMatchers.is( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"))); + MatcherAssert.assertThat( + testJson.innerObject, CoreMatchers.instanceOf(TestJson.InnerObject.class)); + MatcherAssert.assertThat(testJson.innerObject.number, CoreMatchers.is(23)); + MatcherAssert.assertThat( + testJson.innerObject.texts, CoreMatchers.is(Arrays.asList("first text", "second text"))); + MatcherAssert.assertThat( + testJson.innerObject.digests, + CoreMatchers.is( + Arrays.asList( + DescriptorDigest.fromDigest( + "sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"), + DescriptorDigest.fromHash( + "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")))); + // ignore testJson.list + } + + @Test + public void testReadListOfJson() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("core/json/basic_list.json").toURI()); + + String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + List listofJsons = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); + TestJson json1 = listofJsons.get(0); + TestJson json2 = listofJsons.get(1); + + DescriptorDigest digest1 = + DescriptorDigest.fromDigest( + "sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"); + DescriptorDigest digest2 = + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(1, json1.number); + Assert.assertEquals(2, json2.number); + Assert.assertEquals("text1", json1.text); + Assert.assertEquals("text2", json2.text); + Assert.assertEquals(digest1, json1.digest); + Assert.assertEquals(digest2, json2.digest); + Assert.assertEquals(10, json1.innerObject.number); + Assert.assertEquals(20, json2.innerObject.number); + Assert.assertEquals(2, json1.list.size()); + Assert.assertTrue(json2.list.isEmpty()); + } + + @Test + public void testToBlob_listOfJson() throws IOException, URISyntaxException { + Path jsonFile = Paths.get(Resources.getResource("core/json/basic_list.json").toURI()); + + String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + List listOfJson = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); + + Assert.assertEquals(jsonString, JsonTemplateMapper.toUtf8String(listOfJson)); + } + + @Test + public void testReadJson_inputStream() throws IOException { + String testJson = "{\"number\":3, \"text\":\"cool\"}"; + ByteArrayInputStream in = new ByteArrayInputStream(testJson.getBytes(StandardCharsets.UTF_8)); + TestJson json = JsonTemplateMapper.readJson(in, TestJson.class); + Assert.assertEquals(3, json.number); + Assert.assertEquals("cool", json.text); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 62698a2140..44d3fd7c9f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,12 +18,15 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collections; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,13 +38,15 @@ @RunWith(MockitoJUnitRunner.class) public class AuthenticationMethodRetrieverTest { - @Mock private HttpResponseException mockHttpResponseException; + @Mock private ResponseException mockResponseException; @Mock private HttpHeaders mockHeaders; + @Mock private FailoverHttpClient httpClient; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private final AuthenticationMethodRetriever testAuthenticationMethodRetriever = - new AuthenticationMethodRetriever(fakeRegistryEndpointRequestProperties); + new AuthenticationMethodRetriever( + fakeRegistryEndpointRequestProperties, "user-agent", httpClient); @Test public void testGetContent() { @@ -55,8 +60,8 @@ public void testGetAccept() { @Test public void testHandleResponse() { - Assert.assertNull( - testAuthenticationMethodRetriever.handleResponse(Mockito.mock(Response.class))); + Assert.assertFalse( + testAuthenticationMethodRetriever.handleResponse(Mockito.mock(Response.class)).isPresent()); } @Test @@ -80,53 +85,52 @@ public void testGetActionDescription() { @Test public void testHandleHttpResponseException_invalidStatusCode() throws RegistryErrorException { - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); + Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should only handle HTTP 401 Unauthorized errors"); - } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertEquals(mockResponseException, ex); } } @Test - public void tsetHandleHttpResponseException_noHeader() throws HttpResponseException { - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException_noHeader() throws ResponseException { + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(null); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header is not found"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("'WWW-Authenticate' header not found")); } } @Test - public void testHandleHttpResponseException_badAuthenticationMethod() - throws HttpResponseException { + public void testHandleHttpResponseException_badAuthenticationMethod() throws ResponseException { String authenticationMethod = "bad authentication method"; - Mockito.when(mockHttpResponseException.getStatusCode()) + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header failed to parse"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( "Failed get authentication method from 'WWW-Authenticate' header")); @@ -135,20 +139,21 @@ public void testHandleHttpResponseException_badAuthenticationMethod() @Test public void testHandleHttpResponseException_pass() - throws RegistryErrorException, HttpResponseException, MalformedURLException { + throws RegistryErrorException, ResponseException, MalformedURLException { String authenticationMethod = "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\""; - Mockito.when(mockHttpResponseException.getStatusCode()) + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); RegistryAuthenticator registryAuthenticator = - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), - registryAuthenticator.getAuthenticationUrl("someScope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someImageName", "someScope"))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 2bbe2d4392..49cb7fa31d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,11 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; @@ -30,6 +29,7 @@ import java.net.URL; import java.security.DigestException; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -63,7 +63,7 @@ public void testHandleResponse() throws RegistryErrorException { Mockito.when(mockResponse.getContentLength()).thenReturn(0L); BlobDescriptor expectedBlobDescriptor = new BlobDescriptor(0, fakeDigest); - BlobDescriptor blobDescriptor = testBlobChecker.handleResponse(mockResponse); + BlobDescriptor blobDescriptor = testBlobChecker.handleResponse(mockResponse).get(); Assert.assertEquals(expectedBlobDescriptor, blobDescriptor); } @@ -77,83 +77,79 @@ public void testHandleResponse_noContentLength() { Assert.fail("Should throw exception if Content-Length header is not present"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Did not receive Content-Length header")); } } @Test - public void testHandleHttpResponseException() throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException() throws IOException { + ResponseException mockResponseException = Mockito.mock(ResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")); - Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + Mockito.when(mockResponseException.getContent()) + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); - BlobDescriptor blobDescriptor = - testBlobChecker.handleHttpResponseException(mockHttpResponseException); - - Assert.assertNull(blobDescriptor); + Assert.assertFalse( + testBlobChecker.handleHttpResponseException(mockResponseException).isPresent()); } @Test - public void testHandleHttpResponseException_hasOtherErrors() - throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException_hasOtherErrors() throws IOException { + ResponseException mockResponseException = Mockito.mock(ResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")) .addError(new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "some message")); - Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + Mockito.when(mockResponseException.getContent()) + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); - } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertEquals(mockResponseException, ex); } } @Test - public void testHandleHttpResponseException_notBlobUnknown() - throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException_notBlobUnknown() throws IOException { + ResponseException mockResponseException = Mockito.mock(ResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate(); - Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + Mockito.when(mockResponseException.getContent()) + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); - } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertEquals(mockResponseException, ex); } } @Test - public void testHandleHttpResponseException_invalidStatusCode() throws RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); + public void testHandleHttpResponseException_invalidStatusCode() { + ResponseException mockResponseException = Mockito.mock(ResponseException.class); + Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-404 status codes should not be handled"); - } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertEquals(mockResponseException, ex); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index 8345f7a165..09d9f09e39 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,18 +16,19 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.DigestException; +import java.util.concurrent.atomic.LongAdder; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -56,33 +57,50 @@ public void setUpFakes() throws DigestException { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); testBlobPuller = - new BlobPuller(fakeRegistryEndpointRequestProperties, fakeDigest, layerOutputStream); + new BlobPuller( + fakeRegistryEndpointRequestProperties, + fakeDigest, + layerOutputStream, + ignored -> {}, + ignored -> {}); } @Test public void testHandleResponse() throws IOException, UnexpectedBlobDigestException { - Blob testBlob = Blobs.from("some BLOB content"); - DescriptorDigest testBlobDigest = testBlob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + InputStream blobContent = + new ByteArrayInputStream("some BLOB content".getBytes(StandardCharsets.UTF_8)); + DescriptorDigest testBlobDigest = Digests.computeDigest(blobContent).getDigest(); + blobContent.reset(); Response mockResponse = Mockito.mock(Response.class); - Mockito.when(mockResponse.getBody()).thenReturn(testBlob); + Mockito.when(mockResponse.getContentLength()).thenReturn((long) "some BLOB content".length()); + Mockito.when(mockResponse.getBody()).thenReturn(blobContent); + LongAdder byteCount = new LongAdder(); BlobPuller blobPuller = - new BlobPuller(fakeRegistryEndpointRequestProperties, testBlobDigest, layerOutputStream); + new BlobPuller( + fakeRegistryEndpointRequestProperties, + testBlobDigest, + layerOutputStream, + size -> Assert.assertEquals("some BLOB content".length(), size.longValue()), + byteCount::add); blobPuller.handleResponse(mockResponse); Assert.assertEquals( "some BLOB content", new String(layerContentOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals(testBlobDigest, layerOutputStream.toBlobDescriptor().getDigest()); + Assert.assertEquals(testBlobDigest, layerOutputStream.computeDigest().getDigest()); + Assert.assertEquals("some BLOB content".length(), byteCount.sum()); } @Test public void testHandleResponse_unexpectedDigest() throws IOException { - Blob testBlob = Blobs.from("some BLOB content"); - DescriptorDigest testBlobDigest = testBlob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + InputStream blobContent = + new ByteArrayInputStream("some BLOB content".getBytes(StandardCharsets.UTF_8)); + DescriptorDigest testBlobDigest = Digests.computeDigest(blobContent).getDigest(); + blobContent.reset(); Response mockResponse = Mockito.mock(Response.class); - Mockito.when(mockResponse.getBody()).thenReturn(testBlob); + Mockito.when(mockResponse.getBody()).thenReturn(blobContent); try { testBlobPuller.handleResponse(mockResponse); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index 49db99edf5..7950a5f822 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -17,18 +17,23 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.GenericUrl; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.DigestException; import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -41,8 +46,10 @@ @RunWith(MockitoJUnitRunner.class) public class BlobPusherTest { - @Mock private Blob mockBlob; - @Mock private URL mockURL; + private static final String TEST_BLOB_CONTENT = "some BLOB content"; + private static final Blob TEST_BLOB = Blobs.from(TEST_BLOB_CONTENT); + + @Mock private URL mockUrl; @Mock private Response mockResponse; private DescriptorDigest fakeDescriptorDigest; @@ -57,7 +64,8 @@ public void setUpFakes() throws DigestException { new BlobPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeDescriptorDigest, - mockBlob); + TEST_BLOB, + null); } @Test @@ -73,7 +81,7 @@ public void testGetAccept() { @Test public void testInitializer_handleResponse_created() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(201); // Created - Assert.assertNull(testBlobPusher.initializer().handleResponse(mockResponse)); + Assert.assertFalse(testBlobPusher.initializer().handleResponse(mockResponse).isPresent()); } @Test @@ -85,7 +93,7 @@ public void testInitializer_handleResponse_accepted() throws IOException, Regist Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://someurl/location"), - testBlobPusher.initializer().handleResponse(mockResponse)); + testBlobPusher.initializer().handleResponse(mockResponse).get()); } @Test @@ -99,7 +107,7 @@ public void testInitializer_handleResponse_accepted_multipleLocations() Assert.fail("Multiple 'Location' headers should be a registry error"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Expected 1 'Location' header, but found 2")); } @@ -113,15 +121,32 @@ public void testInitializer_handleResponse_unrecognized() throws IOException, Re Assert.fail("Multiple 'Location' headers should be a registry error"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Received unrecognized status code -1")); } } @Test - public void testInitializer_getApiRoute() throws MalformedURLException { + public void testInitializer_getApiRoute_nullSource() throws MalformedURLException { Assert.assertEquals( - new URL("http://someApiBase/someImageName/blobs/uploads/?mount=" + fakeDescriptorDigest), + new URL("http://someApiBase/someImageName/blobs/uploads/"), + testBlobPusher.initializer().getApiRoute("http://someApiBase/")); + } + + @Test + public void testInitializer_getApiRoute_sameSource() throws MalformedURLException { + testBlobPusher = + new BlobPusher( + new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), + fakeDescriptorDigest, + TEST_BLOB, + "sourceImageName"); + + Assert.assertEquals( + new URL( + "http://someApiBase/someImageName/blobs/uploads/?mount=" + + fakeDescriptorDigest + + "&from=sourceImageName"), testBlobPusher.initializer().getApiRoute("http://someApiBase/")); } @@ -139,18 +164,23 @@ public void testInitializer_getActionDescription() { @Test public void testWriter_getContent() throws IOException { - BlobHttpContent body = testBlobPusher.writer(mockURL).getContent(); + LongAdder byteCount = new LongAdder(); + BlobHttpContent body = testBlobPusher.writer(mockUrl, byteCount::add).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); - body.writeTo(ByteStreams.nullOutputStream()); - Mockito.verify(mockBlob).writeTo(ByteStreams.nullOutputStream()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + body.writeTo(byteArrayOutputStream); + + Assert.assertEquals( + TEST_BLOB_CONTENT, new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(TEST_BLOB_CONTENT.length(), byteCount.sum()); } @Test public void testWriter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.writer(mockURL).getAccept().size()); + Assert.assertEquals(0, testBlobPusher.writer(mockUrl, ignored -> {}).getAccept().size()); } @Test @@ -161,41 +191,41 @@ public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), - testBlobPusher.writer(mockURL).handleResponse(mockResponse)); + testBlobPusher.writer(mockUrl, ignored -> {}).handleResponse(mockResponse)); } @Test public void testWriter_getApiRoute() throws MalformedURLException { URL fakeUrl = new URL("http://someurl"); - Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl).getApiRoute("")); + Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl, ignored -> {}).getApiRoute("")); } @Test public void testWriter_getHttpMethod() { - Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL).getHttpMethod()); + Assert.assertEquals("PATCH", testBlobPusher.writer(mockUrl, ignored -> {}).getHttpMethod()); } @Test public void testWriter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.writer(mockURL).getActionDescription()); + testBlobPusher.writer(mockUrl, ignored -> {}).getActionDescription()); } @Test public void testCommitter_getContent() { - Assert.assertNull(testBlobPusher.committer(mockURL).getContent()); + Assert.assertNull(testBlobPusher.committer(mockUrl).getContent()); } @Test public void testCommitter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.committer(mockURL).getAccept().size()); + Assert.assertEquals(0, testBlobPusher.committer(mockUrl).getAccept().size()); } @Test public void testCommitter_handleResponse() throws IOException, RegistryException { Assert.assertNull( - testBlobPusher.committer(mockURL).handleResponse(Mockito.mock(Response.class))); + testBlobPusher.committer(mockUrl).handleResponse(Mockito.mock(Response.class))); } @Test @@ -207,13 +237,13 @@ public void testCommitter_getApiRoute() throws MalformedURLException { @Test public void testCommitter_getHttpMethod() { - Assert.assertEquals("PUT", testBlobPusher.committer(mockURL).getHttpMethod()); + Assert.assertEquals("PUT", testBlobPusher.committer(mockUrl).getHttpMethod()); } @Test public void testCommitter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.committer(mockURL).getActionDescription()); + testBlobPusher.committer(mockUrl).getActionDescription()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java new file mode 100644 index 0000000000..06558ed94e --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.http.Authorization; +import com.google.common.collect.Multimap; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Tests for {@link RegistryClient} around handling of Docker Registry Bearer Tokens. + * + *

    JWTs were generated from jwt.io's JWT Debugger. Set the + * algorithm to HS256, and paste the JSON shown as the Payload. + */ +public class DockerRegistryBearerTokenTest { + @Test + public void testDecode_dockerToken() { + // A genuine token from accessing docker.io's openjdk: + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull"]}] + // Generated by + // $ cd examples/helloworld + // $ mvn package jib:dockerBuild -Djib.from.image=openjdk \ + // -Djava.util.logging.config.file= + Multimap decoded = + RegistryClient.decodeTokenRepositoryGrants( + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" + + ".eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCJdfV0sImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5pbyIsImV4cCI6MTU2MTA0MzkwNSwiaWF0IjoxNTYxMDQzNjA1LCJpc3MiOiJhdXRoLmRvY2tlci5pbyIsImp0aSI6Ikc5bWpiOE9GeU5STFlpY3ZUMFZxIiwibmJmIjoxNTYxMDQzMzA1LCJzdWIiOiIifQ" + + ".jblwG_taIVf3IRiv200ivsc8q_IUj-M9QePKPAULfXdSZlY6H9n_XWtT6lw43k-J6QHfmnY4Yuh3eZq61KS7AT9yggM1VuolRCvYztSZ-MZHMIlvSE2KCc0wXa5gNQarjmDJloYduZuyLaKaRUUbO4osk1MuruODY_c2g2j16ce0Z8XVJ-7R8_J_Z8g0GdtFAfPO4bqpg9dj31MA8AKl3h-ru8NXcs3y1PkrYHpEGCgpcGcUQwLY7uiIrzjr0trCUbsLsv6iq2XTXnN_tTrfvL1R3yTB6gITvXZdsnU3r_UIDTzexTtlZWdntucJAGKX9HMA_jYEcTZ4ZhyEzETGpw"); + Assert.assertEquals(1, decoded.size()); + Assert.assertTrue(decoded.containsEntry("library/openjdk", "pull")); + Assert.assertFalse(decoded.containsEntry("library/openjdk", "push")); + Assert.assertFalse(decoded.containsEntry("randomrepo", "push")); + } + + @Test + public void testDecode_nonToken() { + String base64Text = + Base64.getEncoder() + .encodeToString("something other than a JWT token".getBytes(StandardCharsets.UTF_8)); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(base64Text); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessString() { + // a JWT with an "access" field that is not an array: {"access": "string"} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessArray() { + // a JWT with an "access" field that is an array of non-claim objects: {"access":["string"]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + @Ignore("Annotate AccessClaim.actions to disallow coercion of integers to strings") + public void testDecode_invalidToken_actionsArray() { + // a JWT with an "access" field that is an action array of non-strings: + // {"access":[{"type": "repository","name": "library/openjdk","actions":[1]}]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_randoJwt() { + // the JWT example token from jwt.io + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + /** Basic credential should allow access to all. */ + @Test + public void testCanAttemptBlobMount_basicCredential() { + Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "random")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "library/openjdk")); + } + + @Test + public void testCanAttemptBlobMount_bearer_withToken() { + // a synthetic token for accessing docker.io's openjdk with push and pull + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull","push"]}]} + String token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"; + Authorization authorization = Authorization.fromBearerToken(token); + Assert.assertNotNull(authorization); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertFalse(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); + } + + @Test + public void testCanAttemptBlobMount_bearer_withNonToken() { + // non-Docker Registry Bearer Tokens are assumed to allow access to all + // the JWT example token from jwt.io + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Authorization authorization = Authorization.fromBearerToken(jwt); + Assert.assertNotNull(authorization); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java new file mode 100644 index 0000000000..d5dd5ae007 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.http.ResponseException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Test for {@link ErrorReponseUtil}. */ +@RunWith(MockitoJUnitRunner.class) +public class ErrorResponseUtilTest { + + @Mock private ResponseException responseException; + + @Test + public void testGetErrorCode_knownErrorCode() throws ResponseException { + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}"); + + Assert.assertSame( + ErrorCodes.MANIFEST_INVALID, ErrorResponseUtil.getErrorCode(responseException)); + } + + /** An unknown {@link ErrorCodes} should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_unknownErrorCode() { + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"INVALID_ERROR_CODE\",\"message\":\"invalid code\",\"detail\":{}}]}"); + try { + ErrorResponseUtil.getErrorCode(responseException); + Assert.fail(); + } catch (ResponseException ex) { + Assert.assertSame(responseException, ex); + } + } + + /** Multiple error objects should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_multipleErrors() { + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[" + + "{\"code\":\"MANIFEST_INVALID\",\"message\":\"message 1\",\"detail\":{}}," + + "{\"code\":\"TAG_INVALID\",\"message\":\"message 2\",\"detail\":{}}" + + "]}"); + try { + ErrorResponseUtil.getErrorCode(responseException); + Assert.fail(); + } catch (ResponseException ex) { + Assert.assertSame(responseException, ex); + } + } + + /** An non-error object should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_invalidErrorObject() { + Mockito.when(responseException.getContent()) + .thenReturn("{\"type\":\"other\",\"message\":\"some other object\"}"); + try { + ErrorResponseUtil.getErrorCode(responseException); + Assert.fail(); + } catch (ResponseException ex) { + Assert.assertSame(responseException, ex); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index 99e694cfe4..e168ae0239 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,23 +16,31 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,7 +52,9 @@ @RunWith(MockitoJUnitRunner.class) public class ManifestPullerTest { - @Mock private Response mockResponse; + private static InputStream stringToInputStreamUtf8(String string) { + return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); + } private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); @@ -52,37 +62,158 @@ public class ManifestPullerTest { new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class); + @Mock private Response mockResponse; + @Test public void testHandleResponse_v21() throws URISyntaxException, IOException, UnknownManifestFormatException { - Path v21ManifestFile = Paths.get(Resources.getResource("json/v21manifest.json").toURI()); + Path v21ManifestFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); + InputStream v21Manifest = new ByteArrayInputStream(Files.readAllBytes(v21ManifestFile)); + + DescriptorDigest expectedDigest = Digests.computeDigest(v21Manifest).getDigest(); + v21Manifest.reset(); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v21ManifestFile)); - ManifestTemplate manifestTemplate = + Mockito.when(mockResponse.getBody()).thenReturn(v21Manifest); + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .handleResponse(mockResponse); - Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V21ManifestTemplate.class)); + MatcherAssert.assertThat( + manifestAndDigest.getManifest(), CoreMatchers.instanceOf(V21ManifestTemplate.class)); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test public void testHandleResponse_v22() throws URISyntaxException, IOException, UnknownManifestFormatException { - Path v22ManifestFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path v22ManifestFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); + InputStream v22Manifest = new ByteArrayInputStream(Files.readAllBytes(v22ManifestFile)); + + DescriptorDigest expectedDigest = Digests.computeDigest(v22Manifest).getDigest(); + v22Manifest.reset(); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v22ManifestFile)); - ManifestTemplate manifestTemplate = + Mockito.when(mockResponse.getBody()).thenReturn(v22Manifest); + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) .handleResponse(mockResponse); - Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestTemplate.class)); + MatcherAssert.assertThat( + manifestAndDigest.getManifest(), CoreMatchers.instanceOf(V22ManifestTemplate.class)); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); + } + + @Test + public void testHandleResponse_ociManifest() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path ociManifestFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); + InputStream ociManifest = new ByteArrayInputStream(Files.readAllBytes(ociManifestFile)); + + DescriptorDigest expectedDigest = Digests.computeDigest(ociManifest).getDigest(); + ociManifest.reset(); + + Mockito.when(mockResponse.getBody()).thenReturn(ociManifest); + ManifestAndDigest manifestAndDigest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", OciManifestTemplate.class) + .handleResponse(mockResponse); + + MatcherAssert.assertThat( + manifestAndDigest.getManifest(), CoreMatchers.instanceOf(OciManifestTemplate.class)); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); + } + + @Test + public void testHandleResponse_v22ManifestListFailsWhenParsedAsV22Manifest() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + try { + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) + .handleResponse(mockResponse); + Assert.fail(); + } catch (ClassCastException ex) { + // pass + } + } + + @Test + public void testHandleResponse_v22ManifestListFromParentType() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + DescriptorDigest expectedDigest = Digests.computeDigest(v22ManifestList).getDigest(); + v22ManifestList.reset(); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + ManifestAndDigest manifestAndDigest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) + .handleResponse(mockResponse); + ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); + + MatcherAssert.assertThat( + manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + Assert.assertTrue(((V22ManifestListTemplate) manifestTemplate).getManifests().size() > 0); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); + } + + @Test + public void testHandleResponse_v22ManifestList() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + + DescriptorDigest expectedDigest = Digests.computeDigest(v22ManifestList).getDigest(); + v22ManifestList.reset(); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + ManifestAndDigest manifestAndDigest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, + "test-image-tag", + V22ManifestListTemplate.class) + .handleResponse(mockResponse); + V22ManifestListTemplate manifestTemplate = manifestAndDigest.getManifest(); + + MatcherAssert.assertThat( + manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + Assert.assertTrue(manifestTemplate.getManifests().size() > 0); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); + } + + @Test + public void testHandleResponse_OciIndex() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path ociIndexFile = + Paths.get(Resources.getResource("core/json/ociindex_platforms.json").toURI()); + InputStream ociIndex = new ByteArrayInputStream(Files.readAllBytes(ociIndexFile)); + + DescriptorDigest expectedDigest = Digests.computeDigest(ociIndex).getDigest(); + ociIndex.reset(); + + Mockito.when(mockResponse.getBody()).thenReturn(ociIndex); + ManifestAndDigest manifestAndDigest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", OciIndexTemplate.class) + .handleResponse(mockResponse); + OciIndexTemplate manifestTemplate = manifestAndDigest.getManifest(); + + MatcherAssert.assertThat(manifestTemplate, CoreMatchers.instanceOf(OciIndexTemplate.class)); + Assert.assertTrue(manifestTemplate.getManifests().size() > 0); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test public void testHandleResponse_noSchemaVersion() throws IOException { - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("{}")); + Mockito.when(mockResponse.getBody()).thenReturn(stringToInputStreamUtf8("{}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An empty manifest should throw an error"); @@ -95,19 +226,20 @@ public void testHandleResponse_noSchemaVersion() throws IOException { @Test public void testHandleResponse_invalidSchemaVersion() throws IOException { Mockito.when(mockResponse.getBody()) - .thenReturn(Blobs.from("{\"schemaVersion\":\"not valid\"}")); + .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\":\"not valid\"}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("A non-integer schemaVersion should throw an error"); } catch (UnknownManifestFormatException ex) { - Assert.assertEquals("`schemaVersion` field is not an integer", ex.getMessage()); + Assert.assertEquals("'schemaVersion' field is not an integer", ex.getMessage()); } } @Test public void testHandleResponse_unknownSchemaVersion() throws IOException { - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("{\"schemaVersion\":0}")); + Mockito.when(mockResponse.getBody()) + .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\":0}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An unknown manifest schemaVersion should throw an error"); @@ -117,6 +249,86 @@ public void testHandleResponse_unknownSchemaVersion() throws IOException { } } + @Test + public void testHandleResponse_ociIndexWithNoMediaType() + throws IOException, UnknownManifestFormatException { + String ociManifestJson = + "{\n" + + " \"schemaVersion\": 2,\n" + + " \"manifests\": [\n" + + " {\n" + + " \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n" + + " \"size\": 7143,\n" + + " \"digest\": \"sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f\"\n" + + " }\n" + + " ]\n" + + "}"; + Mockito.when(mockResponse.getBody()).thenReturn(stringToInputStreamUtf8(ociManifestJson)); + + ManifestTemplate manifest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) + .handleResponse(mockResponse) + .getManifest(); + + MatcherAssert.assertThat(manifest, CoreMatchers.instanceOf(OciIndexTemplate.class)); + OciIndexTemplate ociIndex = (OciIndexTemplate) manifest; + + Assert.assertEquals("application/vnd.oci.image.index.v1+json", manifest.getManifestMediaType()); + Assert.assertEquals(1, ociIndex.getManifests().size()); + Assert.assertEquals( + "e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + ociIndex.getManifests().get(0).getDigest().getHash()); + } + + @Test + public void testHandleResponse_ociManfiestWithNoMediaType() + throws IOException, UnknownManifestFormatException { + String ociManifestJson = + "{\n" + + " \"schemaVersion\": 2,\n" + + " \"config\": {\n" + + " \"mediaType\": \"application/vnd.oci.image.config.v1+json\",\n" + + " \"size\": 7023,\n" + + " \"digest\": \"sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7\"\n" + + " },\n" + + " \"layers\": []\n" + + "}"; + Mockito.when(mockResponse.getBody()).thenReturn(stringToInputStreamUtf8(ociManifestJson)); + + ManifestTemplate manifest = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) + .handleResponse(mockResponse) + .getManifest(); + + MatcherAssert.assertThat(manifest, CoreMatchers.instanceOf(OciManifestTemplate.class)); + OciManifestTemplate ociManifest = (OciManifestTemplate) manifest; + + Assert.assertEquals( + "application/vnd.oci.image.manifest.v1+json", manifest.getManifestMediaType()); + Assert.assertEquals( + "b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", + ociManifest.getContainerConfiguration().getDigest().getHash()); + } + + @Test + public void testHandleResponse_invalidOciManfiest() throws IOException { + Mockito.when(mockResponse.getBody()) + .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\": 2}")); + + ManifestPuller manifestPuller = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class); + try { + manifestPuller.handleResponse(mockResponse); + Assert.fail(); + } catch (UnknownManifestFormatException ex) { + Assert.assertEquals( + "'schemaVersion' is 2, but neither 'manifests' nor 'config' exists", ex.getMessage()); + } + } + @Test public void testGetApiRoute() throws MalformedURLException { Assert.assertEquals( @@ -145,15 +357,17 @@ public void testGetContent() { public void testGetAccept() { Assert.assertEquals( Arrays.asList( - OCIManifestTemplate.MANIFEST_MEDIA_TYPE, + OciManifestTemplate.MANIFEST_MEDIA_TYPE, V22ManifestTemplate.MANIFEST_MEDIA_TYPE, - V21ManifestTemplate.MEDIA_TYPE), + V21ManifestTemplate.MEDIA_TYPE, + V22ManifestListTemplate.MANIFEST_MEDIA_TYPE, + OciIndexTemplate.MEDIA_TYPE), testManifestPuller.getAccept()); Assert.assertEquals( - Collections.singletonList(OCIManifestTemplate.MANIFEST_MEDIA_TYPE), + Collections.singletonList(OciManifestTemplate.MANIFEST_MEDIA_TYPE), new ManifestPuller<>( - fakeRegistryEndpointRequestProperties, "test-image-tag", OCIManifestTemplate.class) + fakeRegistryEndpointRequestProperties, "test-image-tag", OciManifestTemplate.class) .getAccept()); Assert.assertEquals( Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE), @@ -165,5 +379,17 @@ public void testGetAccept() { new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .getAccept()); + Assert.assertEquals( + Collections.singletonList(V22ManifestListTemplate.MANIFEST_MEDIA_TYPE), + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, + "test-image-tag", + V22ManifestListTemplate.class) + .getAccept()); + Assert.assertEquals( + Collections.singletonList(OciIndexTemplate.MEDIA_TYPE), + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", OciIndexTemplate.class) + .getAccept()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index f2de99e8a2..e5bd833c45 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,8 +16,13 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; @@ -30,10 +35,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import org.apache.http.HttpStatus; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -41,13 +52,16 @@ @RunWith(MockitoJUnitRunner.class) public class ManifestPusherTest { + @Mock private Response mockResponse; + @Mock private EventHandlers mockEventHandlers; + private Path v22manifestJsonFile; private V22ManifestTemplate fakeManifestTemplate; private ManifestPusher testManifestPusher; @Before public void setUp() throws URISyntaxException, IOException { - v22manifestJsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + v22manifestJsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); fakeManifestTemplate = JsonTemplateMapper.readJsonFromFile(v22manifestJsonFile, V22ManifestTemplate.class); @@ -55,7 +69,8 @@ public void setUp() throws URISyntaxException, IOException { new ManifestPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeManifestTemplate, - "test-image-tag"); + "test-image-tag", + mockEventHandlers); } @Test @@ -74,8 +89,46 @@ public void testGetContent() throws IOException { } @Test - public void testHandleResponse() { - Assert.assertNull(testManifestPusher.handleResponse(Mockito.mock(Response.class))); + public void testHandleResponse_valid() throws IOException { + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.singletonList(expectedDigest.toString())); + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + } + + @Test + public void testHandleResponse_noDigest() throws IOException { + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.emptyList()); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.warn("Expected image digest " + expectedDigest + ", but received none")); + } + + @Test + public void testHandleResponse_multipleDigests() throws IOException { + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Arrays.asList("too", "many")); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.warn("Expected image digest " + expectedDigest + ", but received: too, many")); + } + + @Test + public void testHandleResponse_invalidDigest() throws IOException { + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.singletonList("not valid")); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.warn("Expected image digest " + expectedDigest + ", but received: not valid")); } @Test @@ -101,4 +154,85 @@ public void testGetActionDescription() { public void testGetAccept() { Assert.assertEquals(0, testManifestPusher.getAccept().size()); } + + /** Docker Registry 2.0 and 2.1 return 400 / TAG_INVALID. */ + @Test + public void testHandleHttpResponseException_dockerRegistry_tagInvalid() throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"TAG_INVALID\"," + + "\"message\":\"manifest tag did not match URI\"}]}"); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); + } + } + + /** Docker Registry 2.2 returns a 400 / MANIFEST_INVALID. */ + @Test + public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() + throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"message\":\"manifest invalid\",\"detail\":{}}]}"); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); + } + } + + /** Quay.io returns an undocumented 415 / MANIFEST_INVALID. */ + @Test + public void testHandleHttpResponseException_quayIo() throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"detail\":{\"message\":\"manifest schema version not supported\"}," + + "\"message\":\"manifest invalid\"}]}"); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); + } + } + + @Test + public void testHandleHttpResponseException_otherError() throws RegistryErrorException { + ResponseException exception = Mockito.mock(ResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (ResponseException ex) { + Assert.assertSame(exception, ex); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java new file mode 100644 index 0000000000..f006c04ac0 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.GenericUrl; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Request; +import com.google.cloud.tools.jib.http.Response; +import java.io.IOException; +import java.net.URL; + +/** Forces sending all requests in plain-HTTP protocol. For testing only. */ +class PlainHttpClient extends FailoverHttpClient { + + PlainHttpClient() { + super(true, true, ignored -> {}); + } + + @Override + public Response call(String httpMethod, URL url, Request request) throws IOException { + GenericUrl httpUrl = new GenericUrl(url); + httpUrl.setScheme("http"); + return super.call(httpMethod, httpUrl.toURL(), request); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java new file mode 100644 index 0000000000..4c78c26f8b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.common.collect.Sets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RegistryAliasGroup}. */ +public class RegistryAliasGroupTest { + + @Test + public void testGetAliasesGroup_noKnownAliases() { + List singleton = RegistryAliasGroup.getAliasesGroup("something.gcr.io"); + Assert.assertEquals(1, singleton.size()); + Assert.assertEquals("something.gcr.io", singleton.get(0)); + } + + @Test + public void testGetAliasesGroup_dockerHub() { + Set aliases = + Sets.newHashSet( + "registry.hub.docker.com", "index.docker.io", "registry-1.docker.io", "docker.io"); + for (String alias : aliases) { + Assert.assertEquals(aliases, new HashSet<>(RegistryAliasGroup.getAliasesGroup(alias))); + } + } + + @Test + public void testGetHost_noAlias() { + String host = RegistryAliasGroup.getHost("something.gcr.io"); + Assert.assertEquals("something.gcr.io", host); + } + + @Test + public void testGetHost_dockerIo() { + String host = RegistryAliasGroup.getHost("docker.io"); + Assert.assertEquals("registry-1.docker.io", host); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java new file mode 100644 index 0000000000..cba6cd769a --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RegistryAuthenticationFailedException}. */ +public class RegistryAuthenticationFailedExceptionTest { + + @Test + public void testRegistryAuthenticationFailedException_message() { + RegistryAuthenticationFailedException exception = + new RegistryAuthenticationFailedException("serverUrl", "imageName", "message"); + Assert.assertEquals("serverUrl", exception.getServerUrl()); + Assert.assertEquals("imageName", exception.getImageName()); + Assert.assertEquals( + "Failed to authenticate with registry serverUrl/imageName because: message", + exception.getMessage()); + } + + @Test + public void testRegistryAuthenticationFailedException_exception() { + Throwable cause = new Throwable("message"); + RegistryAuthenticationFailedException exception = + new RegistryAuthenticationFailedException("serverUrl", "imageName", cause); + Assert.assertEquals("serverUrl", exception.getServerUrl()); + Assert.assertEquals("imageName", exception.getImageName()); + Assert.assertSame(cause, exception.getCause()); + Assert.assertEquals( + "Failed to authenticate with registry serverUrl/imageName because: message", + exception.getMessage()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 536922357d..0ce5be3dba 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,62 +16,193 @@ package com.google.cloud.tools.jib.registry; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.cloud.tools.jib.http.TestWebServer; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Collections; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link RegistryAuthenticator}. */ +@RunWith(MockitoJUnitRunner.class) public class RegistryAuthenticatorTest { + private final RegistryEndpointRequestProperties registryEndpointRequestProperties = + new RegistryEndpointRequestProperties("someserver", "someimage"); + + @Mock private FailoverHttpClient httpClient; + @Mock private Response response; + + @Captor private ArgumentCaptor urlCaptor; + + private RegistryAuthenticator registryAuthenticator; + + @Before + public void setUp() throws RegistryAuthenticationFailedException, IOException { + registryAuthenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient) + .get(); + + ByteArrayInputStream tokenJson = + new ByteArrayInputStream("{\"token\":\"my_token\"}".getBytes(StandardCharsets.UTF_8)); + Mockito.when(response.getBody()).thenReturn(tokenJson); + Mockito.when(httpClient.call(Mockito.any(), urlCaptor.capture(), Mockito.any())) + .thenReturn(response); + } @Test public void testFromAuthenticationMethod_bearer() throws MalformedURLException, RegistryAuthenticationFailedException { RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage"); - Assert.assertEquals( - new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient) + .get(); + assertThat( + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))) + .isEqualTo( + new URL("https://somerealm?service=someservice&scope=repository:someimage:scope")); registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage"); + "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient) + .get(); + assertThat( + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))) + .isEqualTo( + new URL("https://somerealm?service=someservice&scope=repository:someimage:scope")); + } + + @Test + public void testAuthRequestParameters_basicAuth() { + Assert.assertEquals( + "service=someservice&scope=repository:someimage:scope", + registryAuthenticator.getAuthRequestParameters( + null, Collections.singletonMap("someimage", "scope"))); + } + + @Test + public void testAuthRequestParameters_oauth2() { + Credential credential = Credential.from("", "oauth2_access_token"); + Assert.assertEquals( + "service=someservice&scope=repository:someimage:scope" + + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + + "&grant_type=refresh_token&refresh_token=oauth2_access_token", + registryAuthenticator.getAuthRequestParameters( + credential, Collections.singletonMap("someimage", "scope"))); + } + + @Test + public void isOAuth2Auth_nullCredential() { + Assert.assertFalse(registryAuthenticator.isOAuth2Auth(null)); + } + + @Test + public void isOAuth2Auth_basicAuth() { + Credential credential = Credential.from("name", "password"); + Assert.assertFalse(registryAuthenticator.isOAuth2Auth(credential)); + } + + @Test + public void isOAuth2Auth_oauth2() { + Credential credential = Credential.from("", "oauth2_token"); + Assert.assertTrue(registryAuthenticator.isOAuth2Auth(credential)); + } + + @Test + public void getAuthenticationUrl_basicAuth() throws MalformedURLException { Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); + } + + @Test + public void istAuthenticationUrl_oauth2() throws MalformedURLException { + Credential credential = Credential.from("", "oauth2_token"); + Assert.assertEquals( + new URL("https://somerealm"), + registryAuthenticator.getAuthenticationUrl(credential, Collections.emptyMap())); } @Test public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFailedException { - Assert.assertNull( - RegistryAuthenticator.fromAuthenticationMethod( - "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + assertThat( + RegistryAuthenticator.fromAuthenticationMethod( + "Basic", registryEndpointRequestProperties, "user-agent", httpClient)) + .isEmpty(); - Assert.assertNull( - RegistryAuthenticator.fromAuthenticationMethod( - "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + assertThat( + RegistryAuthenticator.fromAuthenticationMethod( + "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient)) + .isEmpty(); - Assert.assertNull( - RegistryAuthenticator.fromAuthenticationMethod( - "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + assertThat( + RegistryAuthenticator.fromAuthenticationMethod( + "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient)) + .isEmpty(); + + assertThat( + RegistryAuthenticator.fromAuthenticationMethod( + "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient)) + .isEmpty(); } @Test public void testFromAuthenticationMethod_noBearer() { try { RegistryAuthenticator.fromAuthenticationMethod( - "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", "someimage"); + "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient); Assert.fail("Authentication method without 'Bearer ' or 'Basic ' should fail"); } catch (RegistryAuthenticationFailedException ex) { Assert.assertEquals( - "Failed to authenticate with the registry because: 'Bearer' was not found in the 'WWW-Authenticate' header, tried to parse: realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + "Failed to authenticate with registry someserver/someimage because: 'Bearer' was not found in the 'WWW-Authenticate' header, tried to parse: realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", ex.getMessage()); } } @@ -79,27 +210,144 @@ public void testFromAuthenticationMethod_noBearer() { @Test public void testFromAuthenticationMethod_noRealm() { try { - RegistryAuthenticator.fromAuthenticationMethod("Bearer scope=\"somescope\"", "someimage"); + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient); Assert.fail("Authentication method without 'realm' should fail"); } catch (RegistryAuthenticationFailedException ex) { Assert.assertEquals( - "Failed to authenticate with the registry because: 'realm' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer scope=\"somescope\"", + "Failed to authenticate with registry someserver/someimage because: 'realm' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer scope=\"somescope\"", ex.getMessage()); } } @Test - public void testFromAuthenticationMethod_noService() { - try { - RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\"", "someimage"); - Assert.fail("Authentication method without 'service' should fail"); + public void testFromAuthenticationMethod_noService() + throws MalformedURLException, RegistryAuthenticationFailedException { + RegistryAuthenticator registryAuthenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://somerealm\"", + registryEndpointRequestProperties, + "user-agent", + httpClient) + .get(); - } catch (RegistryAuthenticationFailedException ex) { + Assert.assertEquals( + new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); + } + + @Test + public void testUserAgent() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryCredentialsNotSentException { + try (TestWebServer server = new TestWebServer(false)) { + try { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"" + server.getEndpoint() + "\"", + registryEndpointRequestProperties, + "Competent-Agent", + new FailoverHttpClient(true, false, ignored -> {})) + .get(); + authenticator.authenticatePush(null); + } catch (RegistryAuthenticationFailedException ex) { + // Doesn't matter if auth fails. We only examine what we sent. + } + MatcherAssert.assertThat( + server.getInputRead(), CoreMatchers.containsString("User-Agent: Competent-Agent")); + } + } + + @Test + public void testSourceImage_differentSourceRepository() + throws RegistryCredentialsNotSentException, RegistryAuthenticationFailedException { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://1.2.3.4:5\"", + new RegistryEndpointRequestProperties("someserver", "someimage", "anotherimage"), + "Competent-Agent", + httpClient) + .get(); + authenticator.authenticatePush(null); + MatcherAssert.assertThat( + urlCaptor.getValue().toString(), + CoreMatchers.endsWith( + "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull")); + } + + @Test + public void testSourceImage_sameSourceRepository() + throws RegistryCredentialsNotSentException, RegistryAuthenticationFailedException { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://1.2.3.4:5\"", + new RegistryEndpointRequestProperties("someserver", "someimage", "someimage"), + "Competent-Agent", + httpClient) + .get(); + authenticator.authenticatePush(null); + MatcherAssert.assertThat( + urlCaptor.getValue().toString(), + CoreMatchers.endsWith("service=someserver&scope=repository:someimage:pull,push")); + } + + @Test + public void testAuthorizationCleared() throws RegistryAuthenticationFailedException, IOException { + ResponseException responseException = Mockito.mock(ResponseException.class); + Mockito.when(responseException.getStatusCode()).thenReturn(401); + Mockito.when(responseException.requestAuthorizationCleared()).thenReturn(true); + Mockito.when(httpClient.call(Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(responseException); + + try { + registryAuthenticator.authenticatePush(null); + Assert.fail(); + } catch (RegistryCredentialsNotSentException ex) { Assert.assertEquals( - "Failed to authenticate with the registry because: 'service' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer realm=\"https://somerealm\"", + "Required credentials for someserver/someimage were not sent because the connection was " + + "over HTTP", ex.getMessage()); } } + + @Test + public void testAuthenticationResponseTemplate_readsToken() throws IOException { + String input = "{\"token\":\"test_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_readsAccessToken() throws IOException { + String input = "{\"access_token\":\"test_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_prefersToken() throws IOException { + String input = "{\"token\":\"test_value\",\"access_token\":\"wrong_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_acceptsNull() throws IOException { + String input = "{}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertNull(template.getToken()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 6dd4392329..97f72cda04 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,37 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.TestWebServer; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** @@ -30,55 +55,364 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { - @Mock private Authorization mockAuthorization; + @Mock private EventHandlers eventHandlers; - private RegistryClient.Factory testRegistryClientFactory; + private DescriptorDigest digest; + + private TestWebServer registry; + private TestWebServer authServer; @Before - public void setUp() { - testRegistryClientFactory = RegistryClient.factory("some.server.url", "some image name"); + public void setUp() throws DigestException { + digest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + } + + @After + public void tearDown() throws IOException { + if (registry != null) { + registry.close(); + } + if (authServer != null) { + authServer.close(); + } + } + + @Test + public void testDoBearerAuth_returnsFalseOnBasicAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String basicAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Basic foo\n\n"; + registry = new TestWebServer(false, Arrays.asList(basicAuth), 1); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertFalse(registryClient.doPullBearerAuth()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("server requires basic auth")); + } + + @Test + public void testDoBearerAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + setUpAuthServerAndRegistry(1, "HTTP/1.1 200 OK\nContent-Length: 1234\n\n"); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(1234, digestAndSize.get().getSize()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded")); + } + + @Test + public void testAutomaticTokenRefresh() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + setUpAuthServerAndRegistry(3, "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + // Verify authServer returned bearer token three times (i.e., refreshed twice) + Assert.assertEquals(3, authServer.getTotalResponsesServed()); + Assert.assertEquals(4, registry.getTotalResponsesServed()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded")); + Mockito.verify(eventHandlers, Mockito.times(2)) + .dispatch(logContains("refreshing bearer auth token")); + } + + @Test + public void testAutomaticTokenRefresh_badWwwAuthenticateResponse() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 3); + + List responses = + Arrays.asList( + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n", + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Basic realm=foo\n\n", + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\n\n", + "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"); + registry = new TestWebServer(false, responses, responses.size(), true); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + // Verify authServer returned bearer token three times (i.e., refreshed twice) + Assert.assertEquals(3, authServer.getTotalResponsesServed()); + Assert.assertEquals(4, registry.getTotalResponsesServed()); + + Mockito.verify(eventHandlers) + .dispatch( + logContains("server did not return 'WWW-Authenticate: Bearer' header. Actual: Basic")); + Mockito.verify(eventHandlers) + .dispatch( + logContains("server did not return 'WWW-Authenticate: Bearer' header. Actual: null")); + } + + @Test + public void testAutomaticTokenRefresh_refreshLimit() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 5); + + String bearerAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n"; + String unauthorized = "HTTP/1.1 401 Unauthorized\nContent-Length: 0\n\n"; + List responses = + Arrays.asList( + bearerAuth, unauthorized, unauthorized, unauthorized, unauthorized, unauthorized); + registry = new TestWebServer(false, responses, responses.size(), true); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + try { + registryClient.checkBlob(digest); + Assert.fail("Should have given up refreshing after 4 attempts"); + } catch (RegistryUnauthorizedException ex) { + Assert.assertEquals(401, ex.getHttpResponseException().getStatusCode()); + Assert.assertEquals(5, authServer.getTotalResponsesServed()); + // 1 response asking to do bearer auth + 4 unauth responses for 4 refresh attempts + 1 final + // unauth response propagated as RegistryUnauthorizedException here + Assert.assertEquals(6, registry.getTotalResponsesServed()); + } + } + + @Test + public void testConfigureBasicAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String basicAuth = "HTTP/1.1 200 OK\nContent-Length: 56789\n\n"; + registry = new TestWebServer(false, Arrays.asList(basicAuth), 1); + RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass")); + registryClient.configureBasicAuth(); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(56789, digestAndSize.get().getSize()); + MatcherAssert.assertThat( + registry.getInputRead(), CoreMatchers.containsString("Authorization: Basic dXNlcjpwYXNz")); + } + + @Test + public void testAuthPullByWwwAuthenticate_bearerAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 1); + + String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"; + registry = new TestWebServer(false, Arrays.asList(blobResponse), 1); + + RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass")); + registryClient.authPullByWwwAuthenticate("Bearer realm=\"" + authServer.getEndpoint() + "\""); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded")); + } + + @Test + public void testAuthPullByWwwAuthenticate_basicAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"; + registry = new TestWebServer(false, Arrays.asList(blobResponse), 1); + + RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass")); + registryClient.authPullByWwwAuthenticate("Basic foo"); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + MatcherAssert.assertThat( + registry.getInputRead(), CoreMatchers.containsString("Authorization: Basic dXNlcjpwYXNz")); + } + + @Test + public void testAuthPullByWwwAuthenticate_basicAuthRequestedButNullCredential() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"; + registry = new TestWebServer(false, Arrays.asList(blobResponse), 1); + + RegistryClient registryClient = createRegistryClient(null); + registryClient.authPullByWwwAuthenticate("Basic foo"); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + MatcherAssert.assertThat( + registry.getInputRead(), CoreMatchers.not(CoreMatchers.containsString("Authorization:"))); + } + + @Test + public void testAuthPullByWwwAuthenticate_basicAuthRequestedButOAuth2Credential() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"; + registry = new TestWebServer(false, Arrays.asList(blobResponse), 1); + + Credential credential = Credential.from(Credential.OAUTH2_TOKEN_USER_NAME, "pass"); + Assert.assertTrue(credential.isOAuth2RefreshToken()); + RegistryClient registryClient = createRegistryClient(credential); + registryClient.authPullByWwwAuthenticate("Basic foo"); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + MatcherAssert.assertThat( + registry.getInputRead(), CoreMatchers.not(CoreMatchers.containsString("Authorization:"))); } @Test - public void testGetUserAgent_null() { - Assert.assertTrue( - testRegistryClientFactory - .setAuthorization(mockAuthorization) - .newRegistryClient() - .getUserAgent() - .startsWith("jib")); - - RegistryClient.setUserAgentSuffix(null); - Assert.assertTrue( - testRegistryClientFactory - .setAuthorization(mockAuthorization) - .newRegistryClient() - .getUserAgent() - .startsWith("jib")); + public void testAuthPullByWwwAuthenticate_invalidAuthMethod() { + RegistryClient registryClient = + RegistryClient.factory(eventHandlers, "server", "foo/bar", null).newRegistryClient(); + try { + registryClient.authPullByWwwAuthenticate("invalid WWW-Authenticate"); + Assert.fail(); + } catch (RegistryException ex) { + Assert.assertEquals( + "Failed to authenticate with registry server/foo/bar because: 'Bearer' was not found in " + + "the 'WWW-Authenticate' header, tried to parse: invalid WWW-Authenticate", + ex.getMessage()); + } } @Test - public void testGetUserAgent() { - RegistryClient.setUserAgentSuffix("some user agent suffix"); - - Assert.assertTrue( - testRegistryClientFactory - .setAllowHttp(true) - .newRegistryClient() - .getUserAgent() - .startsWith("jib ")); - Assert.assertTrue( - testRegistryClientFactory - .setAllowHttp(true) - .newRegistryClient() - .getUserAgent() - .endsWith(" some user agent suffix")); + public void testPullManifest() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String manifestResponse = + "HTTP/1.1 200 OK\nContent-Length: 307\n\n{\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n" + + " \"config\": {\n" + + " \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n" + + " \"size\": 7023,\n" + + " \"digest\": \"sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7\"\n" + + " }\n" + + "}"; + + registry = new TestWebServer(false, Arrays.asList(manifestResponse), 1); + RegistryClient registryClient = createRegistryClient(null); + ManifestAndDigest manifestAndDigest = registryClient.pullManifest("image-tag"); + + Assert.assertEquals( + "sha256:6b61466eabab6e5ffb68ae2bd9b85c789225540c2ac54ea1f71eb327588e8946", + manifestAndDigest.getDigest().toString()); + + Assert.assertTrue(manifestAndDigest.getManifest() instanceof V22ManifestTemplate); + V22ManifestTemplate manifest = (V22ManifestTemplate) manifestAndDigest.getManifest(); + Assert.assertEquals(2, manifest.getSchemaVersion()); + Assert.assertEquals( + "application/vnd.docker.distribution.manifest.v2+json", manifest.getManifestMediaType()); + Assert.assertEquals( + "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", + manifest.getContainerConfiguration().getDigest().toString()); + Assert.assertEquals(7023, manifest.getContainerConfiguration().getSize()); + + MatcherAssert.assertThat( + registry.getInputRead(), + CoreMatchers.containsString("GET /v2/foo/bar/manifests/image-tag ")); } @Test - public void testGetApiRouteBase() { + public void testPullManifest_manifestList() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String manifestResponse = + "HTTP/1.1 200 OK\nContent-Length: 403\n\n{\n" + + " \"schemaVersion\": 2,\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.list.v2+json\",\n" + + " \"manifests\": [\n" + + " {\n" + + " \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n" + + " \"size\": 7143,\n" + + " \"digest\": \"sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f\",\n" + + " \"platform\": {\n" + + " \"architecture\": \"amd64\",\n" + + " \"os\": \"linux\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + registry = new TestWebServer(false, Arrays.asList(manifestResponse), 1); + RegistryClient registryClient = createRegistryClient(null); + ManifestAndDigest manifestAndDigest = registryClient.pullManifest("image-tag"); + Assert.assertEquals( - "some.server.url/v2/", - testRegistryClientFactory.setAllowHttp(true).newRegistryClient().getApiRouteBase()); + "sha256:a340fa38667f765f8cfd79d4bc684ec8a6f48cdd63abfe36e109f4125ee38488", + manifestAndDigest.getDigest().toString()); + + Assert.assertTrue(manifestAndDigest.getManifest() instanceof V22ManifestListTemplate); + V22ManifestListTemplate manifestList = + (V22ManifestListTemplate) manifestAndDigest.getManifest(); + Assert.assertEquals(2, manifestList.getSchemaVersion()); + Assert.assertEquals( + Arrays.asList("sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"), + manifestList.getDigestsForPlatform("amd64", "linux")); + + MatcherAssert.assertThat( + registry.getInputRead(), + CoreMatchers.containsString("GET /v2/foo/bar/manifests/image-tag ")); + } + + /** + * Sets up an auth server and a registry. The auth server can return a bearer token up to {@code + * maxAuthTokens} times. The registry will initially return "401 Unauthorized" for {@code + * maxTokenResponses} times. (Therefore, a registry client has to get auth tokens from the auth + * server {@code maxAuthTokens} times. After that, the registry returns {@code finalResponse}. + */ + private void setUpAuthServerAndRegistry(int maxAuthTokens, @Nullable String finalResponse) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), maxAuthTokens); + + String bearerAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n"; + List responses = new ArrayList<>(Collections.nCopies(maxAuthTokens, bearerAuth)); + if (finalResponse != null) { + responses.add(finalResponse); + } + + registry = new TestWebServer(false, responses, responses.size(), true); + } + + private RegistryClient createRegistryClient(@Nullable Credential credential) { + return RegistryClient.factory( + eventHandlers, "localhost:" + registry.getLocalPort(), "foo/bar", new PlainHttpClient()) + .setCredential(credential) + .newRegistryClient(); + } + + private LogEvent logContains(String substring) { + ArgumentMatcher matcher = event -> event.getMessage().contains(substring); + return ArgumentMatchers.argThat(matcher); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index d1601ccfcc..d9b85bc920 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,32 +16,41 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Request; +import com.google.cloud.tools.jib.http.RequestWrapper; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; -import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; +import com.google.cloud.tools.jib.http.ResponseException; +import com.google.common.io.CharStreams; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.net.MalformedURLException; +import java.net.SocketException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; -import java.util.function.Function; import javax.annotation.Nullable; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.NoHttpResponseException; -import org.apache.http.conn.HttpHostConnectException; import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -62,7 +71,7 @@ public String getHttpMethod() { @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { - return new URL(apiRouteBase + "/api"); + return new URL(apiRouteBase + "api"); } @Nullable @@ -79,62 +88,78 @@ public List getAccept() { @Nullable @Override public String handleResponse(Response response) throws IOException { - return Blobs.writeToString(response.getBody()); + return CharStreams.toString( + new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); } @Override public String getActionDescription() { return "actionDescription"; } + + @Override + public String handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } - @Mock private Connection mockConnection; + private static ResponseException mockResponseException(int statusCode) { + ResponseException mock = Mockito.mock(ResponseException.class); + Mockito.when(mock.getStatusCode()).thenReturn(statusCode); + return mock; + } + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + + @Mock private EventHandlers mockEventHandlers; + @Mock private FailoverHttpClient mockHttpClient; @Mock private Response mockResponse; - @Mock private Function mockConnectionFactory; - @Mock private HttpResponse mockHttpResponse; - private RegistryEndpointCaller testRegistryEndpointCallerSecure; + private RegistryEndpointCaller endpointCaller; @Before public void setUp() throws IOException { - testRegistryEndpointCallerSecure = + endpointCaller = new RegistryEndpointCaller<>( + mockEventHandlers, "userAgent", - "apiRouteBase", new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), + Authorization.fromBasicCredentials("user", "pass"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), - false, - mockConnectionFactory); + mockHttpClient); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockHttpResponse.parseAsString()).thenReturn(""); - Mockito.when(mockHttpResponse.getHeaders()).thenReturn(new HttpHeaders()); + Mockito.when(mockResponse.getBody()) + .thenReturn(new ByteArrayInputStream("body".getBytes(StandardCharsets.UTF_8))); } @Test - public void testCall_httpsPeerUnverified() throws IOException, RegistryException { - verifyRetriesWithHttp(SSLPeerUnverifiedException.class); - } + public void testCall_secureCallerOnUnverifiableServer() throws IOException, RegistryException { + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); - @Test - public void testCall_retryWithHttp() throws IOException, RegistryException { - verifyRetriesWithHttp(HttpHostConnectException.class); + try { + endpointCaller.call(); + Assert.fail("Should throw InsecureRegistryException when getting SSLException"); + } catch (InsecureRegistryException ex) { + Assert.assertEquals( + "Failed to verify the server at https://serverUrl/v2/api because only secure connections " + + "are allowed.", + ex.getMessage()); + } } @Test public void testCall_noHttpResponse() throws IOException, RegistryException { - NoHttpResponseException mockNoHttpResponseException = - Mockito.mock(NoHttpResponseException.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(mockNoHttpResponseException); + NoHttpResponseException mockNoResponseException = Mockito.mock(NoHttpResponseException.class); + setUpRegistryResponse(mockNoResponseException); try { - testRegistryEndpointCallerSecure.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); - } catch (RegistryNoResponseException ex) { - Assert.assertSame(mockNoHttpResponseException, ex.getCause()); + } catch (NoHttpResponseException ex) { + Assert.assertSame(mockNoResponseException, ex); } } @@ -143,6 +168,42 @@ public void testCall_unauthorized() throws IOException, RegistryException { verifyThrowsRegistryUnauthorizedException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); } + @Test + public void testCall_credentialsNotSentOverHttp() throws IOException, RegistryException { + ResponseException unauthorizedException = + mockResponseException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + Mockito.when(unauthorizedException.requestAuthorizationCleared()).thenReturn(true); + setUpRegistryResponse(unauthorizedException); + + try { + endpointCaller.call(); + Assert.fail("Call should have failed"); + + } catch (RegistryCredentialsNotSentException ex) { + Assert.assertEquals( + "Required credentials for serverUrl/imageName were not sent because the connection was over HTTP", + ex.getMessage()); + } + } + + @Test + public void testCall_credentialsForcedOverHttp() throws IOException, RegistryException { + ResponseException unauthorizedException = + mockResponseException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + setUpRegistryResponse(unauthorizedException); + System.setProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP, "true"); + + try { + endpointCaller.call(); + Assert.fail("Call should have failed"); + + } catch (RegistryCredentialsNotSentException ex) { + throw new AssertionError("should have sent credentials", ex); + } catch (RegistryUnauthorizedException ex) { + Assert.assertEquals("Unauthorized for serverUrl/imageName", ex.getMessage()); + } + } + @Test public void testCall_forbidden() throws IOException, RegistryException { verifyThrowsRegistryUnauthorizedException(HttpStatusCodes.STATUS_CODE_FORBIDDEN); @@ -165,84 +226,227 @@ public void testCall_methodNotAllowed() throws IOException, RegistryException { @Test public void testCall_unknown() throws IOException, RegistryException { - Mockito.when(mockHttpResponse.getStatusCode()) - .thenReturn(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + ResponseException responseException = + mockResponseException(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); + setUpRegistryResponse(responseException); try { - testRegistryEndpointCallerSecure.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); - } catch (HttpResponseException ex) { - Assert.assertSame(httpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertSame(responseException, ex); } } @Test - public void testCall_temporaryRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); + public void testCall_logErrorOnIoExceptions() throws IOException, RegistryException { + IOException ioException = new IOException("detailed exception message"); + setUpRegistryResponse(ioException); + + try { + endpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Assert.assertSame(ioException, ex); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m detailed exception message\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } } @Test - public void testCall_movedPermanently() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY); + public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryException { + IOException ioException = new IOException("this is due to broken pipe"); + setUpRegistryResponse(ioException); + + try { + endpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Assert.assertSame(ioException, ex); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m this is due to broken pipe\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error( + "\u001B[31;1mbroken pipe: the server shut down the connection. Check the server " + + "log if possible. This could also be a proxy issue. For example, a proxy " + + "may prevent sending packets that are too large.\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } } @Test - public void testCall_permanentRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); + public void testCall_logNullExceptionMessage() throws IOException, RegistryException { + setUpRegistryResponse(new IOException()); + + try { + endpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m (null exception message)\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } } @Test - public void testCall_disallowInsecure() throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. - Mockito.when(mockHttpResponse.getStatusCode()) - .thenReturn(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); - Mockito.when(mockHttpResponse.getHeaders()) - .thenReturn(new HttpHeaders().setLocation("http://newlocation")); - - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException) + public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); - try { - testRegistryEndpointCallerSecure.call(); - Assert.fail("Call should have failed"); + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + endpointCaller.call(); - } catch (InsecureRegistryException ex) { - // pass - } + // We fall back to the default timeout: + // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 + Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } - /** Verifies a request is retried with HTTP protocol if {@code exceptionClass} is thrown. */ - private void verifyRetriesWithHttp(Class exceptionClass) - throws IOException, RegistryException { - // Has mockConnection.send throw first, then succeed. - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(exceptionClass)) + @Test + public void testHttpTimeout_stringValue() throws IOException, RegistryException { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); - RegistryEndpointCaller testRegistryEndpointCallerInsecure = - new RegistryEndpointCaller<>( - "userAgent", - "apiRouteBase", - new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), - new RegistryEndpointRequestProperties("serverUrl", "imageName"), - true, - mockConnectionFactory); - Assert.assertEquals("body", testRegistryEndpointCallerInsecure.call()); - - // Checks that the URL protocol was first HTTPS, then HTTP. - ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlArgumentCaptor.capture()); - Assert.assertEquals("https", urlArgumentCaptor.getAllValues().get(0).getProtocol()); - Assert.assertEquals("http", urlArgumentCaptor.getAllValues().get(1).getProtocol()); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); + endpointCaller.call(); + + Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); + } + + @Test + public void testHttpTimeout_negativeValue() throws IOException, RegistryException { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) + .thenReturn(mockResponse); + + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-1"); + endpointCaller.call(); + + // We let the negative value pass through: + // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 + Assert.assertEquals(-1, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); + } + + @Test + public void testHttpTimeout_0accepted() throws IOException, RegistryException { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) + .thenReturn(mockResponse); + + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "0"); + endpointCaller.call(); + + Assert.assertEquals(0, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); + } + + @Test + public void testHttpTimeout() throws IOException, RegistryException { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) + .thenReturn(mockResponse); + + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "7593"); + endpointCaller.call(); + + Assert.assertEquals(7593, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); + } + + @Test + public void testIsBrokenPipe_notBrokenPipe() { + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new IOException())); + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new SocketException())); + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new SSLException("mock"))); + } + + @Test + public void testIsBrokenPipe_brokenPipe() { + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new IOException("cool broken pipe !"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new SocketException("BROKEN PIPE"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new SSLException("calm BrOkEn PiPe"))); + } + + @Test + public void testIsBrokenPipe_nestedBrokenPipe() { + IOException exception = new IOException(new SSLException(new SocketException("Broken pipe"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(exception)); + } + + @Test + public void testIsBrokenPipe_terminatesWhenCauseIsOriginal() { + IOException exception = Mockito.mock(IOException.class); + Mockito.when(exception.getCause()).thenReturn(exception); + + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(exception)); + } + + @Test + public void testNewRegistryErrorException_jsonErrorOutput() { + ResponseException httpException = Mockito.mock(ResponseException.class); + Mockito.when(httpException.getContent()) + .thenReturn( + "{\"errors\": [{\"code\": \"MANIFEST_UNKNOWN\", \"message\": \"manifest unknown\"}]}"); + + RegistryErrorException registryException = + endpointCaller.newRegistryErrorException(httpException); + Assert.assertSame(httpException, registryException.getCause()); + Assert.assertEquals( + "Tried to actionDescription but failed because: manifest unknown", + registryException.getMessage()); + } + + @Test + public void testNewRegistryErrorException_nonJsonErrorOutput() { + ResponseException httpException = Mockito.mock(ResponseException.class); + // Registry returning non-structured error output + Mockito.when(httpException.getContent()).thenReturn(">>>>> (404) page not found <<<<<"); + Mockito.when(httpException.getStatusCode()).thenReturn(404); + + RegistryErrorException registryException = + endpointCaller.newRegistryErrorException(httpException); + Assert.assertSame(httpException, registryException.getCause()); + Assert.assertEquals( + "Tried to actionDescription but failed because: registry returned error code 404; " + + "possible causes include invalid or wrong reference. Actual error output follows:\n" + + ">>>>> (404) page not found <<<<<\n", + registryException.getMessage()); + } + + @Test + public void testNewRegistryErrorException_noOutputFromRegistry() { + ResponseException httpException = Mockito.mock(ResponseException.class); + // Registry returning null error output + Mockito.when(httpException.getContent()).thenReturn(null); + Mockito.when(httpException.getStatusCode()).thenReturn(404); + + RegistryErrorException registryException = + endpointCaller.newRegistryErrorException(httpException); + Assert.assertSame(httpException, registryException.getCause()); + Assert.assertEquals( + "Tried to actionDescription but failed because: registry returned error code 404 " + + "but did not return any details; possible causes include invalid or wrong reference, or proxy/firewall/VPN interfering \n", + registryException.getMessage()); } /** @@ -251,20 +455,16 @@ private void verifyRetriesWithHttp(Class exceptionClass) */ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) throws IOException, RegistryException { - Mockito.when(mockHttpResponse.getStatusCode()).thenReturn(httpStatusCode); - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + ResponseException responseException = mockResponseException(httpStatusCode); + setUpRegistryResponse(responseException); try { - testRegistryEndpointCallerSecure.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryUnauthorizedException ex) { - Assert.assertEquals("serverUrl", ex.getRegistry()); - Assert.assertEquals("imageName", ex.getRepository()); - Assert.assertSame(httpResponseException, ex.getHttpResponseException()); + Assert.assertEquals("serverUrl/imageName", ex.getImageReference()); + Assert.assertSame(responseException, ex.getCause()); } } @@ -274,54 +474,30 @@ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) */ private void verifyThrowsRegistryErrorException(int httpStatusCode) throws IOException, RegistryException { - ErrorResponseTemplate errorResponseTemplate = - new ErrorResponseTemplate().addError(new ErrorEntryTemplate("code", "message")); - - Mockito.when(mockHttpResponse.getStatusCode()).thenReturn(httpStatusCode); - Mockito.when(mockHttpResponse.parseAsString()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(errorResponseTemplate))); - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + ResponseException errorResponse = mockResponseException(httpStatusCode); + Mockito.when(errorResponse.getContent()) + .thenReturn("{\"errors\":[{\"code\":\"code\",\"message\":\"message\"}]}"); + setUpRegistryResponse(errorResponse); try { - testRegistryEndpointCallerSecure.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryErrorException ex) { - Assert.assertThat( + MatcherAssert.assertThat( ex.getMessage(), - CoreMatchers.containsString( - "Tried to actionDescription but failed because: unknown: message")); + CoreMatchers.startsWith( + "Tried to actionDescription but failed because: unknown error code: code (message)")); } } - /** - * Verifies that a response with {@code httpStatusCode} retries the request with the {@code - * Location} header. - */ - private void verifyRetriesWithNewLocation(int httpStatusCode) - throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. - Mockito.when(mockHttpResponse.getStatusCode()).thenReturn(httpStatusCode); - Mockito.when(mockHttpResponse.getHeaders()) - .thenReturn(new HttpHeaders().setLocation("https://newlocation")); - - // Has mockConnection.send throw first, then succeed. - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException) - .thenReturn(mockResponse); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); - - Assert.assertEquals("body", testRegistryEndpointCallerSecure.call()); - - // Checks that the URL was changed to the new location. - ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlArgumentCaptor.capture()); - Assert.assertEquals( - new URL("https://apiRouteBase/api"), urlArgumentCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("https://newlocation"), urlArgumentCaptor.getAllValues().get(1)); + private void setUpRegistryResponse(Exception exceptionToThrow) + throws MalformedURLException, IOException { + Mockito.when( + mockHttpClient.call( + Mockito.eq("httpMethod"), + Mockito.eq(new URL("https://serverUrl/v2/api")), + Mockito.any())) + .thenThrow(exceptionToThrow); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java index cafc90c7b7..17d8895be2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -33,26 +33,29 @@ public class RegistryErrorExceptionBuilderTest { @Test public void testAddErrorEntry() { RegistryErrorExceptionBuilder builder = - new RegistryErrorExceptionBuilder("do something", mockHttpResponseException); - - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_INVALID.name(), "manifest invalid")); - builder.addReason(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "blob unknown")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "manifest unknown")); - builder.addReason(new ErrorEntryTemplate(ErrorCodes.TAG_INVALID.name(), "tag invalid")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNVERIFIED.name(), "manifest unverified")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.UNSUPPORTED.name(), "some other error happened")); - builder.addReason(new ErrorEntryTemplate("unknown", "some unknown error happened")); + new RegistryErrorExceptionBuilder("do something", mockHttpResponseException) + .addReason( + new ErrorEntryTemplate(ErrorCodes.MANIFEST_INVALID.name(), "manifest invalid")) + .addReason(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "blob unknown")) + .addReason( + new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "manifest unknown")) + .addReason(new ErrorEntryTemplate(ErrorCodes.TAG_INVALID.name(), "tag invalid")) + .addReason( + new ErrorEntryTemplate( + ErrorCodes.MANIFEST_UNVERIFIED.name(), "manifest unverified")) + .addReason( + new ErrorEntryTemplate(ErrorCodes.UNSUPPORTED.name(), "some other error happened")) + .addReason(new ErrorEntryTemplate("unknown", "some unknown error happened")); try { throw builder.build(); } catch (RegistryErrorException ex) { Assert.assertEquals( - "Tried to do something but failed because: manifest invalid (something went wrong), blob unknown (something went wrong), manifest unknown, tag invalid, manifest unverified, other: some other error happened, unknown: some unknown error happened | If this is a bug, please file an issue at https://github.com/GoogleContainerTools/jib/issues/new", + "Tried to do something but failed because: manifest invalid (something went wrong), blob " + + "unknown (something went wrong), manifest unknown, tag invalid, manifest " + + "unverified, other: some other error happened, unknown error code: unknown (some " + + "unknown error happened)", ex.getMessage()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 256e3384e0..509ed28ed4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,15 @@ package com.google.cloud.tools.jib.registry.credentials; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; +import java.util.function.Consumer; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -34,88 +37,202 @@ @RunWith(MockitoJUnitRunner.class) public class DockerConfigCredentialRetrieverTest { - @Mock private Authorization mockAuthorization; - @Mock private DockerCredentialHelper mockDockerCredentialHelper; + private static final Credential FAKE_CREDENTIAL = Credential.from("username", "password"); - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private DockerCredentialHelper mockDockerCredentialHelper; + @Mock private DockerConfig mockDockerConfig; + @Mock private Consumer mockLogger; private Path dockerConfigFile; @Before public void setUp() - throws URISyntaxException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { - dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(mockAuthorization); + throws URISyntaxException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException, IOException { + dockerConfigFile = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); } @Test - public void testRetrieve_nonexistentDockerConfigFile() throws IOException { + public void testRetrieve_nonExistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); + DockerConfigCredentialRetriever.create("some registry", Paths.get("fake/path")); - Assert.assertNull(dockerConfigCredentialRetriever.retrieve()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", dockerConfigFile, null); + DockerConfigCredentialRetriever.create("some other registry", dockerConfigFile); + + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("some", credentials.get().getUsername()); + Assert.assertEquals("other:auth", credentials.get().getPassword()); + Mockito.verify(mockLogger) + .accept( + LogEvent.info( + "Docker config auths section defines credentials for some other registry")); + } - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertNotNull(authorization); - Assert.assertEquals("some auth", authorization.getToken()); + @Test + public void testRetrieve_authTakesPrecedenceOverUsernameAndPassword() throws IOException { + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + DockerConfigCredentialRetriever.create("auth and userpw registry", dockerConfigFile); + + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("some", credentials.get().getUsername()); + Assert.assertEquals("auth", credentials.get().getPassword()); + Mockito.verify(mockLogger) + .accept( + LogEvent.info( + "Docker config auths section defines credentials for auth and userpw registry")); } @Test - public void testRetrieve_useCredsStore() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("some credential store")) - .thenReturn(mockDockerCredentialHelper); + public void testRetrieve_hasAuthWithUsernameAndPassword() throws IOException { + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + DockerConfigCredentialRetriever.create("userpw registry", dockerConfigFile); + + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("someuser", credentials.get().getUsername()); + Assert.assertEquals("somepw", credentials.get().getPassword()); + Mockito.verify(mockLogger) + .accept( + LogEvent.info( + "Docker config auths section defines username and password for userpw registry")); + } + @Test + public void testRetrieve_hasAuth_legacyConfigFormat() throws IOException, URISyntaxException { + dockerConfigFile = Paths.get(Resources.getResource("core/json/legacy_dockercfg").toURI()); + + DockerConfigCredentialRetriever retriever1 = + DockerConfigCredentialRetriever.createForLegacyFormat("some registry", dockerConfigFile); + Optional credentials1 = retriever1.retrieve(mockLogger); + Assert.assertEquals("some", credentials1.get().getUsername()); + Assert.assertEquals("other:auth", credentials1.get().getPassword()); + + DockerConfigCredentialRetriever retriever2 = + DockerConfigCredentialRetriever.createForLegacyFormat("example.com", dockerConfigFile); + Optional credentials2 = retriever2.retrieve(mockLogger); + Assert.assertEquals("user", credentials2.get().getUsername()); + Assert.assertEquals("pass", credentials2.get().getPassword()); + + Mockito.verify(mockLogger) + .accept(LogEvent.info("Docker config auths section defines credentials for some registry")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Docker config auths section defines credentials for example.com")); + } + + @Test + public void testRetrieve_credentialHelperTakesPrecedenceOverAuth() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("some registry")) + .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.getCredentialHelper()) + .thenReturn(Paths.get("docker-credential-foo")); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "just registry", dockerConfigFile, mockDockerCredentialHelperFactory); + DockerConfigCredentialRetriever.create("some registry", dockerConfigFile); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertEquals( + Optional.of(FAKE_CREDENTIAL), + dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger)); + Mockito.verify(mockLogger) + .accept(LogEvent.info("trying docker-credential-foo for some registry")); } @Test - public void testRetrieve_useCredsStore_withProtocol() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("some credential store")) + public void testRetrieve_credentialHelper_warn() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + Mockito.when(mockDockerConfig.getCredentialHelperFor("another registry")) .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.retrieve()) + .thenThrow( + new CredentialHelperNotFoundException( + Paths.get("docker-credential-path"), new Throwable("cause"))); + + DockerConfigCredentialRetriever.create("another registry", dockerConfigFile) + .retrieve(mockDockerConfig, mockLogger); + Mockito.verify(mockLogger) + .accept(LogEvent.warn("The system does not have docker-credential-path CLI")); + Mockito.verify(mockLogger).accept(LogEvent.warn(" Caused by: cause")); + } + + @Test + public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "with.protocol", dockerConfigFile, mockDockerCredentialHelperFactory); + DockerConfigCredentialRetriever.create("unknown registry", dockerConfigFile); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test - public void testRetrieve_useCredHelper() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix( - "another credential helper")) + public void testRetrieve_credentialFromAlias() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("registry.hub.docker.com")) .thenReturn(mockDockerCredentialHelper); + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + DockerConfigCredentialRetriever.create("registry.hub.docker.com", dockerConfigFile); + + Assert.assertEquals( + Optional.of(FAKE_CREDENTIAL), + dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger)); + } + + @Test + public void testRetrieve_suffixMatching() throws IOException, URISyntaxException { + Path dockerConfigFile = + Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "another registry", dockerConfigFile, mockDockerCredentialHelperFactory); + DockerConfigCredentialRetriever.create("index.docker.io", dockerConfigFile); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("token for", credentials.get().getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); } @Test - public void testRetrieve_none() throws IOException { + public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntaxException { + Path dockerConfigFile = + Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); + + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + DockerConfigCredentialRetriever.create("registry.hub.docker.com", dockerConfigFile); + + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("token for", credentials.get().getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); + } + + @Test + public void testRetrieve_azureIdentityToken() throws IOException, URISyntaxException { + Path dockerConfigFile = + Paths.get(Resources.getResource("core/json/dockerconfig_identity_token.json").toURI()); + + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + DockerConfigCredentialRetriever.create("some registry", dockerConfigFile); + + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("", credentials.get().getUsername()); + Assert.assertEquals("cool identity token", credentials.get().getPassword()); + } + + @Test + public void testRetrieve_noErrorWhenMissingAuthField() throws IOException, URISyntaxException { + Path dockerConfigFile = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); + DockerConfigCredentialRetriever.create("no auth field", dockerConfigFile); - Assert.assertNull(dockerConfigCredentialRetriever.retrieve()); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); + Assert.assertFalse(credentials.isPresent()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java new file mode 100644 index 0000000000..3bd62ac404 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerConfig}. */ +public class DockerConfigTest { + + private static String decodeBase64(String base64String) { + return new String(Base64.getDecoder().decode(base64String), StandardCharsets.UTF_8); + } + + @Test + public void test_fromJson() throws URISyntaxException, IOException { + // Loads the JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + // Deserializes into a docker config JSON object. + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(jsonFile, DockerConfigTemplate.class)); + + Assert.assertEquals( + "some:auth", decodeBase64(dockerConfig.getAuthFor("some registry").getAuth())); + Assert.assertEquals( + "some:other:auth", decodeBase64(dockerConfig.getAuthFor("some other registry").getAuth())); + Assert.assertEquals("token", decodeBase64(dockerConfig.getAuthFor("registry").getAuth())); + Assert.assertEquals( + "token", decodeBase64(dockerConfig.getAuthFor("https://registry").getAuth())); + Assert.assertNull(dockerConfig.getAuthFor("just registry")); + + Assert.assertEquals( + Paths.get("docker-credential-some credential helper"), + dockerConfig.getCredentialHelperFor("some registry").getCredentialHelper()); + Assert.assertEquals( + Paths.get("docker-credential-another credential helper"), + dockerConfig.getCredentialHelperFor("another registry").getCredentialHelper()); + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("unknown registry").getCredentialHelper()); + } + + @Test + public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, IOException { + Path json = + Paths.get(Resources.getResource("core/json/dockerconfig_extra_matches.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertEquals( + "my-registry: exact match", dockerConfig.getAuthFor("my-registry").getAuth()); + Assert.assertEquals( + "cool-registry: with https", dockerConfig.getAuthFor("cool-registry").getAuth()); + Assert.assertEquals( + "awesome-registry: starting with name", + dockerConfig.getAuthFor("awesome-registry").getAuth()); + Assert.assertEquals( + "dull-registry: starting with name and with https", + dockerConfig.getAuthFor("dull-registry").getAuth()); + } + + @Test + public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IOException { + Path json = + Paths.get(Resources.getResource("core/json/dockerconfig_extra_matches.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertNull(dockerConfig.getAuthFor("example")); + } + + @Test + public void testGetCredentialHelperFor_exactMatchInCredHelpers() + throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertEquals( + Paths.get("docker-credential-credHelper for just.registry.in.helpers"), + dockerConfig.getCredentialHelperFor("just.registry.in.helpers").getCredentialHelper()); + } + + @Test + public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertEquals( + Paths.get("docker-credential-credHelper for https__with.protocol.in.helpers"), + dockerConfig.getCredentialHelperFor("with.protocol.in.helpers").getCredentialHelper()); + } + + @Test + public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertEquals( + Paths.get("docker-credential-credHelper for with.suffix.in.helpers/v2/"), + dockerConfig.getCredentialHelperFor("with.suffix.in.helpers").getCredentialHelper()); + } + + @Test + public void testGetCredentialHelperFor_withProtocolAndSuffix() + throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + Assert.assertEquals( + Paths.get( + "docker-credential-credHelper for https__with.protocol.and.suffix.in.helpers/suffix"), + dockerConfig + .getCredentialHelperFor("with.protocol.and.suffix.in.helpers") + .getCredentialHelper()); + } + + @Test + public void testGetCredentialHelperFor_correctSuffixMatching() + throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); + + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); + + // Should fall back to credsStore + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("example").getCredentialHelper()); + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("another.example").getCredentialHelper()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java new file mode 100644 index 0000000000..1f17aca613 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.function.Function; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DockerCredentialHelperTest { + + private static final String CREDENTIAL_JSON = + "{\"Username\":\"myusername\",\"Secret\":\"mysecret\"}"; + + @Mock private Process process; + @Mock private Function, ProcessBuilder> processBuilderFactory; + @Mock private ProcessBuilder processBuilder; + @Mock private ProcessBuilder errorProcessBuilder; + + private final Properties systemProperties = new Properties(); + + @Before + public void setUp() throws IOException { + systemProperties.put("os.name", "unknown"); + + Mockito.when(process.getInputStream()) + .thenReturn(new ByteArrayInputStream(CREDENTIAL_JSON.getBytes(StandardCharsets.UTF_8))); + Mockito.when(process.getOutputStream()).thenReturn(ByteStreams.nullOutputStream()); + Mockito.when(processBuilder.start()).thenReturn(process); + Mockito.when(errorProcessBuilder.start()) + .thenThrow(new IOException("No such file or directory")); + } + + @Test + public void testDockerCredentialsTemplate_read() throws IOException { + DockerCredentialHelper.DockerCredentialsTemplate template = + JsonTemplateMapper.readJson( + CREDENTIAL_JSON, DockerCredentialHelper.DockerCredentialsTemplate.class); + Assert.assertEquals("myusername", template.username); + Assert.assertEquals("mysecret", template.secret); + } + + @Test + public void testDockerCredentialsTemplate_canReadNull() throws IOException { + String input = "{}"; + DockerCredentialHelper.DockerCredentialsTemplate template = + JsonTemplateMapper.readJson(input, DockerCredentialHelper.DockerCredentialsTemplate.class); + Assert.assertNull(template.username); + Assert.assertNull(template.secret); + } + + @Test + public void testRetrieve() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + List command = Arrays.asList(Paths.get("/foo/bar").toString(), "get"); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/foo/bar"), systemProperties, processBuilderFactory); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(command); + } + + @Test + public void testRetrieveWithEnvironment() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + List command = Arrays.asList(Paths.get("/foo/bar").toString(), "get"); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + Map processBuilderEnvironment = Mockito.spy(new HashMap<>()); + Mockito.when(processBuilder.environment()).thenReturn(processBuilderEnvironment); + + Map credHelperEnvironment = ImmutableMap.of("ENV_VARIABLE", "Value"); + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", + Paths.get("/foo/bar"), + systemProperties, + processBuilderFactory, + credHelperEnvironment); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(command); + Mockito.verify(processBuilderEnvironment).putAll(credHelperEnvironment); + Assert.assertEquals(1, processBuilderEnvironment.size()); + Assert.assertEquals("Value", processBuilderEnvironment.get("ENV_VARIABLE")); + } + + @Test + public void testRetrieve_cmdSuffixAddedOnWindows() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + systemProperties.setProperty("os.name", "WINdows"); + List command = Arrays.asList(Paths.get("/foo/bar.cmd").toString(), "get"); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/foo/bar"), systemProperties, processBuilderFactory); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(command); + } + + @Test + public void testRetrieve_cmdSuffixAlreadyGivenOnWindows() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + systemProperties.setProperty("os.name", "WINdows"); + List command = Arrays.asList(Paths.get("/foo/bar.CmD").toString(), "get"); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/foo/bar.CmD"), systemProperties, processBuilderFactory); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(command); + } + + @Test + public void testRetrieve_exeSuffixAlreadyGivenOnWindows() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + systemProperties.setProperty("os.name", "WINdows"); + List command = Arrays.asList(Paths.get("/foo/bar.eXE").toString(), "get"); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/foo/bar.eXE"), systemProperties, processBuilderFactory); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(command); + } + + @Test + public void testRetrieve_cmdSuffixNotFoundOnWindows() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + systemProperties.setProperty("os.name", "WINdows"); + List errorCmdCommand = Arrays.asList(Paths.get("/foo/bar.cmd").toString(), "get"); + List errorExeCommand = Arrays.asList(Paths.get("/foo/bar.exe").toString(), "get"); + List command = Arrays.asList(Paths.get("/foo/bar").toString(), "get"); + Mockito.when(processBuilderFactory.apply(errorCmdCommand)).thenReturn(errorProcessBuilder); + Mockito.when(processBuilderFactory.apply(errorExeCommand)).thenReturn(errorProcessBuilder); + Mockito.when(processBuilderFactory.apply(command)).thenReturn(processBuilder); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/foo/bar"), systemProperties, processBuilderFactory); + Credential credential = credentialHelper.retrieve(); + Assert.assertEquals("myusername", credential.getUsername()); + Assert.assertEquals("mysecret", credential.getPassword()); + + Mockito.verify(processBuilderFactory).apply(errorCmdCommand); + Mockito.verify(processBuilderFactory).apply(errorExeCommand); + Mockito.verify(processBuilderFactory).apply(command); + } + + @Test + public void testRetrieve_fileNotFoundExceptionMessage() + throws CredentialHelperUnhandledServerUrlException, IOException { + Mockito.when(processBuilderFactory.apply(Mockito.any())).thenReturn(processBuilder); + Mockito.when(processBuilder.start()) + .thenThrow( + new IOException( + "CreateProcess error=2, Das System kann die angegebene Datei nicht finden")); + + DockerCredentialHelper credentialHelper = + dockerCredentialHelper( + "serverUrl", Paths.get("/ignored"), systemProperties, processBuilderFactory); + try { + credentialHelper.retrieve(); + Assert.fail(); + } catch (CredentialHelperNotFoundException ex) { + Assert.assertNotNull(ex.getMessage()); + } + } + + private DockerCredentialHelper dockerCredentialHelper( + String serverUrl, + Path credentialHelper, + Properties properties, + Function, ProcessBuilder> processBuilderFactory) { + return dockerCredentialHelper( + serverUrl, credentialHelper, properties, processBuilderFactory, Collections.emptyMap()); + } + + private DockerCredentialHelper dockerCredentialHelper( + String serverUrl, + Path credentialHelper, + Properties properties, + Function, ProcessBuilder> processBuilderFactory, + Map environment) { + return new DockerCredentialHelper( + serverUrl, credentialHelper, properties, processBuilderFactory, environment); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java deleted file mode 100644 index 3a7ce059bb..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry.credentials.json; - -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link DockerConfigTemplate}. */ -public class DockerConfigTemplateTest { - - @Test - public void test_fromJson() throws URISyntaxException, IOException { - // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - - // Deserializes into a docker config JSON object. - DockerConfigTemplate dockerConfigTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, DockerConfigTemplate.class); - - Assert.assertEquals("some auth", dockerConfigTemplate.getAuthFor("some registry")); - Assert.assertEquals("some other auth", dockerConfigTemplate.getAuthFor("some other registry")); - Assert.assertEquals("token", dockerConfigTemplate.getAuthFor("registry")); - Assert.assertEquals("token", dockerConfigTemplate.getAuthFor("https://registry")); - Assert.assertEquals(null, dockerConfigTemplate.getAuthFor("just registry")); - - Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("some registry")); - Assert.assertEquals( - "some credential store", - dockerConfigTemplate.getCredentialHelperFor("some other registry")); - Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("just registry")); - Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("with.protocol")); - Assert.assertEquals( - "another credential helper", - dockerConfigTemplate.getCredentialHelperFor("another registry")); - Assert.assertEquals(null, dockerConfigTemplate.getCredentialHelperFor("unknonwn registry")); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java new file mode 100644 index 0000000000..2bff23dbdc --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.tar; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link TarExtractor}. */ +public class TarExtractorTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testExtract() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/extract.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + TarExtractor.extract(source, destination); + + Assert.assertTrue(Files.exists(destination.resolve("file A"))); + Assert.assertTrue(Files.exists(destination.resolve("file B"))); + Assert.assertTrue( + Files.exists(destination.resolve("folder").resolve("nested folder").resolve("file C"))); + + try (Stream lines = Files.lines(destination.resolve("file A"))) { + String contents = lines.collect(Collectors.joining()); + Assert.assertEquals("Hello", contents); + } + } + + @Test + public void testExtract_missingDirectoryEntries() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/extract-missing-dirs.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + TarExtractor.extract(source, destination); + + Assert.assertTrue(Files.exists(destination.resolve("world"))); + Assert.assertTrue( + Files.exists(destination.resolve("a").resolve("b").resolve("c").resolve("world"))); + + try (Stream lines = Files.lines(destination.resolve("world"))) { + String contents = lines.collect(Collectors.joining()); + Assert.assertEquals("world", contents); + } + } + + @Test + public void testExtract_symlinks() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/symlinks.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + TarExtractor.extract(source, destination); + + Assert.assertTrue(Files.isDirectory(destination.resolve("directory1"))); + Assert.assertTrue(Files.isDirectory(destination.resolve("directory2"))); + Assert.assertTrue(Files.isRegularFile(destination.resolve("directory2/regular"))); + Assert.assertTrue(Files.isSymbolicLink(destination.resolve("directory-symlink"))); + Assert.assertTrue(Files.isSymbolicLink(destination.resolve("directory1/file-symlink"))); + } + + @Test + public void testExtract_modificationTimePreserved() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/extract.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + + TarExtractor.extract(source, destination); + + assertThat( + Files.getLastModifiedTime(destination.resolve("file A")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2019-08-01T16:13:09Z")); + assertThat( + Files.getLastModifiedTime(destination.resolve("file B")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2019-08-01T16:12:00Z")); + assertThat( + Files.getLastModifiedTime(destination.resolve("folder")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2019-08-01T16:12:33Z")); + assertThat( + Files.getLastModifiedTime(destination.resolve("folder/nested folder")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2019-08-01T16:13:30Z")); + assertThat( + Files.getLastModifiedTime(destination.resolve("folder/nested folder/file C")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2019-08-01T16:12:21Z")); + } + + @Test + public void testExtract_reproducibleTimestampsEnabled() throws URISyntaxException, IOException { + // The tarfile has only level1/level2/level3/file.txt packaged + Path source = Paths.get(Resources.getResource("core/tarfile-only-file-packaged.tar").toURI()); + + Path destination = temporaryFolder.getRoot().toPath(); + + TarExtractor.extract(source, destination, true); + + assertThat( + Files.getLastModifiedTime(destination.resolve("level-1")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(FileTime.fromMillis(1000L).toInstant()); + assertThat( + Files.getLastModifiedTime(destination.resolve("level-1/level-2")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(FileTime.fromMillis(1000L).toInstant()); + assertThat( + Files.getLastModifiedTime(destination.resolve("level-1/level-2/level-3")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(FileTime.fromMillis(1000L).toInstant()); + assertThat( + Files.getLastModifiedTime(destination.resolve("level-1/level-2/level-3/file.txt")) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(Instant.parse("2021-01-29T21:10:02Z")); + } + + @Test + public void testExtract_reproducibleTimestampsEnabled_destinationNotEmpty() throws IOException { + Path destination = temporaryFolder.getRoot().toPath(); + temporaryFolder.newFile(); + + IllegalStateException exception = + assertThrows( + IllegalStateException.class, + () -> TarExtractor.extract(Paths.get("ignore"), destination, true)); + assertThat(exception).hasMessageThat().startsWith("Cannot enable reproducible timestamps"); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index ddc83e59da..f6afefb325 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,20 +16,22 @@ package com.google.cloud.tools.jib.tar; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.common.io.CharStreams; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; @@ -44,19 +46,19 @@ public class TarStreamBuilderTest { private Path fileA; private Path fileB; private Path directoryA; - private String fileAContents; - private String fileBContents; - private TarStreamBuilder testTarStreamBuilder = new TarStreamBuilder(); + private byte[] fileAContents; + private byte[] fileBContents; + private final TarStreamBuilder testTarStreamBuilder = new TarStreamBuilder(); @Before public void setup() throws URISyntaxException, IOException { // Gets the test resource files. - fileA = Paths.get(Resources.getResource("fileA").toURI()); - fileB = Paths.get(Resources.getResource("fileB").toURI()); - directoryA = Paths.get(Resources.getResource("directoryA").toURI()); + fileA = Paths.get(Resources.getResource("core/fileA").toURI()); + fileB = Paths.get(Resources.getResource("core/fileB").toURI()); + directoryA = Paths.get(Resources.getResource("core/directoryA").toURI()); - fileAContents = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); - fileBContents = new String(Files.readAllBytes(fileB), StandardCharsets.UTF_8); + fileAContents = Files.readAllBytes(fileA); + fileBContents = Files.readAllBytes(fileB); } @Test @@ -95,49 +97,112 @@ public void testToBlob_stringsAndTarArchiveEntriesWithCompression() throws IOExc verifyBlobWithCompression(); } + @Test + public void testToBlob_multiByte() throws IOException { + testTarStreamBuilder.addByteEntry( + "日本語".getBytes(StandardCharsets.UTF_8), "test", Instant.EPOCH); + testTarStreamBuilder.addByteEntry( + "asdf".getBytes(StandardCharsets.UTF_8), "crepecake", Instant.EPOCH); + testTarStreamBuilder.addBlobEntry( + Blobs.from("jib"), "jib".getBytes(StandardCharsets.UTF_8).length, "jib", Instant.EPOCH); + + // Writes the BLOB and captures the output. + ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); + OutputStream compressorStream = new GZIPOutputStream(tarByteOutputStream); + testTarStreamBuilder.writeAsTarArchiveTo(compressorStream); + + // Rearrange the output into input for verification. + ByteArrayInputStream byteArrayInputStream = + new ByteArrayInputStream(tarByteOutputStream.toByteArray()); + InputStream tarByteInputStream = new GZIPInputStream(byteArrayInputStream); + TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(tarByteInputStream); + + // Verify multi-byte characters are written/read correctly + TarArchiveEntry headerFile = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("test", headerFile.getName()); + Assert.assertEquals( + "日本語", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); + + headerFile = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("crepecake", headerFile.getName()); + Assert.assertEquals( + "asdf", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); + + headerFile = tarArchiveInputStream.getNextEntry(); + Assert.assertEquals("jib", headerFile.getName()); + Assert.assertEquals( + "jib", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); + + Assert.assertNull(tarArchiveInputStream.getNextEntry()); + } + + @Test + public void testToBlob_modificationTime() throws IOException { + testTarStreamBuilder.addByteEntry( + "foo".getBytes(StandardCharsets.UTF_8), "foo", Instant.ofEpochSecond(1234)); + testTarStreamBuilder.addBlobEntry( + Blobs.from("bar"), + "bar".getBytes(StandardCharsets.UTF_8).length, + "bar", + Instant.ofEpochSecond(3)); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + testTarStreamBuilder.writeAsTarArchiveTo(outStream); + + TarArchiveInputStream tarInStream = + new TarArchiveInputStream(new ByteArrayInputStream(outStream.toByteArray())); + TarArchiveEntry entry1 = tarInStream.getNextEntry(); + TarArchiveEntry entry2 = tarInStream.getNextEntry(); + + assertThat(entry1.getName()).isEqualTo("foo"); + assertThat(entry1.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(1234)); + assertThat(entry2.getName()).isEqualTo("bar"); + assertThat(entry2.getModTime().toInstant()).isEqualTo(Instant.ofEpochSecond(3)); + } + /** Creates a TarStreamBuilder using TarArchiveEntries. */ - private void setUpWithTarEntries() { + private void setUpWithTarEntries() throws IOException { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry( - new TarArchiveEntry(fileA.toFile(), "some/path/to/resourceFileA")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addTarArchiveEntry( + new TarArchiveEntry(fileA, "some/path/to/resourceFileA")); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(fileB, "crepecake")); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(directoryA, "some/path/to")); + testTarStreamBuilder.addTarArchiveEntry( new TarArchiveEntry( - fileA.toFile(), + fileA, "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890")); } /** Creates a TarStreamBuilder using Strings. */ - private void setUpWithStrings() { + private void setUpWithStrings() throws IOException { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry(fileAContents, "some/path/to/resourceFileA"); - testTarStreamBuilder.addEntry(fileBContents, "crepecake"); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addByteEntry(fileAContents, "some/path/to/resourceFileA", Instant.EPOCH); + testTarStreamBuilder.addByteEntry(fileBContents, "crepecake", Instant.EPOCH); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(directoryA, "some/path/to")); + testTarStreamBuilder.addByteEntry( fileAContents, - "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890"); + "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890", + Instant.EPOCH); } /** Creates a TarStreamBuilder using Strings and TarArchiveEntries. */ - private void setUpWithStringsAndTarEntries() { + private void setUpWithStringsAndTarEntries() throws IOException { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry(fileAContents, "some/path/to/resourceFileA"); - testTarStreamBuilder.addEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addByteEntry(fileAContents, "some/path/to/resourceFileA", Instant.EPOCH); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(fileB, "crepecake")); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(directoryA, "some/path/to")); + testTarStreamBuilder.addByteEntry( fileAContents, - "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890"); + "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890", + Instant.EPOCH); } /** Creates a compressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithCompression() throws IOException { - Blob blob = testTarStreamBuilder.toBlob(); - // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); OutputStream compressorStream = new GZIPOutputStream(tarByteOutputStream); - blob.writeTo(compressorStream); + testTarStreamBuilder.writeAsTarArchiveTo(compressorStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = @@ -149,11 +214,9 @@ private void verifyBlobWithCompression() throws IOException { /** Creates an uncompressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithoutCompression() throws IOException { - Blob blob = testTarStreamBuilder.toBlob(); - // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); - blob.writeTo(tarByteOutputStream); + testTarStreamBuilder.writeAsTarArchiveTo(tarByteOutputStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = @@ -168,32 +231,29 @@ private void verifyBlobWithoutCompression() throws IOException { */ private void verifyTarArchive(TarArchiveInputStream tarArchiveInputStream) throws IOException { // Verifies fileA was archived correctly. - TarArchiveEntry headerFileA = tarArchiveInputStream.getNextTarEntry(); + TarArchiveEntry headerFileA = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("some/path/to/resourceFileA", headerFileA.getName()); - String fileAString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileAContents, fileAString); + byte[] fileAString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileAContents, fileAString); // Verifies fileB was archived correctly. - TarArchiveEntry headerFileB = tarArchiveInputStream.getNextTarEntry(); + TarArchiveEntry headerFileB = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("crepecake", headerFileB.getName()); - String fileBString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileBContents, fileBString); + byte[] fileBString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileBContents, fileBString); // Verifies directoryA was archived correctly. - TarArchiveEntry headerDirectoryA = tarArchiveInputStream.getNextTarEntry(); + TarArchiveEntry headerDirectoryA = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("some/path/to/", headerDirectoryA.getName()); // Verifies the long file was archived correctly. - TarArchiveEntry headerFileALong = tarArchiveInputStream.getNextTarEntry(); + TarArchiveEntry headerFileALong = tarArchiveInputStream.getNextEntry(); Assert.assertEquals( "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890", headerFileALong.getName()); - String fileALongString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileAContents, fileALongString); + byte[] fileALongString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileAContents, fileALongString); - Assert.assertNull(tarArchiveInputStream.getNextTarEntry()); + Assert.assertNull(tarArchiveInputStream.getNextEntry()); } } diff --git a/jib-core/src/test/resources/META-INF/services/com.google.cloud.tools.jib.api.DockerClient b/jib-core/src/test/resources/META-INF/services/com.google.cloud.tools.jib.api.DockerClient new file mode 100644 index 0000000000..597997356e --- /dev/null +++ b/jib-core/src/test/resources/META-INF/services/com.google.cloud.tools.jib.api.DockerClient @@ -0,0 +1 @@ +com.google.cloud.tools.jib.docker.AnotherDockerClient \ No newline at end of file diff --git a/jib-core/src/test/resources/cache/metadata-v2.json b/jib-core/src/test/resources/cache/metadata-v2.json deleted file mode 100644 index cdb6049a7b..0000000000 --- a/jib-core/src/test/resources/cache/metadata-v2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "layers": [ - { - "reference": { - "size": 1000, - "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" - } - }, - { - "reference": { - "size": 2000, - "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", - "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" - } - }, - { - "reference": { - "size": 0, - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "properties": { - "layerEntries": [ - { - "sourceFiles": ["some/source/directory"], - "extractionPath": "some/extraction/path" - } - ], - "lastModifiedTime": 1000 - } - } - ] -} \ No newline at end of file diff --git a/jib-core/src/test/resources/cache/metadata.json b/jib-core/src/test/resources/cache/metadata.json deleted file mode 100644 index fb2ed7fa1b..0000000000 --- a/jib-core/src/test/resources/cache/metadata.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "layers": [ - { - "reference": { - "size": 1000, - "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" - } - }, - { - "reference": { - "size": 2000, - "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", - "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" - } - }, - { - "reference": { - "size": 0, - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "properties": { - "sourceFiles": ["some/source/directory"], - "lastModifiedTime": 1000 - } - } - ] -} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/TestWebServer-keystore b/jib-core/src/test/resources/core/TestWebServer-keystore new file mode 100644 index 0000000000..a485abe8d3 Binary files /dev/null and b/jib-core/src/test/resources/core/TestWebServer-keystore differ diff --git a/jib-core/src/test/resources/application/classes/HelloWorld.class b/jib-core/src/test/resources/core/application/classes/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/application/classes/HelloWorld.class rename to jib-core/src/test/resources/core/application/classes/HelloWorld.class diff --git a/jib-core/src/test/resources/core/application/classes/some.class b/jib-core/src/test/resources/core/application/classes/some.class new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/application/dependencies/dependency-1.0.0.jar b/jib-core/src/test/resources/core/application/dependencies/dependency-1.0.0.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-core/src/test/resources/core/application/dependencies/dependency-1.0.0.jar differ diff --git a/jib-core/src/test/resources/core/application/dependencies/libraryA.jar b/jib-core/src/test/resources/core/application/dependencies/libraryA.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/application/dependencies/libraryB.jar b/jib-core/src/test/resources/core/application/dependencies/libraryB.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar b/jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar new file mode 100644 index 0000000000..4d14054965 --- /dev/null +++ b/jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar @@ -0,0 +1,2 @@ +]e$Ềx, +.3I݅38VKAM)=5~'qю$[- :&% Eo7Ns`iZ0MT.9J[}?\E }UvJdo(i"Mԛ_+/cI.-O=Hi \ No newline at end of file diff --git a/jib-core/src/test/resources/core/application/resources/resourceA b/jib-core/src/test/resources/core/application/resources/resourceA new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/application/resources/resourceB b/jib-core/src/test/resources/core/application/resources/resourceB new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/resources/world b/jib-core/src/test/resources/core/application/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/resources/world rename to jib-core/src/test/resources/core/application/resources/world diff --git a/jib-core/src/test/resources/core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar b/jib-core/src/test/resources/core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-core/src/test/resources/core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar differ diff --git a/jib-core/src/test/resources/blobA b/jib-core/src/test/resources/core/blobA similarity index 100% rename from jib-core/src/test/resources/blobA rename to jib-core/src/test/resources/core/blobA diff --git a/jib-core/src/test/resources/class-finder-tests/extension/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/extension/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/extension/pack/Apple.class b/jib-core/src/test/resources/core/class-finder-tests/extension/pack/Apple.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/pack/Apple.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/pack/Apple.class diff --git a/jib-core/src/test/resources/class-finder-tests/extension/pack/Orange.class b/jib-core/src/test/resources/core/class-finder-tests/extension/pack/Orange.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/pack/Orange.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/pack/Orange.class diff --git a/jib-core/src/test/resources/class-finder-tests/external-classes/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/external-classes/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/external-classes/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/external-classes/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Apple.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Apple.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Apple.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Apple.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Orange.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Orange.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Orange.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Orange.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld$InnerClass.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld$InnerClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld$InnerClass.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld$InnerClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/NotMain.class diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainNoArgs.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainNoArgs.class new file mode 100644 index 0000000000..9ad2282289 Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainNoArgs.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainWithArgs.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainWithArgs.class new file mode 100644 index 0000000000..de79ec790c Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/InstanceMainWithArgs.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PackagePrivateMain.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PackagePrivateMain.class new file mode 100644 index 0000000000..b730f6fbbf Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PackagePrivateMain.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PrivateMain.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PrivateMain.class new file mode 100644 index 0000000000..205793d46c Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/PrivateMain.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/ProtectedMain.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/ProtectedMain.class new file mode 100644 index 0000000000..c9127da511 Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/ProtectedMain.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/StaticMainNoArgs.class b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/StaticMainNoArgs.class new file mode 100644 index 0000000000..81d93aa138 Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/java25-flexible-main/StaticMainNoArgs.class differ diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer2Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/layered/HelloMoon.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/layered/HelloMoon.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/layered/HelloMoon.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/layered/HelloMoon.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer2Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/simple/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/simple/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/simple/HelloWorld.class diff --git a/jib-core/src/test/resources/core/class-finder-tests/simple/NotAClassButMightBe.class b/jib-core/src/test/resources/core/class-finder-tests/simple/NotAClassButMightBe.class new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotEvenAClass.txt b/jib-core/src/test/resources/core/class-finder-tests/simple/NotEvenAClass.txt similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotEvenAClass.txt rename to jib-core/src/test/resources/core/class-finder-tests/simple/NotEvenAClass.txt diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/simple/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/simple/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/Layer2Class.class diff --git a/jib-core/src/test/resources/core/class-finder-tests/synthetic/HelloWorldKt.class b/jib-core/src/test/resources/core/class-finder-tests/synthetic/HelloWorldKt.class new file mode 100644 index 0000000000..e054f6b0af Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/synthetic/HelloWorldKt.class differ diff --git a/jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class new file mode 100644 index 0000000000..82a32d82a3 Binary files /dev/null and b/jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class differ diff --git a/jib-core/src/test/resources/core/directoryA/.gitkeep b/jib-core/src/test/resources/core/directoryA/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/docker/emptyFile b/jib-core/src/test/resources/core/docker/emptyFile new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/extract-missing-dirs.tar b/jib-core/src/test/resources/core/extract-missing-dirs.tar new file mode 100644 index 0000000000..4fb03f1c9a Binary files /dev/null and b/jib-core/src/test/resources/core/extract-missing-dirs.tar differ diff --git a/jib-core/src/test/resources/core/extract.tar b/jib-core/src/test/resources/core/extract.tar new file mode 100644 index 0000000000..fdc05b1427 Binary files /dev/null and b/jib-core/src/test/resources/core/extract.tar differ diff --git a/jib-core/src/test/resources/core/extraction/compressed.tar.gz b/jib-core/src/test/resources/core/extraction/compressed.tar.gz new file mode 100644 index 0000000000..bb55593d55 Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/compressed.tar.gz differ diff --git a/jib-core/src/test/resources/core/extraction/docker-save.tar b/jib-core/src/test/resources/core/extraction/docker-save.tar new file mode 100644 index 0000000000..08ba90b4c9 Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/docker-save.tar differ diff --git a/jib-core/src/test/resources/core/extraction/jib-image.tar b/jib-core/src/test/resources/core/extraction/jib-image.tar new file mode 100644 index 0000000000..1281542d7e Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/jib-image.tar differ diff --git a/jib-core/src/test/resources/core/extraction/not-compressed.tar b/jib-core/src/test/resources/core/extraction/not-compressed.tar new file mode 100644 index 0000000000..9faac26673 Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/not-compressed.tar differ diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f b/jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f new file mode 100644 index 0000000000..571cfb2d04 Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f differ diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 b/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 new file mode 100644 index 0000000000..601a2227c6 --- /dev/null +++ b/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 @@ -0,0 +1 @@ +{"created":"1970-01-01T00:00:01Z","architecture":"amd64","os":"linux","config":{"Env":[],"Entrypoint":["java","-cp","/app/resources:/app/classes:/app/libs/*","Hello"],"ExposedPorts":{},"Labels":{"label1":"value1","label2":"value2"},"Volumes":{}},"history":[{"created":"1970-01-01T00:00:01Z","author":"Jib","created_by":"jib-gradle-plugin:1.4.1-SNAPSHOT","comment":"classes"},{"created":"1970-01-01T00:00:01Z","author":"Jib","created_by":"jib-gradle-plugin:1.4.1-SNAPSHOT","comment":"extra files"}],"rootfs":{"type":"layers","diff_ids":["sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd","sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 b/jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 new file mode 100644 index 0000000000..7e13f3f70b Binary files /dev/null and b/jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 differ diff --git a/jib-core/src/test/resources/core/fileA b/jib-core/src/test/resources/core/fileA new file mode 100644 index 0000000000..d9ef1a6b36 --- /dev/null +++ b/jib-core/src/test/resources/core/fileA @@ -0,0 +1 @@ +Crepe cakes are good. \ No newline at end of file diff --git a/jib-core/src/test/resources/core/fileB b/jib-core/src/test/resources/core/fileB new file mode 100644 index 0000000000..c891cde53b --- /dev/null +++ b/jib-core/src/test/resources/core/fileB @@ -0,0 +1 @@ +Fast image builds are great. \ No newline at end of file diff --git a/jib-core/src/test/resources/json/basic.json b/jib-core/src/test/resources/core/json/basic.json similarity index 100% rename from jib-core/src/test/resources/json/basic.json rename to jib-core/src/test/resources/core/json/basic.json diff --git a/jib-core/src/test/resources/core/json/basic_list.json b/jib-core/src/test/resources/core/json/basic_list.json new file mode 100644 index 0000000000..d140c8738b --- /dev/null +++ b/jib-core/src/test/resources/core/json/basic_list.json @@ -0,0 +1 @@ +[{"number":1,"text":"text1","digest":"sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10","innerObject":{"number":10},"list":[{"number":11},{"number":12}]},{"number":2,"text":"text2","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","innerObject":{"number":20},"list":[]}] \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/containerconfig.json b/jib-core/src/test/resources/core/json/containerconfig.json new file mode 100644 index 0000000000..850b612fd2 --- /dev/null +++ b/jib-core/src/test/resources/core/json/containerconfig.json @@ -0,0 +1 @@ +{"created":"1970-01-01T00:00:20Z","architecture":"wasm","os":"js","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"Healthcheck":{"Test":["CMD-SHELL","/checkhealth"],"Interval":3000000000,"Timeout":1000000000,"StartPeriod":2000000000,"Retries":3},"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/dockerconfig.json b/jib-core/src/test/resources/core/json/dockerconfig.json new file mode 100644 index 0000000000..06a4c77c8b --- /dev/null +++ b/jib-core/src/test/resources/core/json/dockerconfig.json @@ -0,0 +1,26 @@ +{ + "auths":{ + "some other registry":{"auth":"c29tZTpvdGhlcjphdXRo"}, + "some registry":{"auth":"c29tZTphdXRo","password":"ignored"}, + "auth and userpw registry":{"auth":"c29tZTphdXRo","username":"ignored","password":"ignored"}, + "userpw registry":{"username":"someuser","password":"somepw"}, + "https://registry":{"auth":"dG9rZW4="}, + + "example.com":{"auth":"should not match example"}, + "no auth field":{} + }, + "credsStore":"some credential store", + "credHelpers":{ + "another registry":"another credential helper", + "some registry":"some credential helper", + "index.docker.io":"index.docker.io credential helper", + + "just.registry.in.helpers":"credHelper for just.registry.in.helpers", + "https://with.protocol.in.helpers":"credHelper for https__with.protocol.in.helpers", + "with.suffix.in.helpers/v2/":"credHelper for with.suffix.in.helpers/v2/", + "https://with.protocol.and.suffix.in.helpers/suffix": + "credHelper for https__with.protocol.and.suffix.in.helpers/suffix", + + "another.example.com.in.helpers":"should not match example" + } +} diff --git a/jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json b/jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json new file mode 100644 index 0000000000..2b81d7cf31 --- /dev/null +++ b/jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json @@ -0,0 +1,17 @@ +{ + "auths":{ + "https://my-registry/v1/":{"auth":"my-registry: starting with name and with https"}, + "my-registry/v4/":{"auth":"my-registry: starting with name"}, + "https://my-registry":{"auth":"my-registry: with https"}, + "my-registry":{"auth":"my-registry: exact match"}, + + "https://cool-registry":{"auth":"cool-registry: with https"}, + "cool-registry/v8/":{"auth":"cool-registry: starting with registry"}, + "https://cool-registry/v1/":{"auth":"cool-registry: starting with name and with https"}, + + "https://awesome-registry/v9/":{"auth":"awesome-registry: starting with name and with https"}, + "awesome-registry/v9/":{"auth":"awesome-registry: starting with name"}, + + "https://dull-registry/v3/":{"auth":"dull-registry: starting with name and with https"} + } +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/dockerconfig_identity_token.json b/jib-core/src/test/resources/core/json/dockerconfig_identity_token.json new file mode 100644 index 0000000000..9f7117a606 --- /dev/null +++ b/jib-core/src/test/resources/core/json/dockerconfig_identity_token.json @@ -0,0 +1,8 @@ +{ + "auths":{ + "some registry":{ + "auth":"MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==", + "identitytoken":"cool identity token" + } + } +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json b/jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json new file mode 100644 index 0000000000..65ff209005 --- /dev/null +++ b/jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json @@ -0,0 +1,5 @@ +{ + "auths":{ + "index.docker.io/v1/":{"auth":"dG9rZW4gZm9yOiBpbmRleC5kb2NrZXIuaW8vdjEv"} + } +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/legacy_dockercfg b/jib-core/src/test/resources/core/json/legacy_dockercfg new file mode 100644 index 0000000000..aa816a77b7 --- /dev/null +++ b/jib-core/src/test/resources/core/json/legacy_dockercfg @@ -0,0 +1,4 @@ +{ + "some registry":{"auth":"c29tZTpvdGhlcjphdXRo"}, + "https://example.com/v2/":{"auth":"dXNlcjpwYXNz"} +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/loadmanifest.json b/jib-core/src/test/resources/core/json/loadmanifest.json new file mode 100644 index 0000000000..c854e3aa82 --- /dev/null +++ b/jib-core/src/test/resources/core/json/loadmanifest.json @@ -0,0 +1 @@ +[{"Config":"config.json","RepoTags":["testregistry/testrepo:testtag"],"Layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"]}] \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/loadmanifest2.json b/jib-core/src/test/resources/core/json/loadmanifest2.json new file mode 100644 index 0000000000..1bbe7173f1 --- /dev/null +++ b/jib-core/src/test/resources/core/json/loadmanifest2.json @@ -0,0 +1 @@ +[{"Config":"config.json","RepoTags":["testregistry/testrepo:testtag"],"Layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"],"LayerSources":{}}] diff --git a/jib-core/src/test/resources/json/metadata-v2.json b/jib-core/src/test/resources/core/json/metadata-v2.json similarity index 100% rename from jib-core/src/test/resources/json/metadata-v2.json rename to jib-core/src/test/resources/core/json/metadata-v2.json diff --git a/jib-core/src/test/resources/core/json/metadata-v3.json b/jib-core/src/test/resources/core/json/metadata-v3.json new file mode 100644 index 0000000000..84411ba067 --- /dev/null +++ b/jib-core/src/test/resources/core/json/metadata-v3.json @@ -0,0 +1 @@ +{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"/some/source/path","extractionPath":"/some/extraction/path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/metadata.json b/jib-core/src/test/resources/core/json/metadata.json similarity index 100% rename from jib-core/src/test/resources/json/metadata.json rename to jib-core/src/test/resources/core/json/metadata.json diff --git a/jib-core/src/test/resources/json/metadata_corrupted.json b/jib-core/src/test/resources/core/json/metadata_corrupted.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_corrupted.json rename to jib-core/src/test/resources/core/json/metadata_corrupted.json diff --git a/jib-core/src/test/resources/json/metadata_windows-v2.json b/jib-core/src/test/resources/core/json/metadata_windows-v2.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_windows-v2.json rename to jib-core/src/test/resources/core/json/metadata_windows-v2.json diff --git a/jib-core/src/test/resources/json/metadata_windows.json b/jib-core/src/test/resources/core/json/metadata_windows.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_windows.json rename to jib-core/src/test/resources/core/json/metadata_windows.json diff --git a/jib-core/src/test/resources/core/json/ociindex.json b/jib-core/src/test/resources/core/json/ociindex.json new file mode 100644 index 0000000000..943384a6ef --- /dev/null +++ b/jib-core/src/test/resources/core/json/ociindex.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + "size": 1000, + "annotations": { + "org.opencontainers.image.ref.name": "regis.try/repo:tag" + } + } + ] +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/ociindex_artifacttype.json b/jib-core/src/test/resources/core/json/ociindex_artifacttype.json new file mode 100644 index 0000000000..0895ce4deb --- /dev/null +++ b/jib-core/src/test/resources/core/json/ociindex_artifacttype.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 1395, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "artifactType": "application/vnd.docker.container.image.v1+json" + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 1395, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "arm64", + "os": "linux" + }, + "artifactType": "application/vnd.docker.container.image.v1+json" + } + ] +} diff --git a/jib-core/src/test/resources/core/json/ociindex_platforms.json b/jib-core/src/test/resources/core/json/ociindex_platforms.json new file mode 100644 index 0000000000..d4254b474f --- /dev/null +++ b/jib-core/src/test/resources/core/json/ociindex_platforms.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "size": 7143, + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "size": 7682, + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/ocimanifest.json b/jib-core/src/test/resources/core/json/ocimanifest.json similarity index 100% rename from jib-core/src/test/resources/json/ocimanifest.json rename to jib-core/src/test/resources/core/json/ocimanifest.json diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/core/json/translated_ocimanifest.json similarity index 68% rename from jib-core/src/test/resources/json/translated_ocimanifest.json rename to jib-core/src/test/resources/core/json/translated_ocimanifest.json index b1b1d8c803..8429edcab6 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/core/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:71050b2d503c7463cfe574c0b37b6575aed1ace518c0c9174ddb9cd91b57ff4a","size":353},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:9df7a2b76e1b35dcf443c4b6a08e40a5d040120022293f7d03ae082f95835839","size":686},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/core/json/translated_v22manifest.json similarity index 69% rename from jib-core/src/test/resources/json/translated_v22manifest.json rename to jib-core/src/test/resources/core/json/translated_v22manifest.json index 8d35a1fd6d..13082f490d 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/core/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:71050b2d503c7463cfe574c0b37b6575aed1ace518c0c9174ddb9cd91b57ff4a","size":353},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:2000a70a1ce8bba401c493376fdb9eb81bcf7155212f4ce4c6ff96e577718a49","size":818},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/v21manifest.json b/jib-core/src/test/resources/core/json/v21manifest.json new file mode 100644 index 0000000000..09428920ef --- /dev/null +++ b/jib-core/src/test/resources/core/json/v21manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion":1, + "fsLayers": [ + {"blobSum":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"}, + {"blobSum":"sha256:5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3"} + ], + "history": [ + {"v1Compatibility": "{\"architecture\":\"ppc64le\",\"config\":{\"Env\":[\"JAVA_HOME=/opt/openjdk\",\"PATH=/opt/openjdk/bin\"],\"Entrypoint\":[\"/opt/openjdk/bin/java\"],\"Cmd\":[\"-version\"]},\"created\":\"2019-04-17T14:02:34.79404123Z\"}"}, + {"v1Compatibility":"another v1-compatible object"} + ] +} diff --git a/jib-core/src/test/resources/json/v22manifest.json b/jib-core/src/test/resources/core/json/v22manifest.json similarity index 100% rename from jib-core/src/test/resources/json/v22manifest.json rename to jib-core/src/test/resources/core/json/v22manifest.json diff --git a/jib-core/src/test/resources/core/json/v22manifest_list.json b/jib-core/src/test/resources/core/json/v22manifest_list.json new file mode 100644 index 0000000000..cc6b82ff3b --- /dev/null +++ b/jib-core/src/test/resources/core/json/v22manifest_list.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "artifactType": "application/vnd.docker.container.image.v1+json" + }, + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "features": [ + "sse4" + ] + } + }, + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7682, + "digest": "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.version": "potato-9" + } + } + ] +} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json b/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json new file mode 100644 index 0000000000..19b2a0e914 --- /dev/null +++ b/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 2, + "mediaType": "type", + "config":{ + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 10 + }, + "layers":[ + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 1 + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 2, + "urls": ["url-foo", "url-bar"] + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 3, + "annotations": {"key-foo":"value-foo"} + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 4, + "urls": ["cool-url"], + "annotations": {"key1":"value1", "key2":"value2"} + } + ] +} \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/layer/a/b/bar b/jib-core/src/test/resources/core/layer/a/b/bar similarity index 100% rename from jib-maven-plugin/src/test/resources/layer/a/b/bar rename to jib-core/src/test/resources/core/layer/a/b/bar diff --git a/jib-maven-plugin/src/test/resources/layer/c/cat b/jib-core/src/test/resources/core/layer/c/cat similarity index 100% rename from jib-maven-plugin/src/test/resources/layer/c/cat rename to jib-core/src/test/resources/core/layer/c/cat diff --git a/jib-maven-plugin/src/test/resources/layer/foo b/jib-core/src/test/resources/core/layer/foo similarity index 100% rename from jib-maven-plugin/src/test/resources/layer/foo rename to jib-core/src/test/resources/core/layer/foo diff --git a/jib-core/src/test/resources/core/random-contents/file1 b/jib-core/src/test/resources/core/random-contents/file1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/random-contents/file2 b/jib-core/src/test/resources/core/random-contents/file2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/random-contents/sub-directory/file3 b/jib-core/src/test/resources/core/random-contents/sub-directory/file3 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/random-contents/sub-directory/file4 b/jib-core/src/test/resources/core/random-contents/sub-directory/file4 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file5 b/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file5 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file6 b/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file6 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/core/symlinks.tar b/jib-core/src/test/resources/core/symlinks.tar new file mode 100644 index 0000000000..3f5c8f0fb0 Binary files /dev/null and b/jib-core/src/test/resources/core/symlinks.tar differ diff --git a/jib-core/src/test/resources/core/tarfile-only-file-packaged.tar b/jib-core/src/test/resources/core/tarfile-only-file-packaged.tar new file mode 100644 index 0000000000..90bc40ed4f Binary files /dev/null and b/jib-core/src/test/resources/core/tarfile-only-file-packaged.tar differ diff --git a/jib-core/src/test/resources/core/webAppSampleDockerfile b/jib-core/src/test/resources/core/webAppSampleDockerfile new file mode 100644 index 0000000000..3667c9feec --- /dev/null +++ b/jib-core/src/test/resources/core/webAppSampleDockerfile @@ -0,0 +1,9 @@ +FROM tomcat:8.5-jre8-alpine + +COPY libs / +COPY snapshot-libs / +COPY resources / +COPY classes / +COPY root / + +ENTRYPOINT ["catalina.sh","run"] \ No newline at end of file diff --git a/jib-core/src/test/resources/fileA b/jib-core/src/test/resources/fileA deleted file mode 100644 index b29ddafaad..0000000000 --- a/jib-core/src/test/resources/fileA +++ /dev/null @@ -1 +0,0 @@ -Crepe cakes are good. diff --git a/jib-core/src/test/resources/fileB b/jib-core/src/test/resources/fileB deleted file mode 100644 index 710951eef5..0000000000 --- a/jib-core/src/test/resources/fileB +++ /dev/null @@ -1 +0,0 @@ -Fast image builds are great. diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json deleted file mode 100644 index 6c83f82c47..0000000000 --- a/jib-core/src/test/resources/json/containerconfig.json +++ /dev/null @@ -1 +0,0 @@ -{"created":"1970-01-01T00:00:00Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}}},"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/dockerconfig.json b/jib-core/src/test/resources/json/dockerconfig.json deleted file mode 100644 index a4cbe7a126..0000000000 --- a/jib-core/src/test/resources/json/dockerconfig.json +++ /dev/null @@ -1 +0,0 @@ -{"auths":{"some other registry":{"auth":"some other auth"},"some registry":{"auth":"some auth","password":"ignored"},"https://registry":{"auth":"token"},"just registry":{},"https://with.protocol":{}},"credsStore":"some credential store","credHelpers":{"another registry":"another credential helper","some registry":"some credential helper"}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/loadmanifest.json b/jib-core/src/test/resources/json/loadmanifest.json deleted file mode 100644 index f062d3e146..0000000000 --- a/jib-core/src/test/resources/json/loadmanifest.json +++ /dev/null @@ -1 +0,0 @@ -[{"config":"config.json","repoTags":["testregistry/testrepo:testtag"],"layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"]}] \ No newline at end of file diff --git a/jib-core/src/test/resources/json/loadmanifest_notag.json b/jib-core/src/test/resources/json/loadmanifest_notag.json deleted file mode 100644 index 7eaddeb2d4..0000000000 --- a/jib-core/src/test/resources/json/loadmanifest_notag.json +++ /dev/null @@ -1 +0,0 @@ -[{"config":"config.json","repoTags":["testrepo:latest"],"layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"]}] \ No newline at end of file diff --git a/jib-core/src/test/resources/json/v21manifest.json b/jib-core/src/test/resources/json/v21manifest.json deleted file mode 100644 index 4eaa389cf8..0000000000 --- a/jib-core/src/test/resources/json/v21manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion":1,"fsLayers":[{"blobSum":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"}],"history":[{"v1Compatibility":"some v1-compatible object"}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile deleted file mode 100644 index 73d372b6d0..0000000000 --- a/jib-core/src/test/resources/sampleDockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM somebaseimage - -COPY libs /app/libs/ -COPY resources /app/resources/ -COPY classes /app/classes/ - -EXPOSE 1000/tcp -EXPOSE 2000-2010/udp -ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/libs/*:/app/resources/:/app/classes/","SomeMainClass"] -CMD ["arg1","arg2"] diff --git a/jib-gradle-plugin-extension-api/CHANGELOG.md b/jib-gradle-plugin-extension-api/CHANGELOG.md new file mode 100644 index 0000000000..2a19f8f179 --- /dev/null +++ b/jib-gradle-plugin-extension-api/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +### Changed + +### Fixed + +## 0.4.0 + +### Changed + +- Upgraded jib-build-plan to 0.4.0. ([#2660](https://github.com/GoogleContainerTools/jib/pull/2660)) diff --git a/jib-gradle-plugin-extension-api/build.gradle b/jib-gradle-plugin-extension-api/build.gradle new file mode 100644 index 0000000000..1478f30127 --- /dev/null +++ b/jib-gradle-plugin-extension-api/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' +} + +dependencies { + api dependencyStrings.BUILD_PLAN + api dependencyStrings.EXTENSION_COMMON + api gradleApi() +} + +jar { + manifest { + attributes 'Implementation-Version': archiveVersion + attributes 'Automatic-Module-Name': 'com.google.cloud.tools.jib.gradle.extension' + + // OSGi metadata + attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib.gradle.extension' + attributes 'Bundle-Name': 'Extension API for Jib Gradle Plugin' + attributes 'Bundle-Vendor': 'Google LLC' + attributes 'Bundle-DocURL': 'https://github.com/GoogleContainerTools/jib' + attributes 'Bundle-License': 'https://www.apache.org/licenses/LICENSE-2.0' + attributes 'Export-Package': 'com.google.cloud.tools.jib.gradle.extension' + } +} + +/* RELEASE */ +configureMavenRelease() + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Extension API for Jib Gradle Plugin' + description = 'Provides API to extend Jib Gradle Plugin containerization.' + } + from components.java + } + } +} + +// Release plugin (git release commits and version updates) +release { + tagTemplate = 'v$version-gradle-extension' + git { + requireBranch = /^gradle-extension-release-v\d+.*$/ //regex + } +} +/* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +/* ECLIPSE */ diff --git a/jib-gradle-plugin-extension-api/gradle.properties b/jib-gradle-plugin-extension-api/gradle.properties new file mode 100644 index 0000000000..6b7e8ff821 --- /dev/null +++ b/jib-gradle-plugin-extension-api/gradle.properties @@ -0,0 +1 @@ +version = 0.4.1-SNAPSHOT diff --git a/jib-gradle-plugin-extension-api/kokoro/release_build.sh b/jib-gradle-plugin-extension-api/kokoro/release_build.sh new file mode 100755 index 0000000000..d227fb34c6 --- /dev/null +++ b/jib-gradle-plugin-extension-api/kokoro/release_build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Fail on any error. +set -o errexit +# Display commands to stderr. +set -o xtrace + +cd github/jib +./gradlew :jib-gradle-plugin-extension-api:prepareRelease diff --git a/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/GradleData.java b/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/GradleData.java new file mode 100644 index 0000000000..11c2ac36a8 --- /dev/null +++ b/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/GradleData.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.extension; + +import org.gradle.api.Project; + +/** Holds Gradle-specific data and properties. */ +public interface GradleData { + + Project getProject(); +} diff --git a/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/JibGradlePluginExtension.java b/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/JibGradlePluginExtension.java new file mode 100644 index 0000000000..e1c65c76c2 --- /dev/null +++ b/jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/JibGradlePluginExtension.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.extension; + +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.plugins.extension.ExtensionLogger; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtension; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; +import java.util.Map; +import java.util.Optional; + +/** + * Jib Gradle plugin extension API. + * + *

    If a class implementing the interface is visible on the classpath of the Jib Gradle plugin and + * the plugin is configured to load the extension class, the Jib plugin extension framework calls + * the interface method of the class. + */ +public interface JibGradlePluginExtension extends JibPluginExtension { + + /** + * The type of an custom configuration defined by this extension. The configuration object is + * mapped from {@code pluginExtensions.pluginExtension.configuration}. Often, it is sufficient to + * leverage {@code pluginExtensions.pluginExtension.properties} and the extension may not wish to + * define a custom configuration; in that case, use {@link Void} for <T> and have this + * method return {@code Optional#empty()}. (Don't return {@code Optional.of(Void.class)}.) + * + * @return type of an extension-specific custom configuration; {@code Optional.empty()} if no need + * to define custom configuration + */ + Optional> getExtraConfigType(); + + /** + * Extends the build plan prepared by the Jib Gradle plugin. + * + * @param buildPlan original build plan prepared by the Jib Gradle plugin + * @param properties custom properties configured for the plugin extension + * @param extraConfig extension-specific custom configuration mapped from {@code + * jib.pluginExtensions.pluginExtension.configuration} of type type <T>. {@link + * Optional#empty()} when {@link #getExtraConfigType()} returns {@link Optional#empty()} or + * {@code pluginExtension.configuration} is not specified by the extension user. + * @param gradleData {@link GradleData} providing Gradle-specific data and properties + * @param logger logger for writing log messages + * @return updated build plan + * @throws JibPluginExtensionException if an error occurs while running the plugin extension + */ + ContainerBuildPlan extendContainerBuildPlan( + ContainerBuildPlan buildPlan, + Map properties, + Optional extraConfig, + GradleData gradleData, + ExtensionLogger logger) + throws JibPluginExtensionException; +} diff --git a/jib-gradle-plugin/.gitignore b/jib-gradle-plugin/.gitignore deleted file mode 100644 index 898930fb52..0000000000 --- a/jib-gradle-plugin/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -build -target -out -*.iml -*.ipr -*.iws -.idea -.gradle -/.settings -/.classpath -/.project diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c3426cb817..0916ff372c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,84 +9,827 @@ All notable changes to this project will be documented in this file. ### Fixed -- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) +## 3.5.4 + +### Changed +- deps: Update Jackson BOM version to 2.21.1 (#4495) + +### Fixed +- fix: ignore unknown "artifactType" field in manifest descriptors (#4509) + +## 3.5.3 + +### Fixed +- fix: _jibSkaffoldFilesV2 task fails on Gradle 9.0 ([#4469](https://github.com/GoogleContainerTools/jib/issues/4469)) + +## 3.5.2 + +### Added +- feat: Avoid use of StartParameter.getSettingsFile() in jibSkaffoldFilesV2 when Gradle version is 9 or higher + +## 3.5.1 + +### Added +- feat: support Java 25 main methods + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.9 + +## 3.5.0 + +### Added +- feat: add default base image for Java 25 (#4436) + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.8 for java 25 support + +## 3.4.5 + +### Fixed +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4267) + + +## 3.4.4 +- fix: allow pushing images with different arch/os to docker daemon [#4265](https://github.com/GoogleContainerTools/jib/issues/4265) +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4267) + +## 3.4.3 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + +## 3.4.2 + +### Changed +- deps: bump org.apache.commons:commons-compress from 1.21 to 1.26.0 ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) + +### Fixed +- fix: set PAX headers to address build reproducibility issue ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) +- fix: (WAR Containerization) modify default entrypoint to `java -jar /usr/local/jetty/start.jar --module=ee10-deploy` for Jetty 12+ compatibility ([#4216](https://github.com/GoogleContainerTools/jib/pull/4216)) + +## 3.4.1 + +### Fixed +- fix: support parsing manifest JSON containing `LayerSources:` from latest Docker. ([#4171](https://github.com/GoogleContainerTools/jib/pull/4171)) + +## 3.4.0 + +### Added +- feat: support gradle lazy configuration for entrypoint container parameter. ([#4003](https://github.com/GoogleContainerTools/jib/pull/4003)) + +### Changed +- deps: bump com.github.luben:zstd-jni from 1.5.5-2 to 1.5.5-4. ([#4049](https://github.com/GoogleContainerTools/jib/pull/4049/)) +- deps: bump com.fasterxml.jackson:jackson-bom from 2.15.0 to 2.15.2. ([#4055](https://github.com/GoogleContainerTools/jib/pull/4055)) +- deps: bump com.google.guava:guava from 32.0.1-jre to 32.1.2-jre ([#4078](https://github.com/GoogleContainerTools/jib/pull/4078)) +- deps: bump org.slf4j:slf4j-simple from 2.0.7 to 2.0.9. ([#4098](https://github.com/GoogleContainerTools/jib/pull/4098)) + +### Fixed +- fix: fix WWW-Authenticate header parsing for Basic authentication ([#4035](https://github.com/GoogleContainerTools/jib/pull/4035/)) +- Fixed Gradle deprecations for Gradle 8.2. ([#3892](https://github.com/GoogleContainerTools/jib/pull/3892)) + +## 3.3.2 + +### Added +- Support lazy configuration for `jib.container.mainClass` and `jib.container.jvmFlags` parameters ([#3936](https://github.com/GoogleContainerTools/jib/pull/3936)) + +### Changed +- Log an info instead of warning when entrypoint makes the image to ignore jvm parameters ([#3904](https://github.com/GoogleContainerTools/jib/pull/3904)) + +Thanks to our community contributors @rmannibucau, @erdi! + +## 3.3.1 + +### Added +- Added lazy evaluation for `jib.container.creationTime` and `jib.container.filesModificationTime` parameters using Gradle Property and Provider. ([#3709](https://github.com/GoogleContainerTools/jib/pull/3709)) + +### Changed +- Upgraded Google HTTP libraries to 1.42.2 ([#3745](https://github.com/GoogleContainerTools/jib/pull/3745)) + +### Fixed +- Fixed issue with `jibBuildTar`'s `UP-TO-DATE` check by adding back main `SourceSet`'s outputs to task dependency ([#3793](https://github.com/GoogleContainerTools/jib/pull/3793)) + +Thanks to our community contributors @creckord! + +## 3.3.0 + +### Added + +- Included `imagePushed` field to image metadata json output file which provides information on whether an image was pushed by Jib. Note that the output file is `build/jib-image.json` by default or configurable with `jib.outputPaths.imageJson`. ([#3641](https://github.com/GoogleContainerTools/jib/pull/3641)) +- Added lazy evaluation for `jib.extraDirectories` parameters using Gradle Property and Provider. ([#3737](https://github.com/GoogleContainerTools/jib/issues/3737)) +- Better error messaging when environment map in `container.environment` contains null values ([#3672](https://github.com/GoogleContainerTools/jib/pull/3672)). +- Support for OCI image index manifests ([#3715](https://github.com/GoogleContainerTools/jib/pull/3715)). +- Support for base image layer compressed with zstd ([#3717](https://github.com/GoogleContainerTools/jib/pull/3717)). + +### Changed + +- Upgraded slf4j-api to 2.0.0 ([#3735](https://github.com/GoogleContainerTools/jib/pull/3735)). +- Upgraded nullaway to 0.9.9 ([#3720](https://github.com/GoogleContainerTools/jib/pull/3720)) +- Jib now only checks for file existence instead of running the executable passed into `dockerClient.executable` for the purpose of verifying if docker is installed correctly. Users are responsible for ensuring that the docker executable specified through this property is valid and has the correct permissions ([#3744](https://github.com/GoogleContainerTools/jib/pull/3744)). +- Jib now throws an exception when the base image doesn't support target platforms during multi-platform build ([#3707](https://github.com/GoogleContainerTools/jib/pull/3707)). + +Thanks to our community contributors @wwadge, @oliver-brm, @rquinio and @gsquared94! + +## 3.2.1 + +### Added + +- Environment variables can now be used in configuring credential helpers. ([#2814](https://github.com/GoogleContainerTools/jib/issues/2814)) + ```gradle + jib.to { + image = 'myimage' + credHelper { + helper = 'ecr-login' + environment = [ + AWS_PROFILE: 'profile' + ] + } + } + ``` + +### Changed +- Upgraded jackson-databind to 2.13.2.2 ([#3612](https://github.com/GoogleContainerTools/jib/pull/3612)). + +### Fixed + +- Fixed setting image format in Kotlin ([#3593](https://github.com/GoogleContainerTools/jib/pull/3593)). + +## 3.2.0 + +### Added + +- [`jib.from.platforms`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#from-closure) parameter for multi-architecture image building can now be configured through Maven and system properties (for example, `-Djib.from.platforms=linux/amd64,linux/arm64` on the command-line). ([#2742](https://github.com/GoogleContainerTools/jib/pull/2742)) +- For retrieving credentials, Jib additionally looks for `$XDG_RUNTIME_DIR/containers/auth.json`, `$XDG_CONFIG_HOME/containers/auth.json`, and `$HOME/.config/containers/auth.json`. ([#3524](https://github.com/GoogleContainerTools/jib/issues/3524)) + + +### Changed + +- Changed the default base image of the Jib CLI `jar` command from the `adoptopenjdk` images to the [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin) on Docker Hub. Note that Temurin (by Adoptium) is the new name of AdoptOpenJDK. ([#3483](https://github.com/GoogleContainerTools/jib/issues/3483)) +- Build will fail if `extraDirectories.paths` contain `from` directory that doesn't exist locally ([#3542](https://github.com/GoogleContainerTools/jib/issues/3542)) + +### Fixed + +- Fixed `ClassCastException` when using non-`String` value (for example, [`Provider`](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html)) for `Main-Class` manifest attribute of the `jar` task. ([#3396](https://github.com/GoogleContainerTools/jib/issues/3396)) +- Fixed incorrect parsing with comma escaping when providing Jib list or map property values on the command-line. ([#2224](https://github.com/GoogleContainerTools/jib/issues/2224)) + +## 3.1.4 + +### Changed + +- Downgraded Google HTTP libraries to 1.34.0 to resolve network issues. ([#3415](https://github.com/GoogleContainerTools/jib/pull/3415), [#3058](https://github.com/GoogleContainerTools/jib/issues/3058), [#3409](https://github.com/GoogleContainerTools/jib/issues/3409)) +- If `allowInsecureRegistries=true`, HTTP requests are retried on I/O errors only after insecure failover is finalized for each server. ([#3422](https://github.com/GoogleContainerTools/jib/issues/3422)) + +## 3.1.3 + +### Added + +- Increased robustness in registry communications by retrying HTTP requests (to the effect of retrying image pushes or pulls) on I/O exceptions with exponential backoffs. ([#3351](https://github.com/GoogleContainerTools/jib/pull/3351)) +- Now also supports `username` and `password` properties for the `auths` section in a Docker config (`~/.docker/config.json`). (Previously, only supported was a base64-encoded username and password string of the `auth` property.) ([#3365](https://github.com/GoogleContainerTools/jib/pull/3365)) + +### Changed + +- Upgraded Google HTTP libraries to 1.39.2. ([#3387](https://github.com/GoogleContainerTools/jib/pull/3387)) + +## 3.1.2 + +### Fixed + +- Fixed the bug introduced in 3.1 that constructs a wrong Java runtime classpath when two dependencies have the same artifact ID and version but different group IDs. The bug occurs only when using Java 9+ or setting `jib.container.expandClasspathDependencies`. ([#3331](https://github.com/GoogleContainerTools/jib/pull/3331)) + +## 3.1.1 + +### Fixed + +- Fixed the regression introduced in 3.1.0 where a build may fail due to an error from main class inference even if `jib.container.entrypoint` is configured. ([#3295](https://github.com/GoogleContainerTools/jib/pull/3295)) + +## 3.1.0 + +### Added + +- For Google Artifact Registry (`*-docker.pkg.dev`), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) last like it has been doing for `gcr.io`. ([#3241](https://github.com/GoogleContainerTools/jib/pull/3241)) +- Added lazy evaluation for `jib.container.labels` using Gradle Property and Provider. ([#3242](https://github.com/GoogleContainerTools/jib/issues/3242)) + +### Changed + +- Jib now creates an additional layer that contains two small text files: [`/app/jib-classpath-file` and `/app/jib-main-class-file`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin/README.md#custom-container-entrypoint). They hold, respectively, the final Java runtime classpath and the main class computed by Jib that are suitable for app execution on JVM. For example, with Java 9+, setting the container entrypoint to `java --class-path @/app/jib-classpath-file @/app/jib-main-class-file` will work to start the app. (This is basically the default entrypoint set by Jib when the entrypoint is not explicitly configured by the user.) The files are always generated whether Java 8 or 9+, or whether `jib.container.entrypoint` is explicitly configured. The files can be helpful especially when setting a custom entrypoint for a shell script that needs to get the classpath and the main class computed by Jib, or for [AppCDS](https://github.com/GoogleContainerTools/jib/issues/2471). ([#3280](https://github.com/GoogleContainerTools/jib/pull/3280)) +- For Java 9+ apps, the default Java runtime classpath explicitly lists all the app dependencies, preserving the dependency loading order declared by Gradle. This is done by changing the default entrypoint to use the new classpath JVM argument file (basically `java -cp @/app/jib-classpath-file`). As such, `jib.container.expandClasspathDependencies` takes no effect for Java 9+. ([#3280](https://github.com/GoogleContainerTools/jib/pull/3280)) +- Timestamps of file entries in a tarball built with `jibBuildTar` are set to the epoch, making the tarball reproducible. ([#3158](https://github.com/GoogleContainerTools/jib/issues/3158)) + +## 3.0.0 + +### Added + +- New `includes` and `excludes` options for `jib.extraDirectories`. This enables copying a subset of files from the source directory using glob patterns. ([#2564](https://github.com/GoogleContainerTools/jib/issues/2564)) +- Added an option `configurationName` to specify the name of the [Gradle Configuration](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html) to use. The option can be lazily configured, for example, using Gradle `Provider` or `Property`. ([#3034](https://github.com/GoogleContainerTools/jib/pull/3034)) +```gradle + jib { + configurationName = 'myconfig' + } +``` + +### Changed + +- [Switched the default base images](https://github.com/GoogleContainerTools/jib/blob/master/docs/default_base_image.md) from Distroless to [`adoptopenjdk:{8,11}-jre`](https://hub.docker.com/_/adoptopenjdk) and [`jetty`](https://hub.docker.com/_/jetty) (for WAR). ([#3124](https://github.com/GoogleContainerTools/jib/pull/3124)) + +### Fixed + +- Fixed an issue where some log messages used color in the "plain" console output. ([#2764](https://github.com/GoogleContainerTools/jib/pull/2764)) + +## 2.8.0 + +### Added + +- Added support for [configuring registry mirrors](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-am-hitting-docker-hub-rate-limits-how-can-i-configure-registry-mirrors) for base images. This is useful when hitting [Docker Hub rate limits](https://www.docker.com/increase-rate-limits). Only public mirrors (such as `mirror.gcr.io`) are supported. ([#3011](https://github.com/GoogleContainerTools/jib/issues/3011)) + +### Changed + +- Build will fail if Jib cannot create or read the [global Jib configuration file](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#global-jib-configuration). ([#2996](https://github.com/GoogleContainerTools/jib/pull/2996)) + +## 2.7.1 + +### Fixed + +- Updated jackson dependency version causing compatibility issues. ([#2931](https://github.com/GoogleContainerTools/jib/issues/2931)) +- Deprecated use of `@Optional` on boolean attribute ([#2930](https://github.com/GoogleContainerTools/jib/issues/2930)) + +## 2.7.0 + +### Added + +- Added an option `jib.container.expandClasspathDependencies` to preserve the order of loading dependencies as configured in a project. The option enumerates dependency JARs instead of using a wildcard (`/app/libs/*`) in the Java runtime classpath for an image entrypoint. ([#1871](https://github.com/GoogleContainerTools/jib/issues/1871), [#1907](https://github.com/GoogleContainerTools/jib/issues/1907), [#2228](https://github.com/GoogleContainerTools/jib/issues/2228), [#2733](https://github.com/GoogleContainerTools/jib/issues/2733)) + - The option is also useful for AppCDS. ([#2471](https://github.com/GoogleContainerTools/jib/issues/2471)) + - Turning on the option may result in a very long classpath string, and the OS may not support passing such a long string to JVM. +- Added lazy evaluation for `jib.(to|from).auth.(username|password)` and `jib.from.image` using Gradle Property and Provider. ([#2905](https://github.com/GoogleContainerTools/jib/issues/2905)) + +### Fixed + +- Fixed `NullPointerException` when pulling an OCI base image whose manifest does not have `mediaType` information. ([#2819](https://github.com/GoogleContainerTools/jib/issues/2819)) +- Fixed build failure when using a Docker daemon base image (`docker://...`) that has duplicate layers. ([#2829](https://github.com/GoogleContainerTools/jib/issues/2829)) + +## 2.6.0 + +### Added + +- Added lazy evaluation for `jib.to.image` and `jib.to.tags` using Gradle Property and Provider. ([#2727](https://github.com/GoogleContainerTools/jib/issues/2727)) +- _Incubating feature_: can now configure multiple platforms (such as architectures) to build multiple images as a bundle and push as a manifest list (also known as a fat manifest). As an incubating feature, there are certain limitations. For example, OCI image indices are not supported, and building a manifest list is supported only for registry pushing (the `jib` task). ([#2523](https://github.com/GoogleContainerTools/jib/issues/2523)) + ```gradle + jib.from { + image = '... image reference to a manifest list ...' + platforms { + platform { + architecture = 'arm64' + os = 'linux' + } + } + } + ``` + +### Changed + +- Previous locally cached base image manifests will be ignored, as the caching mechanism changed to enable multi-platform image building. ([#2730](https://github.com/GoogleContainerTools/jib/pull/2730), [#2711](https://github.com/GoogleContainerTools/jib/pull/2711)) +- Upgraded the ASM library to 9.0 to resolve an issue when auto-inferring main class in Java 15+. ([#2776](https://github.com/GoogleContainerTools/jib/pull/2776)) + +### Fixed + +- Fixed `NullPointerException` during input validation (in Java 9+) when configuring Jib parameters using certain immutable collections (such as `List.of()`). ([#2702](https://github.com/GoogleContainerTools/jib/issues/2702)) +- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784)) +- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134)) +- Throw `IllegalArgumentException` with an error message instead of throwing a `NullPointerException` when `jib.to.tags` is set to a collection containing a `null` value. ([#2760](https://github.com/GoogleContainerTools/jib/issues/2760)) + +## 2.5.1 + +### Fixed + +- Fixed an issue that configuring `jib.from.platforms` was always additive to the default `amd64/linux` platform. ([#2783](https://github.com/GoogleContainerTools/jib/issues/2783)) + +## 2.5.0 + +### Added + +- Also tries `.exe` file extension for credential helpers on Windows. ([#2527](https://github.com/GoogleContainerTools/jib/issues/2527)) +- New system property `jib.skipExistingImages` (false by default) to skip pushing images (manifests) if the image already exists in the registry. ([#2360](https://github.com/GoogleContainerTools/jib/issues/2360)) +- _Incubating feature_: can now configure desired platform (architecture and OS) to select the matching manifest from a Docker manifest list. Currently supports building only one image. OCI image indices are not supported. ([#1567](https://github.com/GoogleContainerTools/jib/issues/1567)) + ```gradle + jib.from { + image = '... image reference to a manifest list ...' + platforms { + platform { + architecture = 'arm64' + os = 'linux' + } + } + } + ``` + +### Fixed + +- Fixed reporting a wrong credential helper name when the helper does not exist on Windows. ([#2527](https://github.com/GoogleContainerTools/jib/issues/2527)) +- Fixed `NullPointerException` when the `"auths":` section in `~/.docker/config.json` has an entry with no `"auth":` field. ([#2535](https://github.com/GoogleContainerTools/jib/issues/2535)) +- Fixed `NullPointerException` to return a helpful message when a server does not provide any message in certain error cases (400 Bad Request, 404 Not Found, and 405 Method Not Allowed). ([#2532](https://github.com/GoogleContainerTools/jib/issues/2532)) +- Now supports sending client certificate (for example, via the `javax.net.ssl.keyStore` and `javax.net.ssl.keyStorePassword` system properties) and thus enabling mutual TLS authentication. ([#2585](https://github.com/GoogleContainerTools/jib/issues/2585), [#2226](https://github.com/GoogleContainerTools/jib/issues/2226)) +- Fixed an issue where Jib cannot infer Kotlin main class that takes no arguments. ([#2666](https://github.com/GoogleContainerTools/jib/pull/2666)) + +## 2.4.0 + +### Added + +- Jib Extension Framework! The framework enables anyone to easily extend and tailor the Jib Gradle plugin behavior to their liking. Check out the new [Jib Extensions](https://github.com/GoogleContainerTools/jib-extensions) GitHub repository to learn more. ([#2401](https://github.com/GoogleContainerTools/jib/issues/2401)) +- Project dependencies in a multi-module WAR project are now stored in a separate "project dependencies" layer (as currently done for a non-WAR project). ([#2450](https://github.com/GoogleContainerTools/jib/issues/2450)) + +### Changed + +- Previous locally cached application layers (`/target/jib-cache`) will be ignored because of changes to the caching selectors. ([#2499](https://github.com/GoogleContainerTools/jib/pull/2499)) + +### Fixed + +- Fixed authentication failure with Azure Container Registry when using an identity token defined in the `auths` section of Docker config (`~/.docker/config.json`). ([#2488](https://github.com/GoogleContainerTools/jib/pull/2488)) + +## 2.3.0 + +### Added + +- `jib.extraDirectories.paths` closure to allow configuring the source and target of an extra directory. ([#1581](https://github.com/GoogleContainerTools/jib/issues/1581)) + +### Fixed + +- Fixed the problem not inheriting `USER` container configuration from a base image. ([#2421](https://github.com/GoogleContainerTools/jib/pull/2421)) +- Fixed wrong capitalization of JSON properties in a loadable Docker manifest when building a tar image. ([#2430](https://github.com/GoogleContainerTools/jib/issues/2430)) +- Fixed an issue when using a base image whose image creation timestamp contains timezone offset. ([#2428](https://github.com/GoogleContainerTools/jib/issues/2428)) +- Fixed an issue inferring a wrong main class or using an invalid main class (for example, Spring Boot project containing multiple main classes). ([#2456](https://github.com/GoogleContainerTools/jib/issues/2456)) + +## 2.2.0 + +### Added + +- Glob pattern support for `jib.extraDirectories.permissions`. ([#1200](https://github.com/GoogleContainerTools/jib/issues/1200)) +- Support for image references with both a tag and a digest. ([#1481](https://github.com/GoogleContainerTools/jib/issues/1481)) +- The `DOCKER_CONFIG` environment variable specifying the directory containing docker configs is now checked during credential retrieval. ([#1618](https://github.com/GoogleContainerTools/jib/issues/1618)) +- Also tries `.cmd` file extension for credential helpers on Windows. ([#2399](https://github.com/GoogleContainerTools/jib/issues/2399)) + +### Changed + +- `jib.container.creationTime` now accepts more timezone formats:`+HHmm`. This allows for easier configuration of creationTime by external systems. ([#2320](https://github.com/GoogleContainerTools/jib/issues/2320)) + +## 2.1.0 + +### Added + +- Additionally reads credentials from `~/.docker/.dockerconfigjson` and legacy Docker config (`~/.docker/.dockercfg`). Also searches for `$HOME/.docker/*` (in addition to current `System.get("user.home")/.docker/*`). This may help retrieve credentials, for example, on Kubernetes. ([#2260](https://github.com/GoogleContainerTools/jib/issues/2260)) +- New skaffold configuration options that modify how jib's build config is presented to skaffold ([#2292](https://github.com/GoogleContainerTools/jib/pull/2292)): + - `jib.skaffold.watch.buildIncludes`: a list of build files to watch + - `jib.skaffold.watch.includes`: a list of project files to watch + - `jib.skaffold.watch.excludes`: a list of files to exclude from watching + - `jib.skaffold.sync.excludes`: a list of files to exclude from sync'ing + +### Fixed + +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) +- Fixed build failures with Skaffold when the Gradle Java Platform plugin is applied. ([#2269](https://github.com/GoogleContainerTools/jib/issues/2269)) +- For Spring Boot projects using `containerizingMode = 'packaged'`, Jib now overrides `archiveClassifier` of the `jar` task only when safe and necessary. ([#2278](https://github.com/GoogleContainerTools/jib/issues/2278)) +- Fixed an issue where user-configured task dependencies for the Jib task is overwritten and thus ineffective. ([#2289](https://github.com/GoogleContainerTools/jib/pull/2289)) + +## 2.0.0 + +### Added + +- Added json output file for image metadata after a build is complete. Writes to `build/jib-image.json` by default, configurable with `jib.outputPaths.imageJson`. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) +- Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) + +### Changed + +- Removed `jibDockerBuild.dockerClient` in favor of `jib.dockerClient`. ([#1983](https://github.com/GoogleContainerTools/jib/issues/1983)) +- Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) +- Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- Requires Gradle 5.1 or newer (up from 4.9). +- The default base image cache location has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` + - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location + +### Fixed + +- `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Now `jib.containerizingMode='packaged'` works as intended with Spring Boot projects that generate a fat JAR. ([#2178](https://github.com/GoogleContainerTools/jib/pull/2178)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) + +## 1.8.0 + +### Changed + +- Requires Gradle 4.9 or newer (up from 4.6). +- Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + +### Fixed + +- Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) +- Now correctly uses the `war` task if it is enabled and the `bootWar` task is disabled for Spring WAR projects. ([#2096](https://github.com/GoogleContainerTools/jib/issues/2096)) +- `allowInsecureRegistries` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) +- Fixed inefficient communications when interacting with insecure registries and servers (when `allowInsecureRegistries` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) + +## 1.7.0 + +### Added + +- `jib.outputPaths` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) + - `jib.outputPaths.tar` configures output path of `jibBuildTar` (`build/jib-image.tar` by default) + - `jib.outputPaths.digest` configures the output path of the image digest (`build/jib-image.digest` by default) + - `jib.outputPaths.imageId` configures output path of the image id (`build/jib-image.id` by default) +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) + +### Changed + +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `docker://` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) + +### Fixed + +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) +- Fixed `tar://` base image failing if tar does not contain explicit directory entries. ([#2067](https://github.com/GoogleContainerTools/jib/issues/2067)) + +## 1.6.1 + +### Fixed + +- Fixed an issue with using custom base images in Java 12+ projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) + +## 1.6.0 + +### Added + +- Support for local base images by prefixing `jib.from.image` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) + +### Changed + +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- `jib.dockerClient` is now configurable on all tasks, not just `jibDockerBuild`. ([#1932](https://github.com/GoogleContainerTools/jib/issues/1932)) +- `jibDockerBuild.dockerClient` is deprecated in favor of `jib.dockerClient`. + +### Fixed + +- Fixed the regression of slow network operations introduced at 1.5.0. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when `allowInsecureRegistries` is set. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) + +## 1.5.1 + +### Fixed + +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) + +## 1.5.0 + +### Added + +- Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) + +### Changed + +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `jib.container.useCurrentTimestamp` has been deprecated in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) + +## 1.4.0 + +### Added + +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'`. ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) +- Now automatically supports WAR created by the Spring Boot Gradle Plugin via the `bootWar` task. ([#1786](https://github.com/GoogleContainerTools/jib/issues/1786)) +- Can now use `jib.from.image = 'scratch'` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) + +### Changed + +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies. ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) + +### Fixed + +- Re-enabled cross-repository blob mounts. ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + +## 1.3.0 + +### Changed + +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers. ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) + +### Fixed + +- Fixed an issue with `jibBuildTar` where `UP-TO-DATE` checks were incorrect. ([#1757](https://github.com/GoogleContainerTools/jib/issues/1757)) + +## 1.2.0 + +### Added + +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath`. ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline`. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) +- Now supports multiple extra directories with `jib.extraDirectories.{paths|.permissions}`. ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) + +### Changed + +- `jib.extraDirectory({.path|.permissions})` are deprecated in favor of the new `jib.extraDirectories.{paths|.permissions}` configurations. ([#1671](https://github.com/GoogleContainerTools/jib/pull/1671)) + +### Fixed + +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) + +## 1.1.2 + +### Fixed + +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) + +## 1.1.1 + +### Fixed + +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + +## 1.1.0 + +### Changed + +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) + +### Fixed + +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) + +## 1.0.2 + +### Added + +- Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) +- Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) + +### Fixed + +- Fixed an issue where setting `allowInsecureRegistries` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) +- Crash on talking to servers that do not set the `Content-Length` HTTP header or send an incorrect value. ([#1512](https://github.com/GoogleContainerTools/jib/issues/1512)) + +## 1.0.1 + +### Added + +- Java 9+ projects are now supported and run on the distroless Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + +### Fixed + +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + +## 1.0.0 + +### Changed + +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy. ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + +## 1.0.0-rc2 + +### Added + +- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. + +### Changed + +- Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) + +## 1.0.0-rc1 + +### Added + +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories. ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages. ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) + +### Changed + +- Removed `jib.useOnlyProjectCache` parameter in favor of the `jib.useOnlyProjectCache` system property. ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) + +### Fixed + +- Builds failing due to dependency JARs with the same name. ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) + +## 0.10.1 + +### Added + +- Image ID is now written to `build/jib-image.id`. ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) +- `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. +- `container.workingDirectory` configuration parameter to set the working directory. ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) + +### Changed + +- Removed `jibExportDockerContext` task. ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) + +### Fixed + +- NullPointerException thrown with incomplete `auth` configuration. ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) + +## 0.10.0 + +### Added + +- Properties for each configuration parameter, allowing any parameter to be set via commandline. ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) +- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable. ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `container.user` configuration parameter to configure the user and group to run the container as. ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects. ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) +- `jib.extraDirectory` closure with a `path` and `permissions` field. ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) + - `jib.extraDirectory.path` configures the extra layer directory (still also configurable via `jib.extraDirectory = file(...)`) + - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string). +- Image digest is now written to `build/jib-image.digest`. ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) +- Adds the layer type to the layer history as comments. ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) +- `jibDockerBuild.dockerClient.executable` and `jibDockerBuild.dockerClient.environment` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary. ([#1214](https://github.com/GoogleContainerTools/jib/pull/1214)) + +### Changed + +- Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container`. ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) +- `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support. ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- Sets tag to "latest" instead of "unspecified" if `jib.to.image` and project version are both unspecified when running `jibDockerBuild` or `jibBuildTar`. ([#1096](https://github.com/GoogleContainerTools/jib/issues/1096)) + +## 0.9.13 + +### Fixed + +- Adds environment variable configuration to Docker context generator. ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) + +## 0.9.12 + +### Fixed + +- `Cannot access 'image': it is public in ` error. ([#1060](https://github.com/GoogleContainerTools/jib/issues/1060)) + +## 0.9.11 + +### Added + +- `container.environment` configuration parameter to configure environment variables. ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image. ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `jib.to.tags` (list) defines additional tags to push to. ([#978](https://github.com/GoogleContainerTools/jib/pull/978)) + +### Fixed + +- Keep duplicate layers to match container history. ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) + +## 0.9.10 + +### Added + +- `container.labels` configuration parameter for configuring labels. ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `container.entrypoint` configuration parameter to set the entrypoint. ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) +- `history` to layer metadata. ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image. ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) + +### Fixed + +- Corrects permissions for directories in the container filesystem. ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + +## 0.9.9 + +### Added + +- Passthrough labels from base image. ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) + +### Changed + +- Reordered classpath in entrypoint to use _resources_, _classes_, and then _dependencies_, to allow dependency patching. + . ([#777](https://github.com/GoogleContainerTools/jib/issues/777)). Note that this classpath ordering differs from that used by Gradle's `run` task. +- Changed logging level of missing build output directory message. ([#677](https://github.com/GoogleContainerTools/jib/issues/677)) + +### Fixed + +- Gradle project dependencies have their `assemble` task run before running a jib task. ([#815](https://github.com/GoogleContainerTools/jib/issues/815)) + +## 0.9.8 + +### Added + +- Docker context generation now includes snapshot dependencies and extra files. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true`. ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) + +### Changed + +- Propagates environment variables from the base image. ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates). ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) + +### Fixed + +- Slow image reference parsing. ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth. ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same. ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) + +## 0.9.7 + +### Added + +- Snapshot dependencies are added as their own layer. ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load`. ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- `container.useCurrentTimestamp` parameter to set the image creation time to the build time. ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) +- Authentication over HTTP using the `sendCredentialsOverHttp` system property. ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property. ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) +- Docker context export command-line option `--targetDir` to `--jibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) + +### Changed + +- Docker context export command-line option `--targetDir` to `--jibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) + +### Fixed + +- Using multi-byte characters in container configuration. ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config. ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) + +## 0.9.6 + +### Fixed + +- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true`. ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 ### Added -- Incubating feature to build `src/main/jib` as extra layer in image ([#562](https://github.com/GoogleContainerTools/jib/pull/562)) +- Incubating feature to build `src/main/jib` as extra layer in image. ([#562](https://github.com/GoogleContainerTools/jib/pull/562)) ## 0.9.4 ### Fixed -- Fixed handling case-insensitive `Basic` authentication method ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) -- Fixed regression that broke pulling base images from registries that required token authentication ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) +- Fixed handling case-insensitive `Basic` authentication method. ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) +- Fixed regression that broke pulling base images from registries that required token authentication. ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) ## 0.9.3 ### Fixed -- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol) ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) +- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol). ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) ## 0.9.2 ### Added -- Can configure `jibExportDockerContext` output directory with `jibExportDockerContext.targetDir` ([#492](https://github.com/GoogleContainerTools/jib/pull/492)) +- Can configure `jibExportDockerContext` output directory with `jibExportDockerContext.targetDir`. ([#492](https://github.com/GoogleContainerTools/jib/pull/492)) ### Changed ### Fixed -- Set `jibExportDockerContext` output directory with command line option `--targetDir` ([#499](https://github.com/GoogleContainerTools/jib/pull/499)) +- Set `jibExportDockerContext` output directory with command line option `--targetDir`. ([#499](https://github.com/GoogleContainerTools/jib/pull/499)) ## 0.9.1 ### Added -- `container.ports` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`) ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) -- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) +- `container.ports` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`). ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) +- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP. ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) ### Changed -- Fetches credentials from inferred credential helper before Docker config ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) -- Container creation date set to timestamp 0 ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) -- Does not authenticate base image pull unless necessary - reduces build time by about 500ms ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) -- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) -- Warns instead of errors when classes not found ([#462](https://github.com/GoogleContainerTools/jib/pull/462)) +- Fetches credentials from inferred credential helper before Docker config. ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) +- Container creation date set to timestamp 0. ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) +- Does not authenticate base image pull unless necessary - reduces build time by about 500ms. ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) +- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object. ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) +- Warns instead of errors when classes not found. ([#462](https://github.com/GoogleContainerTools/jib/pull/462)) -### Fixed +### Fixed -- Using Azure Container Registry now works - define credentials in `jib.to.auth`/`jib.from.auth` ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) -- Supports `access_token` as alias to `token` in registry authentication ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) -- Docker context export for Groovy project ([#459](https://github.com/GoogleContainerTools/jib/pull/459)) -- Visibility of `jib.to.image` ([#460](https://github.com/GoogleContainerTools/jib/pull/460)) +- Using Azure Container Registry now works - define credentials in `jib.to.auth`/`jib.from.auth`. ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) +- Supports `access_token` as alias to `token` in registry authentication. ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) +- Docker context export for Groovy project. ([#459](https://github.com/GoogleContainerTools/jib/pull/459)) +- Visibility of `jib.to.image`. ([#460](https://github.com/GoogleContainerTools/jib/pull/460)) ## 0.9.0 ### Added -- Export a Docker context (including a Dockerfile) with `jibExportDockerContext` ([#204](https://github.com/google/jib/issues/204)) -- Warns if build may not be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- `jibDockerBuild` gradle task to build straight to Docker daemon ([#265](https://github.com/GoogleContainerTools/jib/pull/265)) -- `mainClass` is inferred by searching through class files if configuration is missing ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) -- All tasks depend on `classes` by default ([#335](https://github.com/GoogleContainerTools/jib/issues/335)) -- Can now specify target image with `--image` ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) -- `args` parameter to define default main arguments ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) +- Export a Docker context (including a Dockerfile) with `jibExportDockerContext`. ([#204](https://github.com/google/jib/issues/204)) +- Warns if build may not be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- `jibDockerBuild` gradle task to build straight to Docker daemon. ([#265](https://github.com/GoogleContainerTools/jib/pull/265)) +- `mainClass` is inferred by searching through class files if configuration is missing. ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) +- All tasks depend on `classes` by default. ([#335](https://github.com/GoogleContainerTools/jib/issues/335)) +- Can now specify target image with `--image`. ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) +- `args` parameter to define default main arguments. ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) ### Changed -- Removed `reproducible` parameter - application layers will always be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- Removed `reproducible` parameter - application layers will always be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) ### Fixed -- Using base images that lack entrypoints ([#284](https://github.com/GoogleContainerTools/jib/pull/284) +- Using base images that lack entrypoints. ([#284](https://github.com/GoogleContainerTools/jib/pull/284)) ## 0.1.1 ### Added -- Warns if specified `mainClass` is not a valid Java class ([#206](https://github.com/google/jib/issues/206)) -- Can specify registry credentials to use directly with `from.auth` and `to.auth` ([#215](https://github.com/google/jib/issues/215)) +- Warns if specified `mainClass` is not a valid Java class. ([#206](https://github.com/google/jib/issues/206)) +- Can specify registry credentials to use directly with `from.auth` and `to.auth`. ([#215](https://github.com/google/jib/issues/215)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 0fdbbbbe1a..c2e7351366 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,33 +1,59 @@ -[![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-gradle-plugin)](https://github.com/igrigorik/ga-beacon) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.4-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) +[![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Gradle Java project -Jib is [Gradle](https://gradle.org/) plugin for building Docker and OCI images for your Java applications. +Jib is a [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. -For information about the project, see the [Jib project README](../README.md). For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). -## Upcoming Features - -These features are not currently supported but will be added in later releases. +For information about the project, see the [Jib project README](../README.md). -* Support for WAR format -* Run and debug the built container +| ☑️ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + +## Table of Contents + +* [Upcoming Features](#upcoming-features) +* [Quickstart](#quickstart) + * [Setup](#setup) + * [Configuration](#configuration) + * [Build your image](#build-your-image) + * [Build to Docker Daemon](#build-to-docker-daemon) + * [Build an image tarball](#build-an-image-tarball) + * [Run `jib` with each build](#run-jib-with-each-build) + * [Additional Build Artifacts](#additional-build-artifacts) +* [Multi Module Projects](#multi-module-projects) +* [Extended Usage](#extended-usage) + * [System Properties](#system-properties) + * [Global Jib Configuration](#global-jib-configuration) + * [Example](#example) + * [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) + * [Authentication Methods](#authentication-methods) + * [Using Docker configuration files](#using-docker-configuration-files) + * [Using Docker Credential Helpers](#using-docker-credential-helpers) + * [Using Specific Credentials](#using-specific-credentials) + * [Custom Container Entrypoint](#custom-container-entrypoint) + * [Reproducible Build Timestamps](#reproducible-build-timestamps) + * [Jib Extensions](#jib-extensions) + * [WAR Projects](#war-projects) + * [Skaffold Integration](#skaffold-integration) +* [Need Help?](#need-help) +* [Community](#community) ## Quickstart ### Setup -*Make sure you are using Gradle version 4.6 or later.* +*Make sure you are using Gradle version 5.1 or later.* In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.4' + id 'com.google.cloud.tools.jib' version '3.5.4' } ``` @@ -83,7 +109,25 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b jib.to.image = 'my-docker-id/my-app' ``` -#### *TODO: Add more examples for common registries.* +#### Using [JFrog Container Registry (JCR)](https://jfrog.com/container-registry) or [JFrog Artifactory](https://jfrog.com/help/r/jfrog-artifactory-documentation/getting-started-with-artifactory-as-a-docker-registry)... + +*Make sure you have a [docker-credential-helper](https://github.com/docker/docker-credential-helpers#available-programs) set up. For example, on macOS, the credential helper would be `docker-credential-osxkeychain`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my-company-docker-local.jfrog.io/my-app`, the configuration would be: + +```groovy +jib.to.image = 'my-company-docker-local.jfrog.io/my-app' +``` + +#### Using [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/)... + +*Make sure you have a [`ACR Docker Credential Helper`](https://github.com/Azure/acr-docker-credential-helper) installed and set up. For example, on Windows, the credential helper would be `docker-credential-acr-windows`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my_acr_name.azurecr.io/my-app`, the configuration would be: + +```groovy +jib.to.image = 'my_acr_name.azurecr.io/my-app' +``` ### Build Your Image @@ -93,7 +137,7 @@ Build your container image with: gradle jib ``` -Subsequent builds are much faster than the initial build. +Subsequent builds are much faster than the initial build. *Having trouble? Let us know by [submitting an issue](/../../issues/new), contacting us on [Gitter](https://gitter.im/google/jib), or posting to the [Jib users forum](https://groups.google.com/forum/#!forum/jib-users).* @@ -105,13 +149,29 @@ Jib can also build your image directly to a Docker daemon. This uses the `docker gradle jibDockerBuild ``` -If you are using [`minikube`](https://github.com/kubernetes/minikube)'s remote Docker daemon, make sure you [set up the correct environment variables](https://github.com/kubernetes/minikube/blob/master/docs/reusing_the_docker_daemon.md) to point to the remote daemon: +If you are using [`minikube`](https://github.com/kubernetes/minikube)'s remote Docker daemon, make sure you [set up the correct environment variables](https://minikube.sigs.k8s.io/docs/handbook/pushing/#1-pushing-directly-to-the-in-cluster-docker-daemon-docker-env) to point to the remote daemon: ```shell eval $(minikube docker-env) gradle jibDockerBuild ``` +Alternatively, you can set environment variables in the Jib configuration. See [`dockerClient`](#dockerclient-closure) for more configuration options. + +#### Build an image tarball + +You can build and save your image to disk as a tarball with: + +```shell +gradle jibBuildTar +``` + +This builds and saves your image to `build/jib-image.tar`, which you can load into docker with: + +```shell +docker load --input build/jib-image.tar +``` + ### Run `jib` with each build You can also have `jib` run with each build by attaching it to the `build` task: @@ -122,25 +182,14 @@ tasks.build.dependsOn tasks.jib Then, ```gradle build``` will build and containerize your application. -### Export to a Docker context +### Additional Build Artifacts -Jib can also export a Docker context so that you can build with Docker, if needed: +As part of an image build, Jib also writes out the _image digest_ and the _image ID_. By default, these are written out to `build/jib-image.digest` and `build/jib-image.id` respectively, but the locations can be configured using the `jib.outputPaths.digest` and `jib.outputPaths.imageId` configuration properties. See [Extended Usage](#outputpaths-closure) for more details. -```shell -gradle jibExportDockerContext -``` +## Multi Module Projects -The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `---jib.dockerDir` parameter: - -```shell -gradle jibExportDockerContext --jib.dockerDir=my/docker/context/ -``` - -You can then build your image with Docker: - -```shell -docker build -t myimage my/docker/context/ -``` +Special handling of project dependencies is recommended when building complex +multi module projects. See [Multi Module Example](https://github.com/GoogleContainerTools/jib/tree/master/examples/multi-module) for detailed information. ## Extended Usage @@ -148,27 +197,34 @@ The plugin provides the `jib` extension for configuration with the following opt Field | Type | Default | Description --- | --- | --- | --- -`from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. +`from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. -`useProjectOnlyCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Maven projects. -`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS. Leaving this parameter set to false is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`extraDirectories` | [`extraDirectories`](#extradirectories-closure) | See [`extraDirectories`](#extradirectories-closure) | Configures the directories used to add arbitrary files to the image. +`outputPaths` | [`outputPaths`](#outputpaths-closure) | See [`outputPaths`](#outputpaths-closure) | Configures the locations of additional build artifacts generated by Jib. +`dockerClient` | [`dockerClient`](#dockerclient-closure) | See [`dockerClient`](#dockerclient-closure) | Configures Docker for building to/from the Docker daemon. +`skaffold` | [`skaffold`](#skaffold-integration) | See [`skaffold`](#skaffold-integration) | Configures the internal skaffold tasks. This configuration should only be used when integrating with [`skaffold`](#skaffold-integration). | +`containerizingMode` | `String` | `exploded` | If set to `packaged`, puts the JAR artifact built by the Gradle Java plugin into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. +`allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. +`configurationName` | `String` | `runtimeClasspath` | Specify the name of the [Gradle Configuration](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html) to use. `from` is a closure with the following properties: -Property | Type | Default | Description ---- | --- | --- | --- -`image` | `String` | `gcr.io/distroless/java` | The image reference for the base image. -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). -`auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). +Property | Type | Default | Description +--- | --- |------------------------------------------------------------| --- +`image` | `String` | `eclipse-temurin:{8,11,17,21,25}-jre` (or `jetty` for WAR) | The image reference for the base image. The source type can be specified using a [special type prefix](#setting-the-base-image). +`auth` | [`auth`](#auth-closure) | *None* | Specifies credentials directly (alternative to `credHelper`). +`credHelper` | `String` | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). +`platforms` | [`platforms`](#platforms-closure) | See [`platforms`](#platforms-closure) | Configures platforms of base images to select from a manifest list. `to` is a closure with the following properties: Property | Type | Default | Description --- | --- | --- | --- -`image` | `String` | *Required* | The image reference for the target image. This can also be specified via the `--image` command line option. -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). -`auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). +`image` | `String` | *Required* | The image reference for the target image. This can also be specified via the `--image` command line option. If the tag is not present here `:latest` is implied. +`auth` | [`auth`](#auth-closure) | *None* | Specifies credentials directly (alternative to `credHelper`). +`credHelper` | `String` | *None* | Specifies a credential helper that can authenticate pushing the target image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). +`tags` | `List` | *None* | Additional tags to push to. `auth` is a closure with the following properties (see [Using Specific Credentials](#using-specific-credentials)): @@ -177,25 +233,139 @@ Property | Type `username` | `String` `password` | `String` +`platforms` can configure multiple `platform` closures. Each individual `platform` has the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`architecture` | `String` | `amd64` | The architecture of a base image to select from a manifest list. +`os` | `String` | `linux` | The OS of a base image to select from a manifest list. + +See [How do I specify a platform in the manifest list (or OCI index) of a base image?](../docs/faq.md#how-do-i-specify-a-platform-in-the-manifest-list-or-oci-index-of-a-base-image) for examples. + `container` is a closure with the following properties: Property | Type | Default | Description --- | --- | --- | --- -`jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. -`mainClass` | `String` | *Inferred\** | The main class to launch your application from. -`args` | `List` | *None* | Default main method arguments to run your application with. -`ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. +`args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. +`creationTime` | `String` | `EPOCH` | Sets the container creation time. (Note that this property does not affect the file modification times, which are configured using `jib.container.filesModificationTime`.) The value can be `EPOCH` to set the timestamps to Epoch (default behavior), `USE_CURRENT_TIMESTAMP` to forgo reproducibility and use the real creation time, or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. The value can also be initialized [lazily](https://docs.gradle.org/current/userguide/lazy_configuration.html) with a provider. +`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags`, `mainClass`, `extraClasspath`, and `expandClasspathDependencies` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* The value can also be initialized [lazily](https://docs.gradle.org/current/userguide/lazy_configuration.html) with a provider. +`environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | `List` | *None* | Additional paths in the container to prepend to the computed Java classpath. +`expandClasspathDependencies` | `boolean` | `false` |

    • Java 8 *or* Jib < 3.1: When set to true, does not use a wildcard (for example, `/app/lib/*`) for dependency JARs in the default Java runtime classpath but instead enumerates the JARs. Has the effect of preserving the classpath loading order as defined by the Gradle project.
    • Java >= 9 *and* Jib >= 3.1: The option has no effect. Jib *always* enumerates the dependency JARs. This is achieved by [creating and using an argument file](#custom-container-entrypoint) for the `--class-path` JVM argument.
    +`filesModificationTime` | `String` | `EPOCH_PLUS_SECOND` | Sets the modification time (last modified time) of files in the image put by Jib. (Note that this does not set the image creation time, which can be set using `jib.container.creationTime`.) The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. The value can also be initialized [lazily](https://docs.gradle.org/current/userguide/lazy_configuration.html) with a provider. `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). +`jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. The value can also be initialized [lazily](https://docs.gradle.org/current/userguide/lazy_configuration.html) with a provider. +`labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). +`mainClass` | `String` | *Inferred*\*\* | The main class to launch your application from. The value can also be initialized [lazily](https://docs.gradle.org/current/userguide/lazy_configuration.html) with a provider. +`ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`user` | `String` | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. +`volumes` | `List` | *None* | Specifies a list of mount points on the container. +`workingDirectory` | `String` | *None* | The working directory in the container. + +`extraDirectories` is a closure with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): + +Property | Type | Default | Description +--- | --- | --- | --- +`paths` | [`paths`](#paths-closure) closure, or `Object` | `(project-dir)/src/main/jib` | May be configured as a closure configuring `path` elements, or as source directory values recognized by [`Project.files()`](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#files-java.lang.Object...-), such as `String`, `File`, `Path`, `List`, etc. +`permissions` | `Map` | *None* | Maps file paths (glob patterns) on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. + +`paths` can configure multiple `path` closures (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)). Each individual `path` has the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`from` | `Object` | `(project-dir)/src/main/jib` | Accepts source directories that are recognized by [`Project.files()`](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#files-java.lang.Object...-), such as `String`, `File`, `Path`, `List`, etc. +`into` | `String` | `/` | The absolute unix path on the container to copy the extra directory contents into. +`includes` | `List` | *None* | Glob patterns for including files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. +`excludes` | `List` | *None* | Glob patterns for excluding files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. + +`outputPaths` is a closure with the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`tar` | `File` | `(project-dir)/build/jib-image.tar` | The path of the tarball generated by `jibBuildTar`. Relative paths are resolved relative to the project root. +`digest` | `File` | `(project-dir)/build/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. +`imageId` | `File` | `(project-dir)/build/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. + +`dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`executable` | `File` | `docker` | Sets the path to the Docker executable that is called to load the image into the Docker daemon. **Please note**: Users are responsible for ensuring that the Docker path passed in is valid and has the right permissions to be executed. +`environment` | `Map` | *None* | Sets environment variables used by the Docker executable. + +#### System Properties + +Each of these parameters is configurable via commandline using system properties. Jib's system properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: +```shell +gradle jib \ + -Djib.to.image=myregistry/myimage:latest \ + -Djib.to.auth.username=$USERNAME \ + -Djib.to.auth.password=$PASSWORD + +gradle jibDockerBuild \ + -Djib.dockerClient.executable=/path/to/docker \ + -Djib.container.environment=key1="value1",key2="value2" \ + -Djib.container.args=arg1,arg2,arg3 +``` + +The following table contains additional system properties that are not available as build configuration parameters: + +Property | Type | Default | Description +--- | --- | --- | --- +`jib.httpTimeout` | `int` | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. +`jib.useOnlyProjectCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Gradle projects. +`jib.baseImageCache` | `File` | *Platform-dependent*\*\*\* | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.applicationCache` | `File` | `[project dir]/build/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. +`jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. + +*\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* + +*\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* + +*\*\*\* The default base image cache is in the following locations on each platform:* + * *Linux: `[cache root]/google-cloud-tools-java/jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/.cache/` if not set)* + * *Mac: `[cache root]/Google/Jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/Library/Caches/` if not set)* + * *Windows: `[cache root]\Google\Jib\Cache`, where `[cache root]` is `%XDG_CACHE_HOME%` (`%LOCALAPPDATA%` if not set)* + +### Global Jib Configuration + +Some options can be set in the global Jib configuration file. The file is at the following locations on each platform: + +* *Linux: `[config root]/google-cloud-tools-java/jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/.config/` if not set)* +* *Mac: `[config root]/Google/Jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/Library/Preferences/Config/` if not set)* +* *Windows: `[config root]\Google\Jib\Config\config.json`, where `[config root]` is `%XDG_CONFIG_HOME%` (`%LOCALAPPDATA%` if not set)* -*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* +#### Properties + +* `disableUpdateCheck`: when set to true, disables the periodic up-to-date version check. +* `registryMirrors`: a list of mirror settings for each base image registry. In the following example, if the base image configured in Jib is for a Docker Hub image, then `mirror.gcr.io`, `localhost:5000`, and the Docker Hub (`registry-1.docker.io`) are tried in order until Jib can successfully pull a base image. + +```json +{ + "disableUpdateCheck": false, + "registryMirrors": [ + { + "registry": "registry-1.docker.io", + "mirrors": ["mirror.gcr.io", "localhost:5000"] + }, + { + "registry": "quay.io", + "mirrors": ["private-mirror.test.com"] + } + ] +} +``` +**Note about `mirror.gcr.io`**: it is _not_ a Docker Hub mirror but a cache. It caches [frequently-accessed public Docker Hub images](https://cloud.google.com/container-registry/docs/pulling-cached-images), and it's often possible that your base image does not exist in `mirror.gcr.io`. In that case, Jib will have to fall back to use Docker Hub. ### Example In this configuration, the image: * Is built from a base of `openjdk:alpine` (pulled from Docker Hub) -* Is pushed to `localhost:5000/my-image:built-with-jib` -* Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` +* Is pushed to `localhost:5000/my-image:built-with-jib`, `localhost:5000/my-image:tag2`, and `localhost:5000/my-image:latest` +* Runs by calling `java -Dmy.property=example.value -Xms512m -Xdebug -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp +* Has two labels (key1:value1 and key2:value2) * Is built as OCI format ```groovy @@ -206,20 +376,121 @@ jib { to { image = 'localhost:5000/my-image/built-with-jib' credHelper = 'osxkeychain' + tags = ['tag2', 'latest'] } container { - jvmFlags = ['-Xms512m', '-Xdebug', '-Xmy:flag=jib-rules'] + jvmFlags = ['-Dmy.property=example.value', '-Xms512m', '-Xdebug'] mainClass = 'mypackage.MyApp' args = ['some', 'args'] ports = ['1000', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] format = 'OCI' } } ``` +### Setting the Base Image + +There are three different types of base images that Jib accepts: an image from a container registry, an image stored in the Docker daemon, or an image tarball on the local filesystem. You can specify which you would like to use by prepending the `jib.from.image` configuration with a special prefix, listed below: + +Prefix | Example | Type +--- | --- | --- +*None* | `openjdk:11-jre` | Pulls the base image from a registry. +`registry://` | `registry://eclipse-temurin:11-jre` | Pulls the base image from a registry. +`docker://` | `docker://busybox` | Retrieves the base image from the Docker daemon. +`tar://` | `tar:///path/to/file.tar` | Uses an image tarball stored at the specified path as the base image. Also accepts relative paths (e.g. `tar://build/jib-image.tar`). + +### Adding Arbitrary Files to the Image + +You can add arbitrary, non-classpath files to the image without extra configuration by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the target directory (`/` by default) in the image, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). + +Note that Jib does not follow symbolic links in the container image. If a symbolic link is present, _it will be removed_ prior to placing the files and directories. + +You can configure different directories by using the `jib.extraDirectories.paths` parameter in your `build.gradle`: +```groovy +jib { + // Copies files from 'src/main/custom-extra-dir' and '/home/user/jib-extras' instead of 'src/main/jib' + extraDirectories.paths = ['src/main/custom-extra-dir', '/home/user/jib-extras'] +} +``` + +Alternatively, the `jib.extraDirectories` parameter can be used as a closure to set custom extra directories, as well as the extra files' permissions on the container: + +```groovy +jib { + extraDirectories { + paths = 'src/main/custom-extra-dir' // Copies files from 'src/main/custom-extra-dir' + permissions = [ + '/path/on/container/to/fileA': '755', // Read/write/execute for owner, read/execute for group/other + '/path/to/another/file': '644', // Read/write for owner, read-only for group/other + '/glob/pattern/**/*.sh': 755 + ] + } +} +``` + +Using `paths` as a closure, you may also specify the target of the copy and include or exclude files: + +```groovy + extraDirectories { + paths { + path { + // copies the contents of 'src/main/extra-dir' into '/' on the container + from = file('src/main/extra-dir') + } + path { + // copies the contents of 'src/main/another/dir' into '/extras' on the container + from = file('src/main/another/dir') + into = '/extras' + } + path { + // copies a single-file.xml + from = 'src/main/resources/xml-files' + into = '/dest-in-container' + includes = ['single-file.xml'] + } + path { + // copies only .txt files except for 'hidden.txt' at the source root + from = 'build/some-output' + into = '/txt-files' + includes = ['*.txt', '**/*.txt'] + excludes = ['hidden.txt'] + } + } + } +``` + +You can also configure `paths` and `permissions` through [lazy configuration in Gradle](https://docs.gradle.org/current/userguide/lazy_configuration.html), using providers in `build.gradle`: + +```groovy +extraDirectories { + paths = project.provider { 'src/main/custom-extra-dir' } + permissions = project.provider { ['/path/on/container/to/fileA': '755'] } +} +``` + +```groovy +extraDirectories { + paths { + path { + from = project.provider { 'src/main/custom-extra-dir' } + into = project.provider { '/dest-in-container' } + includes = project.provider { ['*.txt', '**/*.txt'] } + excludes = project.provider { ['hidden.txt'] } + } + } +} +``` + ### Authentication Methods -Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. +Pushing/pulling from private registries require authorization credentials. + +#### Using Docker configuration files + +* Jib looks from credentials from `$XDG_RUNTIME_DIR/containers/auth.json`, `$XDG_CONFIG_HOME/containers/auth.json`, `$HOME/.config/containers/auth.json`, `$DOCKER_CONFIG/config.json`, and `$HOME/.docker/config.json`. + +See [this issue](/../../issues/101) and [`man containers-auth.json`](https://www.mankier.com/5/containers-auth.json) for more information about the files. #### Using Docker Credential Helpers @@ -230,12 +501,11 @@ Some common credential helpers include: * Google Container Registry: [`docker-credential-gcr`](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper) * AWS Elastic Container Registry: [`docker-credential-ecr-login`](https://github.com/awslabs/amazon-ecr-credential-helper) * Docker Hub Registry: [`docker-credential-*`](https://github.com/docker/docker-credential-helpers) - +* Azure Container Registry: [`docker-credential-acr-*`](https://github.com/Azure/acr-docker-credential-helper) Configure credential helpers to use by specifying them as a `credHelper` for their respective image in the `jib` extension. -*Example configuration:* +*Example configuration:* ```groovy jib { from { @@ -272,7 +542,7 @@ jib { } ``` -These credentials can be stored in `gradle.properties`, retrieved from a command (like `gcloud auth print-access-token`), or read in from a file. +These credentials can be stored in `gradle.properties`, retrieved from a command (like `gcloud auth print-access-token`), or read in from a file. For example, you can use a key file for authentication (for GCR, see [Using a JSON key file](https://cloud.google.com/container-registry/docs/advanced-authentication#using_a_json_key_file)): @@ -288,14 +558,115 @@ jib { } ``` -## How Jib Works -See the [Jib project README](/../../#how-jib-works). +### Custom Container Entrypoint -## Frequently Asked Questions (FAQ) +If you don't set `jib.container.entrypoint`, the default container entrypoint to launch your app will be basically `java -cp `. (The final `java` command can be further configured by setting `jib.container.{jvmFlags|args|extraClasspath|mainClass|expandClasspathDependencies}`.) -See the [Jib project README](/../../#frequently-asked-questions-faq). +Sometimes, you'll want to set a custom entrypoint to use a shell to wrap the `java` command. For example, to let `sh` or `bash` [expand environment variables](https://stackoverflow.com/a/59361658/1701388), or to have more sophisticated logic to construct a launch command. (Note, however, that running a command with a shell forks a new child process unless you run it with `exec` like `sh -c "exec java ..."`. Whether to run the JVM process as PID 1 or a child process of a PID-1 shell is a [decision you should make carefully](https://github.com/GoogleContainerTools/distroless/issues/550#issuecomment-791610603).) In this scenario, you will want to have a way inside a shell script to reliably know the default runtime classpath and the main class that Jib would use by default. To help this, Jib >= 3.1 creates two JVM argument files under `/app` (the default app root) inside the built image. + +- `/app/jib-classpath-file`: runtime classpath that Jib would use for default app launch +- `/app/jib-main-class-file`: main class + +Therefore, *for example*, the following commands will be able to launch your app: + +- (Java 9+) `java -cp @/app/jib-classpath-file @/app/jib-main-class-file` +- (with shell) `java -cp $( cat /app/jib-classpath-file ) $( cat /app/jib-main-class-file )` + +### Reproducible Build Timestamps + +To ensure that a Jib build is reproducible, Jib sets the image creation time to the Unix epoch (00:00:00, January 1st, 1970 in UTC) and all file modification times to one second past the epoch by default. See the [Jib FAQ](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#why-is-my-image-created-48-years-ago) for more details on reproducible builds. + +Another, more complex way to achieve reproducible builds with stable creation times is to leverage commit timestamps from the project's SCM. For example, the [gradle-git-properties](https://plugins.gradle.org/plugin/com.gorylenko.gradle-git-properties) plugin can be used to inject Git commit information into the current build. These can then be used to configure `jib.container.creationTime`. Since the actual Git information is not yet available at the time the build is configured, it needs to be set through [lazy configuration in Gradle](https://docs.gradle.org/current/userguide/lazy_configuration.html), using a provider in `build.gradle`: + +```groovy +jib { + container { + creationTime = project.provider { project.ext.git['git.commit.time'] } + } +} +``` + +This would build an image with the creation time set to the time of the latest commit from `project.ext.git['git.commit.time']`. + +### Jib Extensions + +The Jib build plugins have an extension framework that enables anyone to easily extend Jib's behavior to their needs. We maintain select [first-party](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party) plugins for popular use cases like [fine-grained layer control](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-layer-filter-extension-gradle) and [Quarkus support](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-quarkus-extension-gradle), but anyone can write and publish an extension. Check out the [jib-extensions](https://github.com/GoogleContainerTools/jib-extensions) repository for more information. + + +### WAR Projects + +Jib also containerizes WAR projects. If the Gradle project uses the [WAR Plugin](https://docs.gradle.org/current/userguide/war_plugin.html), Jib will by default use [`jetty`](https://hub.docker.com/_/jetty) as a base image to deploy the project WAR. No extra configuration is necessary other than using the WAR Plugin to make Jib build WAR images. + +Note that Jib will work slightly differently for WAR projects from JAR projects: + - `container.mainClass` and `container.jvmFlags` are ignored. + - The WAR will be exploded into `/var/lib/jetty/webapps/ROOT`, which is the expected WAR location for the Jetty base image. + +To use a different Servlet engine base image, you can customize `container.appRoot`, `container.entrypoint`, and `container.args`. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may not need to configure them. However, you will most likely have to set `container.appRoot` to a proper location depending on the base image. Here is an example of using a Tomcat image: + +```gradle +jib { + from.image = 'tomcat:8.5-jre8-alpine' + + // For demonstration only: this directory in the base image contains a Tomcat default + // app (welcome page), so you may first want to delete this directory in the base image. + container.appRoot = '/usr/local/tomcat/webapps/ROOT' +} +``` +When specifying a [`jetty`](https://hub.docker.com/_/jetty) image yourself with `from.image`, you may run into an issue ([#3204](https://github.com/GoogleContainerTools/jib/issues/3204)) and need to override the entrypoint. +```gradle +jib { + from.image = 'jetty:11.0.2-jre11' + container.entrypoint = ['java', '-jar', '/usr/local/jetty/start.jar'] +} +``` + + +### Skaffold Integration + +Jib is an included builder in [Skaffold](https://github.com/GoogleContainerTools/skaffold). Jib passes build information to skaffold through special internal tasks so that skaffold understands when it should rebuild or synchronize files. For complex builds, the defaults may not be sufficient, so the `jib` extension provides a `skaffold` configuration closure which exposes: + +Field | Type | Default | Description +--- | --- | --- | --- +`watch` | [`watch`](#skaffold-watch-closure) | *None* | Additional configuration for file watching +`sync` | [`sync`](#skaffold-sync-closure) | *None* | Additional configuration for file synchronization + +`watch` is a closure with the following properties: + +Field | Type | Default | Description +--- | --- | --- | --- +`buildIncludes` | `List` | *None* | Additional build files that skaffold should watch +`includes` | `List` | *None* | Additional project files or directories that skaffold should watch +`excludes` | `List` | *None* | Files and directories that skaffold should not watch + +`sync` is a closure with the following properties: + +Field | Type | Default | Description +--- | --- | --- | --- +`excludes` | `List` | *None* | Files and directories that skaffold should not sync + +## Need Help? + +A lot of questions are already answered! + +* [Frequently Asked Questions (FAQ)](../docs/faq.md) +* [Stack Overflow](https://stackoverflow.com/questions/tagged/jib) +* [GitHub issues](https://github.com/GoogleContainerTools/jib/issues) + +_For usage questions, please ask them on Stack Overflow._ + +## Privacy + +See the [Privacy page](docs/privacy.md). + +## Upcoming Features + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. ## Community See the [Jib project README](/../../#community). + +## Disclaimer + +This is not an officially supported Google product. diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index d94cbe65c9..46f416ab1e 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -1,173 +1,93 @@ plugins { - id 'java-gradle-plugin' - id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.7.1' - - // For local install - id 'maven' - - // Error-prone checker - id 'net.ltgt.apt' version '0.13' - id 'net.ltgt.errorprone' version '0.0.13' - - // Prepare release - id 'net.researchgate.release' version '2.6.0' - // Gradle Plugin Portal publish - id 'com.gradle.plugin-publish' version '0.9.10' -} - -group 'com.google.cloud.tools' - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -repositories { - // Use jcenter for gradle plugin portal releases. - jcenter() -} - -sourceSets { - main { - java.srcDir file('../jib-core/src/main/java') - resources.srcDir file('../jib-core/src/main/resources') - } - test { - java.srcDir file('../jib-core/src/test/java') - resources.srcDir file('../jib-core/src/test/resources') + id 'java-gradle-plugin' + id 'net.researchgate.release' + id 'com.gradle.plugin-publish' + // for eclipse import modifications + id 'eclipse' +} + +/* LOCAL SNAPSHOT DEV */ +// to install for local testing - do not load this plugin when publishing to plugin portal +// 'maven-publish' and 'com.gradle.plugin-publish' do not interact well with each other +// https://discuss.gradle.org/t/debug-an-issue-in-publish-plugin-gradle-plugin-not-being-prepended-to-groupid/32720 +if (version.contains('SNAPSHOT')) { + apply plugin: 'maven-publish' + + publishing { + repositories { + mavenLocal() } - integrationTest { - java { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/java') - srcDir file('../jib-core/src/integration-test/java') - } - resources { - srcDir file('src/integration-test/resources') - srcDir file('../jib-core/src/integration-test/resources') - } - } -} + } -configurations { - integrationTestCompile.extendsFrom testCompile - integrationTestRuntime.extendsFrom testRuntime + task install { + dependsOn publishToMavenLocal + } } +/* LOCAL SNAPSHOT DEV */ dependencies { - // These are copied over from jib-core and are necessary for the jib-core sourcesets. - compile 'com.google.http-client:google-http-client:1.23.0' - compile 'org.apache.commons:commons-compress:1.17' - compile 'com.google.guava:guava:23.5-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.slf4j:slf4j-api:1.7.25' - compile 'org.javassist:javassist:3.22.0-GA' - - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.12.0' + sourceProject project(':jib-core') + sourceProject project(':jib-plugins-common') + ensureNoProjectDependencies() - compile gradleApi() - - // NullAway errorprone plugin - apt 'com.uber.nullaway:nullaway:0.4.2' - errorprone 'com.google.errorprone:error_prone_core:2.2.0' -} + implementation dependencyStrings.GRADLE_EXTENSION -task wrapper(type: Wrapper) { - gradleVersion = '4.6' -} + testImplementation dependencyStrings.JUNIT + testImplementation dependencyStrings.TRUTH + testImplementation dependencyStrings.TRUTH8 + testImplementation dependencyStrings.MOCKITO_CORE + testImplementation dependencyStrings.SLF4J_API + testImplementation dependencyStrings.SYSTEM_RULES -// Integration tests must be run explicitly -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - systemProperty '_JIB_DISABLE_USER_AGENT', true -} -integrationTest.dependsOn test + testImplementation project(path:':jib-plugins-common', configuration:'tests') + integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') -test { - testLogging { - showStandardStreams = true - exceptionFormat = 'full' - } -} + integrationTestImplementation dependencyStrings.JBCRYPT -// Adds NullAway errorprone checks. -tasks.withType(JavaCompile) { - if (!name.toLowerCase().contains("test")) { - options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] - } + // only for testing a concrete Spring Boot example in a test (not for test infrastructure) + testImplementation 'org.springframework.boot:spring-boot-gradle-plugin:2.2.11.RELEASE' } -// Fail build on javadoc warnings -tasks.withType(Javadoc) { - options.addBooleanOption('Xwerror', true) -} - -tasks.withType(Test) { - reports.html.setDestination file("${reporting.baseDir}/${name}") -} - -/* GOOGLE JAVA FORMAT */ -googleJavaFormat { - toolVersion = '1.6' -} -check.dependsOn verifyGoogleJavaFormat -/* GOOGLE JAVA FORMAT */ - -/* CHECKSTYLE */ -checkstyle { - toolVersion = "7.6.1" - - // get the google_checks.xml file from the checkstyle jar and take out the java checks - def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - - config = resources.text.fromString(googleChecksNoJava) - - maxErrors = 0 - maxWarnings = 0 -} -/* CHECKSTYLE */ - -/* JAR */ -// Necessary for adding version information into the JAR. -jar { - manifest { - attributes 'Implementation-Title': project.name, - 'Implementation-Version': version, - 'Built-By': System.getProperty('user.name'), - 'Built-Date': new Date(), - 'Built-JDK': System.getProperty('java.version'), - 'Built-Gradle': gradle.gradleVersion - } -} -/* JAR */ - /* RELEASE */ // Prepare release release { - tagTemplate = 'v$version-gradle' - git { - requireBranch = /^gradle_release_v\d+.*$/ //regex - } + tagTemplate = 'v$version-gradle' + ignoredSnapshotDependencies = [ + 'com.google.cloud.tools:jib-core', + 'com.google.cloud.tools:jib-plugins-common', + ] + git { + requireBranch = /^gradle-release-v\d+.*$/ //regex + } } // Gradle Plugin Portal releases pluginBundle { - website = 'https://github.com/GoogleContainerTools/jib/' - vcsUrl = 'https://github.com/GoogleContainerTools/jib/' - - plugins { - jibPlugin { - id = 'com.google.cloud.tools.jib' - displayName = 'Jib' - description = 'Containerize your Java application' - tags = ['google', 'java', 'containers', 'docker', 'kubernetes', 'microservices'] - } + website = 'https://github.com/GoogleContainerTools/jib/' + vcsUrl = 'https://github.com/GoogleContainerTools/jib/' + tags = ['google', 'java', 'containers', 'docker', 'kubernetes', 'microservices'] +} + +gradlePlugin { + testSourceSets sourceSets.integrationTest, sourceSets.test + plugins { + jibPlugin { + id = 'com.google.cloud.tools.jib' + displayName = 'Jib' + description = 'Containerize your Java application' + implementationClass = 'com.google.cloud.tools.jib.gradle.JibPlugin' } + } } -tasks.publishPlugins.dependsOn integrationTest +tasks.publishPlugins.dependsOn build /* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +eclipse.classpath.file.whenMerged { + entries.each { + if (it.path == 'src/test/resources' || it.path == 'src/integration-test/resources') { + it.excludes += 'gradle/projects/' + } + } +} +/* ECLIPSE */ diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 4afad4b820..904db0f30e 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.7-SNAPSHOT +version = 3.5.5-SNAPSHOT diff --git a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7a3265ee94..0000000000 Binary files a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh deleted file mode 100755 index c24179711f..0000000000 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# Usage: ./scripts/prepare_release.sh - -set -e - -Colorize() { - echo "$(tput setff $2)$1$(tput sgr0)" -} - -EchoRed() { - echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" -} -EchoGreen() { - echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" -} - -Die() { - EchoRed "$1" - exit 1 -} - -DieUsage() { - Die "Usage: ./scripts/prepare_release.sh " -} - -# Usage: CheckVersion -CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." -} - -[ $# -ne 2 ] || DieUsage - -EchoGreen '===== RELEASE SETUP SCRIPT =====' - -VERSION=$1 -CheckVersion ${VERSION} - -if [[ $(git status -uno --porcelain) ]]; then - Die 'There are uncommitted changes.' -fi - -# Runs integration tests. -./gradlew integrationTest --info --stacktrace - -# Checks out a new branch for this version release (eg. 1.5.7). -BRANCH=gradle_release_v${VERSION} -git checkout -b ${BRANCH} - -# Changes the version for release and creates the commits/tags. -echo | ./gradlew release -PreleaseVersion=${VERSION} - -# Pushes the release branch and tag to Github. -git push origin ${BRANCH} -git push origin v${VERSION}-gradle - -# File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. -EchoGreen 'File a PR for the new release branch:' -echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} - -EchoGreen "Once approved and merged, checkout the 'v${VERSION}-gradle' tag and run './gradlew publishPlugins'." diff --git a/jib-gradle-plugin/scripts/release.sh b/jib-gradle-plugin/scripts/release.sh new file mode 100755 index 0000000000..faa3ed076a --- /dev/null +++ b/jib-gradle-plugin/scripts/release.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -o errexit + +readonly PUBLISH_KEY=$(cat "${KOKORO_KEYSTORE_DIR}/72743_gradle_publish_key") +readonly PUBLISH_SECRET=$(cat "${KOKORO_KEYSTORE_DIR}/72743_gradle_publish_secret") + +set -o xtrace + +# From default hostname, get id of container to exclude +CONTAINER_ID=$(hostname) +echo "$CONTAINER_ID" + +# Stops any left-over containers. +docker stop $(docker ps --all --quiet | grep -v "$CONTAINER_ID") || true +docker kill $(docker ps --all --quiet | grep -v "$CONTAINER_ID") || true +cd github/jib + +echo "gradle publish" + +# turn of command tracing when dealing with secrets +set +o xtrace + +./gradlew :jib-gradle-plugin:publishPlugins \ + -Pgradle.publish.key="${PUBLISH_KEY}" \ + -Pgradle.publish.secret="${PUBLISH_SECRET}" \ + --info --stacktrace diff --git a/jib-gradle-plugin/scripts/update_gcs_latest.sh b/jib-gradle-plugin/scripts/update_gcs_latest.sh new file mode 100755 index 0000000000..d406ec21e2 --- /dev/null +++ b/jib-gradle-plugin/scripts/update_gcs_latest.sh @@ -0,0 +1,41 @@ +#!/bin/bash - +# Usage: ./jib-gradle-plugin/scripts/update_gcs_latest.sh + +set -o errexit + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." +} + +[ $# -ne 1 ] && Die "Usage: ./jib-gradle-plugin/scripts/update_gcs_latest.sh " + +CheckVersion $1 + +versionString="{\"latest\":\"$1\"}" +destination="gs://jib-versions/jib-gradle" + +echo $versionString > jib-gradle +gcloud storage cp jib-gradle $destination +gcloud storage objects update --add-acl-grant=entity=allUsers,role=READER $destination +rm jib-gradle + +gcsResult=$(curl https://storage.googleapis.com/jib-versions/jib-gradle) +if [ "$gcsResult" == "$versionString" ] +then + EchoGreen "Version updated successfully" +else + Die "Version update failed" +fi \ No newline at end of file diff --git a/jib-gradle-plugin/settings.gradle b/jib-gradle-plugin/settings.gradle deleted file mode 100644 index 24c73383cc..0000000000 --- a/jib-gradle-plugin/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'jib-gradle-plugin' diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java new file mode 100644 index 0000000000..8d78986bc9 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import java.io.IOException; +import java.security.DigestException; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building "default-target" project images. */ +public class DefaultTargetProjectIntegrationTest { + + @ClassRule + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); + + /** + * Asserts that the test project has the required exposed ports, labels and volumes. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/udp\":{},\"2001/udp\":{},\"2002/udp\":{},\"2003/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + } + + @Test + public void testBuild_defaultTarget() { + // Test error when 'to' is missing + try { + defaultTargetTestProject.build( + "clean", "jib", "-Djib.useOnlyProjectCache=true", "-x=classes"); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Missing target image parameter, perhaps you should add a 'jib.to.image' " + + "configuration parameter to your build.gradle or set the parameter via the " + + "commandline (e.g. 'gradle jib --image ').")); + } + } + + @Test + public void testDockerDaemon_defaultTarget() + throws IOException, InterruptedException, DigestException { + Assert.assertEquals( + "Hello, world. An argument.\n", + JibRunHelper.buildToDockerDaemonAndRun( + defaultTargetTestProject, + "default-target-name:default-target-version", + "build.gradle")); + assertDockerInspect("default-target-name:default-target-version"); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java new file mode 100644 index 0000000000..b639885605 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import java.io.IOException; +import java.security.DigestException; +import java.time.Instant; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building empty project images. */ +public class EmptyProjectIntegrationTest { + + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); + + /** + * Asserts that the test project has the required exposed ports and labels. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/udp\":{},\"2001/udp\":{},\"2002/udp\":{},\"2003/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + } + + @Test + public void testBuild_empty() throws IOException, InterruptedException, DigestException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/emptyimage:gradle" + + System.nanoTime(); + Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); + assertDockerInspect(targetImage); + assertThat(JibRunHelper.getCreationTime(targetImage)).isEqualTo(Instant.EPOCH); + } + + @Test + public void testBuild_multipleTags() + throws IOException, InterruptedException, InvalidImageReferenceException, DigestException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/multitag-image:gradle" + + System.nanoTime(); + JibRunHelper.buildAndRunAdditionalTag( + emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); + assertDockerInspect(targetImage); + } + + @Test + public void testDockerDaemon_empty() throws IOException, InterruptedException, DigestException { + String targetImage = "emptyimage:gradle" + System.nanoTime(); + Assert.assertEquals( + "", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage, "build.gradle")); + assertThat(JibRunHelper.getCreationTime(targetImage)).isEqualTo(Instant.EPOCH); + assertDockerInspect(targetImage); + } + + @Test + public void testDockerDaemon_userNumeric() + throws IOException, InterruptedException, DigestException { + String targetImage = "emptyimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build.gradle"); + Assert.assertEquals( + "12345:54321", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } + + @Test + public void testDockerDaemon_userNames() + throws IOException, InterruptedException, DigestException { + String targetImage = "brokenuserimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build-broken-user.gradle"); + Assert.assertEquals( + "myuser:mygroup", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java new file mode 100644 index 0000000000..62c02f27c5 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.ClassRule; +import org.junit.Test; + +public class GradleLayerConfigurationIntegrationTest { + + @ClassRule + public static final TestProject multiTestProject = new TestProject("all-local-multi-service"); + + @ClassRule public static final TestProject configTestProject = new TestProject("simple"); + + @Test + public void testGradleLayerConfiguration_configurationName() throws IOException { + configTestProject.build("jibBuildTar", "-b=build-configuration.gradle"); + Path jibTar = configTestProject.getProjectRoot().resolve("build/jib-image.tar"); + List> layers = getLayers(jibTar); + + // the expected order is: + // no base image layers (scratch) + // jvm arg files (0) + // classes (1) + // resources (2) + // dependencies (3) + // verify dependencies + List dependencies = layers.get(3); + assertThat(dependencies).containsExactly("app/", "app/libs/", "app/libs/dependency2").inOrder(); + } + + @Test + public void testGradleLayerConfiguration_configurationName_prioritizeSystemProperty() + throws IOException { + configTestProject.build( + "jibBuildTar", + "--stacktrace", + "-Djib.configurationName=otherConfiguration", + "-b=build-configuration.gradle"); + Path jibTar = configTestProject.getProjectRoot().resolve("build/jib-image.tar"); + List> layers = getLayers(jibTar); + + // the expected order is: + // no base image layers (scratch) + // jvm arg files (0) + // classes (1) + // resources (2) + // dependencies (3) + // verify dependencies + List dependencies = layers.get(3); + assertThat(dependencies).containsExactly("app/", "app/libs/", "app/libs/dependency3").inOrder(); + } + + @Test + public void testGradleLayerConfiguration_multiModule() throws IOException { + multiTestProject.build(":complex-service:jibBuildTar"); + + Path jibTar = multiTestProject.getProjectRoot().resolve("complex-service/build/jib-image.tar"); + + List> layers = getLayers(jibTar); + + assertThat(layers).hasSize(7); + + // the expected order is: + // no base image layers (scratch) + // extra-files (0) + // jvm arg files (1) + // classes (2) + // resources (3) + // project dependencies (4) + // snapshot dependencies (5) + // dependencies (6) + + // verify dependencies + assertThat(layers.get(6)) + .containsExactly("app/", "app/libs/", "app/libs/dependency-1.0.0.jar") + .inOrder(); + + // verify snapshot dependencies + assertThat(layers.get(5)) + .containsExactly("app/", "app/libs/", "app/libs/dependencyX-1.0.0-SNAPSHOT.jar") + .inOrder(); + + // verify project dependencies + assertThat(layers.get(4)).containsExactly("app/", "app/libs/", "app/libs/lib.jar").inOrder(); + + // verify resources + assertThat(layers.get(3)) + .containsExactly( + "app/", "app/resources/", "app/resources/resource1.txt", "app/resources/resource2.txt") + .inOrder(); + + // verify classes + assertThat(layers.get(2)) + .containsExactly( + "app/", + "app/classes/", + "app/classes/com/", + "app/classes/com/test/", + "app/classes/com/test/HelloWorld.class"); + + // verify jvm arg files + assertThat(layers.get(1)) + .containsExactly("app/", "app/jib-classpath-file", "app/jib-main-class-file") + .inOrder(); + + // verify extra files + assertThat(layers.get(0)).containsExactly("extra-file"); + } + + @Test + public void testGradleLayerConfiguration_simpleModule() throws IOException { + multiTestProject.build(":simple-service:jibBuildTar"); + + Path jibTar = multiTestProject.getProjectRoot().resolve("simple-service/build/jib-image.tar"); + + List> layers = getLayers(jibTar); + + assertThat(layers).hasSize(2); + + // the expected order is: + // no base image layers (scratch) + // jvm arg files (0) + // classes (1) + + // verify classes + assertThat(layers.get(1)) + .containsExactly( + "app/", + "app/classes/", + "app/classes/com/", + "app/classes/com/test/", + "app/classes/com/test/HelloWorld.class") + .inOrder(); + + // verify jvm arg files + assertThat(layers.get(0)) + .containsExactly("app/", "app/jib-classpath-file", "app/jib-main-class-file") + .inOrder(); + } + + // returns all files in layers (*.tar.gz) in a image tar + private List> getLayers(Path tar) throws IOException { + List> layers = new ArrayList<>(); + + try (TarArchiveInputStream image = new TarArchiveInputStream(Files.newInputStream(tar))) { + TarArchiveEntry entry; + while ((entry = image.getNextEntry()) != null) { + if (entry.getName().endsWith(".tar.gz")) { + @SuppressWarnings("resource") // must not close sub-streams + TarArchiveInputStream layer = new TarArchiveInputStream(new GZIPInputStream(image)); + TarArchiveEntry layerEntry; + List layerFiles = new ArrayList<>(); + while ((layerEntry = layer.getNextEntry()) != null) { + layerFiles.add(layerEntry.getName()); + } + layers.add(0, layerFiles); + } + } + } + return layers; + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java deleted file mode 100644 index 617788b5a5..0000000000 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.Command; -import java.io.IOException; -import java.nio.file.Files; -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.BuildTask; -import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Test; - -/** Integration tests for {@link JibPlugin}. */ -public class JibPluginIntegrationTest { - - @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); - - @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - - @ClassRule - public static final TestProject defaultTargetTestProject = new TestProject("default-target"); - - private static String buildAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { - BuildResult buildResult = testProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - new Command("docker", "pull", imageReference).run(); - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - return new Command("docker", "run", imageReference).run(); - } - - private static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { - BuildResult buildResult = testProject.build("clean", JibPlugin.BUILD_DOCKER_TASK_NAME); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibBuildDockerTask = buildResult.task(":" + JibPlugin.BUILD_DOCKER_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibBuildDockerTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibBuildDockerTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built image to Docker daemon as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - return new Command("docker", "run", imageReference).run(); - } - - @Test - public void testBuild_empty() throws IOException, InterruptedException { - Assert.assertEquals( - "", buildAndRun(emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); - } - - @Test - public void testBuild_simple() throws IOException, InterruptedException { - // Test empty output error - try { - simpleTestProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes"); - Assert.fail(); - } catch (UnexpectedBuildFailure ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "No classes files were found - did you compile your project?")); - } - - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - buildAndRun(simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); - } - - @Test - public void testBuild_defaultTarget() { - // Test error when 'to' is missing - try { - defaultTargetTestProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes"); - Assert.fail(); - } catch (UnexpectedBuildFailure ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "Missing target image parameter, perhaps you should add a 'jib.to.image' " - + "configuration parameter to your build.gradle or set the parameter via the " - + "commandline (e.g. 'gradle jib --image ').")); - } - } - - @Test - public void testDockerDaemon_empty() throws IOException, InterruptedException { - Assert.assertEquals( - "", - buildToDockerDaemonAndRun( - emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); - } - - @Test - public void testDockerDaemon_simple() throws IOException, InterruptedException { - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - buildToDockerDaemonAndRun( - simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); - } - - @Test - public void testDockerDaemon_defaultTarget() throws IOException, InterruptedException { - Assert.assertEquals( - "Hello, world. An argument.\n", - buildToDockerDaemonAndRun( - defaultTargetTestProject, "default-target-name:default-target-version")); - } - - @Test - public void testDockerContext() throws IOException, InterruptedException { - BuildResult buildResult = - simpleTestProject.build("clean", JibPlugin.DOCKER_CONTEXT_TASK_NAME, "--info"); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibDockerContextTask = buildResult.task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibDockerContextTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibDockerContextTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); - - String imageName = "jib-gradle-plugin/integration-test"; - new Command( - "docker", - "build", - "-t", - imageName, - simpleTestProject - .getProjectRoot() - .resolve("build") - .resolve("jib-docker-context") - .toString()) - .run(); - - Assert.assertThat( - new Command("docker", "inspect", imageName).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageName).run()); - - // Checks that generating the Docker context again is skipped. - BuildTask upToDateJibDockerContextTask = - simpleTestProject - .build(JibPlugin.DOCKER_CONTEXT_TASK_NAME) - .task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); - Assert.assertNotNull(upToDateJibDockerContextTask); - Assert.assertEquals(TaskOutcome.UP_TO_DATE, upToDateJibDockerContextTask.getOutcome()); - - // Checks that adding a new file generates the Docker context again. - Files.createFile( - simpleTestProject - .getProjectRoot() - .resolve("src") - .resolve("main") - .resolve("resources") - .resolve("newfile")); - try { - BuildTask reexecutedJibDockerContextTask = - simpleTestProject - .build(JibPlugin.DOCKER_CONTEXT_TASK_NAME) - .task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); - Assert.assertNotNull(reexecutedJibDockerContextTask); - Assert.assertEquals(TaskOutcome.SUCCESS, reexecutedJibDockerContextTask.getOutcome()); - - } catch (UnexpectedBuildFailure ex) { - // THis might happen on systems without SecureDirectoryStream, so we just ignore it. - // See com.google.common.io.MoreFiles#deleteDirectoryContents. - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "Export Docker context failed because cannot clear directory")); - } - } -} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java new file mode 100644 index 0000000000..70119236ed --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -0,0 +1,205 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; + +/** Helper class to run integration tests. */ +public class JibRunHelper { + + static String buildAndRun(TestProject testProject, String imageReference) + throws IOException, InterruptedException, DigestException { + return buildAndRun(testProject, imageReference, "build.gradle"); + } + + static String buildAndRun( + TestProject testProject, + String imageReference, + String gradleBuildFile, + String... extraRunArguments) + throws IOException, InterruptedException, DigestException { + BuildResult buildResult = + testProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), + "-b=" + gradleBuildFile); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertThatExpectedImageDigestAndIdReturned(testProject.getProjectRoot()); + MatcherAssert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + return pullAndRunBuiltImage(imageReference, extraRunArguments); + } + + static String buildAndRunFromLocalBase(String target, String base) + throws IOException, InterruptedException, DigestException { + BuildResult buildResult = + SingleProjectIntegrationTest.simpleTestProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + target, + "-D_BASE_IMAGE=" + base, + "-Djib.allowInsecureRegistries=" + target.startsWith("localhost"), + "-b=" + "build-local-base.gradle"); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertThatExpectedImageDigestAndIdReturned( + SingleProjectIntegrationTest.simpleTestProject.getProjectRoot()); + MatcherAssert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(target)); + return pullAndRunBuiltImage(target); + } + + static void buildAndRunAdditionalTag( + TestProject testProject, String imageReference, String additionalTag, String expectedOutput) + throws InvalidImageReferenceException, IOException, InterruptedException, DigestException { + BuildResult buildResult = + testProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), + "-D_ADDITIONAL_TAG=" + additionalTag); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertThatExpectedImageDigestAndIdReturned(testProject.getProjectRoot()); + MatcherAssert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + String additionalImageReference = + ImageReference.parse(imageReference).withQualifier(additionalTag).toString(); + MatcherAssert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString(additionalImageReference)); + + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(imageReference)); + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(additionalImageReference)); + assertThat(getCreationTime(imageReference)).isEqualTo(Instant.EPOCH); + assertThat(getCreationTime(additionalImageReference)).isEqualTo(Instant.EPOCH); + } + + static BuildResult buildToDockerDaemon( + TestProject testProject, String imageReference, String gradleBuildFile) + throws IOException, InterruptedException, DigestException { + BuildResult buildResult = + testProject.build( + "clean", + "jibDockerBuild", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), + "-b=" + gradleBuildFile); + assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); + assertThatExpectedImageDigestAndIdReturned(testProject.getProjectRoot()); + MatcherAssert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + String history = new Command("docker", "history", imageReference).run(); + MatcherAssert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + return buildResult; + } + + static String buildToDockerDaemonAndRun( + TestProject testProject, String imageReference, String gradleBuildFile) + throws IOException, InterruptedException, DigestException { + buildToDockerDaemon(testProject, imageReference, gradleBuildFile); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + /** + * Asserts that the test project build output indicates a success. + * + * @param buildResult the builds results of the project under test + * @param taskName the name of the Jib task that was run + * @param successMessage a Jib-specific success message to check for + */ + static void assertBuildSuccess(BuildResult buildResult, String taskName, String successMessage) { + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibTask = buildResult.task(":" + taskName); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + MatcherAssert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(successMessage)); + } + + static Instant getCreationTime(String imageReference) throws IOException, InterruptedException { + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); + return Instant.parse(inspect); + } + + static void assertThatExpectedImageDigestAndIdReturned(Path projectRoot) + throws IOException, DigestException { + Path digestPath = projectRoot.resolve("build/jib-image.digest"); + Assert.assertTrue(Files.exists(digestPath)); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + DescriptorDigest digest1 = DescriptorDigest.fromDigest(digest); + + Path idPath = projectRoot.resolve("build/jib-image.id"); + Assert.assertTrue(Files.exists(idPath)); + String id = new String(Files.readAllBytes(idPath), StandardCharsets.UTF_8); + DescriptorDigest digest2 = DescriptorDigest.fromDigest(id); + Assert.assertNotEquals(digest1, digest2); + } + + /** + * Pulls a built image and attempts to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @param extraRunArguments extra arguments passed to {@code docker run} + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + MatcherAssert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + List command = new ArrayList<>(Arrays.asList("docker", "run", "--rm")); + command.addAll(Arrays.asList(extraRunArguments)); + command.add(imageReference); + return new Command(command).run(); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java new file mode 100644 index 0000000000..faf32e883e --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -0,0 +1,605 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.time.Instant; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Integration tests for building single project images. */ +public class SingleProjectIntegrationTest { + + @ClassRule + public static final LocalRegistry localRegistry1 = + new LocalRegistry(5000, "testuser", "testpassword"); + + @ClassRule + public static final LocalRegistry localRegistry2 = + new LocalRegistry(6000, "testuser", "testpassword"); + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + + private static boolean isJavaRuntimeAtLeast(int version) { + Iterable split = Splitter.on(".").split(System.getProperty("java.version")); + return Integer.valueOf(split.iterator().next()) >= version; + } + + private static String getWorkingDirectory(String imageReference) + throws IOException, InterruptedException { + return new Command("docker", "inspect", "-f", "{{.Config.WorkingDir}}", imageReference) + .run() + .trim(); + } + + private static String getEntrypoint(String imageReference) + throws IOException, InterruptedException { + return new Command("docker", "inspect", "-f", "{{.Config.Entrypoint}}", imageReference) + .run() + .trim(); + } + + private static int getLayerSize(String imageReference) throws IOException, InterruptedException { + Command command = + new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); + String layers = command.run().trim(); + return Splitter.on(",").splitToList(layers).size(); + } + + /** + * Asserts that the test project has the required exposed ports, labels and volumes. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspectVolumes = + new Command("docker", "inspect", "-f", "'{{json .Config.Volumes}}'", imageReference).run(); + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + + MatcherAssert.assertThat( + dockerInspectVolumes, CoreMatchers.containsString("\"/var/log\":{},\"/var/log2\":{}")); + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/udp\":{},\"2001/udp\":{},\"2002/udp\":{},\"2003/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + } + + private static String readDigestFile(Path digestPath) throws IOException, DigestException { + assertThat(Files.exists(digestPath)).isTrue(); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + return DescriptorDigest.fromDigest(digest).toString(); + } + + private static String buildAndRunComplex( + String imageReference, String username, String password, LocalRegistry targetRegistry) + throws IOException, InterruptedException { + Path baseCache = simpleTestProject.getProjectRoot().resolve("build/jib-base-cache"); + BuildResult buildResult = + simpleTestProject.build( + "clean", + "jib", + "-Djib.baseImageCache=" + baseCache, + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + imageReference, + "-D_TARGET_USERNAME=" + username, + "-D_TARGET_PASSWORD=" + password, + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + JibRunHelper.assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertThat(buildResult.getOutput()).contains(imageReference); + + targetRegistry.pull(imageReference); + assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + assertThat(history).contains("jib-gradle-plugin"); + + String output = new Command("docker", "run", "--rm", imageReference).run(); + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rwxr-xr-x", + "rwxrwxrwx", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z", + "-Xms512m", + "-Xdebug", + "envvalue1", + "envvalue2"); + return output; + } + + @Before + public void setup() throws IOException, InterruptedException { + // Pull distroless and push to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + } + + @Test + public void testBuild_simple() + throws IOException, InterruptedException, DigestException, InvalidImageReferenceException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simpleimage:gradle" + + System.nanoTime(); + + // Test empty output error + Exception exception = + assertThrows( + UnexpectedBuildFailure.class, + () -> + simpleTestProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-x=classes", + "-D_TARGET_IMAGE=" + targetImage)); + assertThat(exception) + .hasMessageThat() + .contains("No classes files were found - did you compile your project?"); + + String output = JibRunHelper.buildAndRun(simpleTestProject, targetImage); + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + + String digest = + readDigestFile(simpleTestProject.getProjectRoot().resolve("build/jib-image.digest")); + String imageReferenceWithDigest = + ImageReference.parse(targetImage).withQualifier(digest).toString(); + assertThat(JibRunHelper.pullAndRunBuiltImage(imageReferenceWithDigest)).isEqualTo(output); + + String id = readDigestFile(simpleTestProject.getProjectRoot().resolve("build/jib-image.id")); + assertThat(id).isNotEqualTo(digest); + assertThat(new Command("docker", "run", "--rm", id).run()).isEqualTo(output); + + assertDockerInspect(targetImage); + assertThat(JibRunHelper.getCreationTime(targetImage)).isEqualTo(Instant.EPOCH); + assertThat(getWorkingDirectory(targetImage)).isEqualTo("/home"); + assertThat(getEntrypoint(targetImage)) + .isEqualTo( + "[java -cp /d1:/d2:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]"); + assertThat(getLayerSize(targetImage)).isEqualTo(10); + } + + @Test + public void testBuild_dockerDaemonBase() + throws IOException, InterruptedException, DigestException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithdockerdaemonbase:gradle" + + System.nanoTime(); + String output = + JibRunHelper.buildAndRunFromLocalBase( + targetImage, "docker://gcr.io/distroless/java:latest"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + } + + @Test + public void testBuild_tarBase() throws IOException, InterruptedException, DigestException { + Path path = temporaryFolder.getRoot().toPath().resolve("docker-save-distroless"); + new Command("docker", "save", "gcr.io/distroless/java:latest", "-o", path.toString()).run(); + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithtarbase:gradle" + + System.nanoTime(); + String output = JibRunHelper.buildAndRunFromLocalBase(targetImage, "tar://" + path); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + } + + @Test + public void testBuild_failOffline() { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simpleimageoffline:gradle" + + System.nanoTime(); + + Exception exception = + assertThrows( + UnexpectedBuildFailure.class, + () -> + simpleTestProject.build( + "--offline", + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + targetImage)); + assertThat(exception) + .hasMessageThat() + .contains("Cannot build to a container registry in offline mode"); + } + + @Test + public void testDockerDaemon_simpleOnJava17() + throws DigestException, IOException, InterruptedException { + assumeTrue(isJavaRuntimeAtLeast(17)); + + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-java17.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly("Hello, world. ", "1970-01-01T00:00:01Z"); + } + + @Test + public void testDockerDaemon_simpleOnJava11() + throws DigestException, IOException, InterruptedException { + assumeTrue(isJavaRuntimeAtLeast(11)); + + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-java11.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly("Hello, world. ", "1970-01-01T00:00:01Z"); + } + + @Test + public void testDockerDaemon_simpleWithIncompatibleJava11() { + assumeTrue(isJavaRuntimeAtLeast(11)); + + Exception exception = + assertThrows( + UnexpectedBuildFailure.class, + () -> + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, "willnotbuild", "build-java11-incompatible.gradle")); + assertThat(exception) + .hasMessageThat() + .contains( + "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + + "configure a Java 11-compatible base image using the 'jib.from.image' parameter, " + + "or set targetCompatibility = 8 or below in your build configuration"); + } + + @Test + public void testDockerDaemon_simple_multipleExtraDirectories() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-extra-dirs.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. ", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + assertThat(getLayerSize(targetImage)).isEqualTo(11); // one more than usual + } + + @Test + public void testDockerDaemon_simple_multipleExtraDirectoriesWithAlternativeConfig() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-extra-dirs2.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. ", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + assertThat(getLayerSize(targetImage)).isEqualTo(11); // one more than usual + } + + @Test + public void testDockerDaemon_simple_multipleExtraDirectoriesWithClosure() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-extra-dirs3.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. ", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z", + "baz", + "1970-01-01T00:00:01Z"); + assertThat(getLayerSize(targetImage)).isEqualTo(11); // one more than usual + } + + @Test + public void testDockerDaemon_simple_extraDirectoriesFiltering() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemon( + simpleTestProject, targetImage, "build-extra-dirs-filtering.gradle"); + String output = + new Command("docker", "run", "--rm", "--entrypoint=ls", targetImage, "-1R", "/extras") + .run(); + + // /extras/cat.txt + // /extras/foo + // /extras/sub/ + // /extras/sub/a.json + assertThat(output).isEqualTo("/extras:\ncat.txt\nfoo\nsub\n\n/extras/sub:\na.json\n"); + } + + @Test + public void testBuild_complex() + throws IOException, InterruptedException, DigestException, InvalidImageReferenceException { + String targetImage = dockerHost + ":6000/compleximage:gradle" + System.nanoTime(); + Instant beforeBuild = Instant.now(); + String output = buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry2); + + String digest = + readDigestFile( + simpleTestProject.getProjectRoot().resolve("build/different-jib-image.digest")); + String imageReferenceWithDigest = + ImageReference.parse(targetImage).withQualifier(digest).toString(); + localRegistry2.pull(imageReferenceWithDigest); + assertThat(new Command("docker", "run", "--rm", imageReferenceWithDigest).run()) + .isEqualTo(output); + + String id = + readDigestFile(simpleTestProject.getProjectRoot().resolve("different-jib-image.id")); + assertThat(id).isNotEqualTo(digest); + assertThat(new Command("docker", "run", "--rm", id).run()).isEqualTo(output); + + assertThat(JibRunHelper.getCreationTime(targetImage)).isGreaterThan(beforeBuild); + assertThat(getWorkingDirectory(targetImage)).isEqualTo("/"); + } + + @Test + public void testBuild_complex_sameFromAndToRegistry() throws IOException, InterruptedException { + String targetImage = dockerHost + ":5000/compleximage:gradle" + System.nanoTime(); + Instant beforeBuild = Instant.now(); + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1); + assertThat(JibRunHelper.getCreationTime(targetImage)).isGreaterThan(beforeBuild); + assertThat(getWorkingDirectory(targetImage)).isEqualTo("/"); + } + + @Test + public void testDockerDaemon_simple() throws IOException, InterruptedException, DigestException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage, "build.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + assertThat(JibRunHelper.getCreationTime(targetImage)).isEqualTo(Instant.EPOCH); + assertDockerInspect(targetImage); + assertThat(getWorkingDirectory(targetImage)).isEqualTo("/home"); + } + + @Test + public void testDockerDaemon_jarContainerization() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-jar-containerization.gradle"); + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. ", "Implementation-Title: helloworld", "Implementation-Version: 1"); + } + + @Test + public void testBuild_skipDownloadingBaseImageLayers() throws IOException, InterruptedException { + Path baseLayersCacheDirectory = + simpleTestProject.getProjectRoot().resolve("build/jib-base-cache/layers"); + String targetImage = dockerHost + ":6000/simpleimage:gradle" + System.nanoTime(); + + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry2); + // Base image layer tarballs exist. + assertThat(Files.exists(baseLayersCacheDirectory)).isTrue(); + assertThat(baseLayersCacheDirectory.toFile().list().length >= 2).isTrue(); + + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry2); + // no base layers downloaded after "gradle clean jib ..." + assertThat(Files.exists(baseLayersCacheDirectory)).isFalse(); + } + + @Test + public void testDockerDaemon_timestampCustom() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String output = + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-timestamps-custom.gradle"); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly("Hello, world. ", "2011-12-03T01:15:30Z"); + assertThat(JibRunHelper.getCreationTime(targetImage)) + .isEqualTo(Instant.parse("2013-11-04T21:29:30Z")); + } + + @Test + public void testBuild_dockerClient() throws IOException, InterruptedException, DigestException { + assumeFalse(System.getProperty("os.name").startsWith("Windows")); + new Command( + "chmod", "+x", simpleTestProject.getProjectRoot().resolve("mock-docker.sh").toString()) + .run(); + + String targetImage = "simpleimage:gradle" + System.nanoTime(); + + BuildResult buildResult = + simpleTestProject.build( + "clean", + "jibDockerBuild", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + targetImage, + "-b=build-dockerclient.gradle", + "--debug"); + JibRunHelper.assertBuildSuccess( + buildResult, "jibDockerBuild", "Built image to Docker daemon as "); + JibRunHelper.assertThatExpectedImageDigestAndIdReturned(simpleTestProject.getProjectRoot()); + assertThat(buildResult.getOutput()).contains(targetImage); + assertThat(buildResult.getOutput()).contains("Docker load called. value1 value2"); + } + + @Test + public void testBuildTar_simple() throws IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + + String outputPath = + simpleTestProject + .getProjectRoot() + .resolve("build") + .resolve("different-jib-image.tar") + .toString(); + BuildResult buildResult = + simpleTestProject.build( + "clean", + "jibBuildTar", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + targetImage); + + JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); + assertThat(buildResult.getOutput()).contains(outputPath); + + new Command("docker", "load", "--input", outputPath).run(); + String output = new Command("docker", "run", "--rm", targetImage).run(); + + assertThat(ImmutableList.copyOf(output.split("\n"))) + .containsExactly( + "Hello, world. An argument.", + "1970-01-01T00:00:01Z", + "rw-r--r--", + "rw-r--r--", + "foo", + "cat", + "1970-01-01T00:00:01Z", + "1970-01-01T00:00:01Z"); + assertDockerInspect(targetImage); + assertThat(JibRunHelper.getCreationTime(targetImage)).isEqualTo(Instant.EPOCH); + assertThat(getWorkingDirectory(targetImage)).isEqualTo("/home"); + } + + @Test + public void testCredHelperConfiguration() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + assertThat( + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-cred-helper.gradle")) + .isEqualTo("Hello, world. \n1970-01-01T00:00:01Z\n"); + } + + @Test + public void testToDockerDaemon_multiPlatform() + throws DigestException, IOException, InterruptedException { + String targetImage = "multiplatform:gradle" + System.nanoTime(); + assertThat( + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-multi-platform.gradle")) + .isEqualTo("Hello, world. \n1970-01-01T00:00:01Z\n"); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java new file mode 100644 index 0000000000..dca766587b --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.api.HttpRequestTester; +import java.io.IOException; +import java.net.URL; +import java.security.DigestException; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building Spring Boot images. */ +public class SpringBootProjectIntegrationTest { + + @ClassRule public static final TestProject springBootProject = new TestProject("spring-boot"); + + @Nullable private String containerName; + + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { + new Command("docker", "stop", containerName).run(); + } + } + + @Test + public void testBuild_packagedMode() throws IOException, InterruptedException, DigestException { + buildAndRunWebApp("springboot:gradle", "build.gradle"); + + String output = + new Command( + "docker", + "exec", + containerName, + "/busybox/wc", + "-c", + "/app/classpath/spring-boot-original.jar") + .run(); + + Assert.assertEquals("1360 /app/classpath/spring-boot-original.jar\n", output); + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080")); + } + + private void buildAndRunWebApp(String label, String gradleBuildFile) + throws IOException, InterruptedException, DigestException { + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; + String targetImage = nameBase + label + System.nanoTime(); + String output = + JibRunHelper.buildAndRun( + springBootProject, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); + containerName = output.trim(); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java new file mode 100644 index 0000000000..dbcf72940b --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.api.HttpRequestTester; +import java.io.IOException; +import java.net.URL; +import java.security.DigestException; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building WAR images. */ +public class WarProjectIntegrationTest { + + @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); + + @Nullable private String containerName; + + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { + new Command("docker", "stop", containerName).run(); + } + } + + @Test + public void testBuild_jettyServlet25() throws IOException, InterruptedException, DigestException { + verifyBuildAndRun(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); + } + + @Test + public void testBuild_tomcatServlet25() + throws IOException, InterruptedException, DigestException { + verifyBuildAndRun(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); + } + + private void verifyBuildAndRun(TestProject project, String label, String gradleBuildFile) + throws IOException, InterruptedException, DigestException { + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; + String targetImage = nameBase + label + System.nanoTime(); + String output = + JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); + containerName = output.trim(); + + HttpRequestTester.verifyBody( + "Hello world", + new URL("http://" + HttpRequestTester.fetchDockerHostForHttpRequest() + ":8080/hello")); + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle new file mode 100644 index 0000000000..82b0405ecd --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'eclipse-temurin:8-jdk-focal' + container { + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/gradle.properties b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/gradle.properties similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/gradle.properties rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/gradle.properties diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/libs/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/settings.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/settings.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/settings.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java similarity index 96% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java index 5fc6d1cf3a..a49407401d 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/test/resources/application/resources/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle new file mode 100644 index 0000000000..fe514bee06 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.google.cloud.tools.jib' +} + +// Applies the java plugin after Jib to make sure it works in this order. +apply plugin: 'java' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + user = 'myuser:mygroup' + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle new file mode 100644 index 0000000000..d5424994a1 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'com.google.cloud.tools.jib' +} + +// Applies the java plugin after Jib to make sure it works in this order. +apply plugin: 'java' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + from.image = 'eclipse-temurin:8-jdk-focal' + to { + image = System.getProperty('_TARGET_IMAGE') + credHelper = 'gcr' + } + container { + user = '12345:54321' + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } +} + +def additionalTag = System.getProperty('_ADDITIONAL_TAG') +if (additionalTag != null && !additionalTag.isEmpty()) { + jib.to.tags = [System.getProperty('_ADDITIONAL_TAG')] +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/src/main/java/com/test/Empty.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/empty/src/main/java/com/test/Empty.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/src/main/java/com/test/Empty.java diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle new file mode 100644 index 0000000000..786de565ae --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'com.google.cloud.tools.jib' + id 'java' +} + +dependencies { + implementation project(':b_dependency') +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + credHelper = 'gcr' + } + container { + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/empty/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/empty/src/main/java/com/test/Empty.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle new file mode 100644 index 0000000000..9900fd60df --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle @@ -0,0 +1 @@ +apply plugin: 'java-library' diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java new file mode 100644 index 0000000000..f5940d0dab --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java @@ -0,0 +1,6 @@ +package com.test; + +public class Empty { + + public static void main(String[] args) {} +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle new file mode 100644 index 0000000000..33eec26740 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle @@ -0,0 +1 @@ +apply plugin: 'base' diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle new file mode 100644 index 0000000000..ea19453ad3 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle @@ -0,0 +1,2 @@ +include ':a_packaged' +include ':b_dependency' diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-configuration.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-configuration.gradle new file mode 100644 index 0000000000..0d78b139d3 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-configuration.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + from { + image = "scratch" + } + configurationName = "myConfiguration" +} + + +configurations { + register("myConfiguration") + register("otherConfiguration") +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') + runtime 'com.google.guava:guava:30.1-jre' + myConfiguration files('libs/dependency2') + otherConfiguration files('libs/dependency3') +} + diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-cred-helper.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-cred-helper.gradle new file mode 100644 index 0000000000..50bb30fa06 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-cred-helper.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = 'gcr.io/distroless/java@sha256:2315ed1472a09826c1f31ab93ff13ceaa3a4e7d5482f357d15a296b3db0d1c96' + credHelper = 'gcr' + } + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper { + helper = 'gcr' + environment = [ + ENV_VAR: 'A VAR' + ] + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle new file mode 100644 index 0000000000..e08a14880a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } + dockerClient { + executable = file('mock-docker.sh') + environment = [envvar1:'value1', envvar2:'value2'] + } +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs-filtering.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs-filtering.gradle new file mode 100644 index 0000000000..4e04624dac --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs-filtering.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'busybox' + to.image = System.getProperty("_TARGET_IMAGE") + extraDirectories { + paths { + path { + from = 'src/main/custom-extra-dir3' + into = '/extras' + includes = ['**/*a*', '*.txt'] + excludes = ['**/*.txt'] + } + path { + from = 'src/main/custom-extra-dir4' + into = '/extras' + includes = ['foo'] + } + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle new file mode 100644 index 0000000000..da49d7772b --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'gcr.io/distroless/java@sha256:2315ed1472a09826c1f31ab93ff13ceaa3a4e7d5482f357d15a296b3db0d1c96' + to.image = System.getProperty("_TARGET_IMAGE") + extraDirectories.paths = ['src/main/custom-extra-dir', 'src/main/custom-extra-dir2'] +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle new file mode 100644 index 0000000000..f4c21d2005 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'gcr.io/distroless/java@sha256:2315ed1472a09826c1f31ab93ff13ceaa3a4e7d5482f357d15a296b3db0d1c96' + to.image = System.getProperty("_TARGET_IMAGE") + extraDirectories { + paths = files('src/main/custom-extra-dir', 'src/main/custom-extra-dir2') + permissions = ['/baz':'705'] + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs3.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs3.gradle new file mode 100644 index 0000000000..178c0f80c9 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs3.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'gcr.io/distroless/java@sha256:2315ed1472a09826c1f31ab93ff13ceaa3a4e7d5482f357d15a296b3db0d1c96' + to.image = System.getProperty("_TARGET_IMAGE") + extraDirectories { + paths { + path { + from = 'src/main/custom-extra-dir' + into = '/' + } + path { + from = 'src/main/custom-extra-dir2' + into = '/target/on/container' + } + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle new file mode 100644 index 0000000000..917a55ef7f --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jar { + manifest { + attributes( + 'Implementation-Title': 'helloworld', + 'Implementation-Version': '1' + ) + } +} + +jib { + to.image = System.getProperty("_TARGET_IMAGE") + from.image = 'eclipse-temurin:11-jdk-focal' + containerizingMode = 'packaged' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle new file mode 100644 index 0000000000..2cc07b662e --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.from.image = 'eclipse-temurin:8-jdk-focal' +jib.to.image = System.getProperty("_TARGET_IMAGE") \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle new file mode 100644 index 0000000000..8c563bfb00 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.from.image = 'eclipse-temurin:11-jdk-focal' +jib.to.image = System.getProperty("_TARGET_IMAGE") \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java17.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java17.gradle new file mode 100644 index 0000000000..9c860213ef --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java17.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 17 +targetCompatibility = 17 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle new file mode 100644 index 0000000000..a757ac89fe --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = System.getProperty("_BASE_IMAGE") + } + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + creationTime = 'EPOCH' + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] + workingDirectory = '/home' + extraClasspath = ['/d1','/d2'] + } + extraDirectories.paths = file('src/main/custom-extra-dir') +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle new file mode 100644 index 0000000000..5235164884 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = 'eclipse-temurin:11' + platforms { + platform { + architecture = 'amd64' + os = 'linux' + } + platform { + architecture = 'arm64' + os = 'linux' + } + } + } + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle new file mode 100644 index 0000000000..9771046978 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'eclipse-temurin:11-jdk-focal' + to.image = System.getProperty("_TARGET_IMAGE") + container { + filesModificationTime = '2011-12-03T10:15:30+09:00' + creationTime = '2013-11-05T06:29:30+09:00' + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle new file mode 100644 index 0000000000..b6d172b976 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + from.image = 'gcr.io/distroless/java@sha256:2315ed1472a09826c1f31ab93ff13ceaa3a4e7d5482f357d15a296b3db0d1c96' + to { + image = System.getProperty('_TARGET_IMAGE') + credHelper = 'gcr' + } + container { + creationTime = 'EPOCH' + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] + workingDirectory = '/home' + extraClasspath = ['/d1','/d2'] + } + outputPaths { + tar = file("$buildDir/different-jib-image.tar") + } + extraDirectories.paths = file('src/main/custom-extra-dir') +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle new file mode 100644 index 0000000000..c87182c454 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + + +jib { + from { + def dockerHost = System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost" + image = dockerHost + ':5000/distroless/java' + auth { + username = 'testuser' + password = 'testpassword' + } + } + to { + image = System.getProperty("_TARGET_IMAGE") + auth { + username = System.getProperty("_TARGET_USERNAME") + password = System.getProperty("_TARGET_PASSWORD") + } + } + container { + creationTime = 'USE_CURRENT_TIMESTAMP' + args = ['An argument.'] + mainClass = 'com.test.HelloWorld' + jvmFlags = ['-Xms512m', '-Xdebug'] + environment = [env1:'envvalue1', env2:'envvalue2'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] + } + extraDirectories { + paths = file('src/main/custom-extra-dir') + permissions = ['/foo':'755', '/bar/cat':'777'] + } + outputPaths { + digest = file("$buildDir/different-jib-image.digest") + imageId = file("different-jib-image.id") + } + allowInsecureRegistries = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency2 b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency3 b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency3 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh new file mode 100755 index 0000000000..84eafa19a8 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [[ "$1" == "info" ]]; then + # Output the JSON string + echo "{\"OSType\":\"linux\",\"Architecture\":\"x86_64\"}" + exit 0 +fi + +# Read stdin to avoid broken pipe +cat > /dev/null + +echo "Docker load called. $envvar1 $envvar2" \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/bar/cat b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/bar/cat rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/foo b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/foo rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz new file mode 100644 index 0000000000..3f95386662 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz @@ -0,0 +1 @@ +baz \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.json b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.txt b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.json b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.txt b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/bar b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/bar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/foo b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/foo new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..efceb0c3d7 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +import dependency.Greeting; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; + +/** Example class that uses a dependency and a resource file. */ +public class HelloWorld { + + public static void main(String[] args) throws IOException, URISyntaxException { + // 'Greeting' comes from the dependency artfiact. + String greeting = Greeting.getGreeting(); + + // Gets the contents of the resource file 'world'. + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader( + HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8))) { + String world = reader.readLine(); + System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); + Path worldFilePath = Paths.get("/app/resources/world"); + if (worldFilePath.toFile().exists()) { + System.out.println(Files.getLastModifiedTime(worldFilePath).toString()); + } + + // Prints the contents of the extra files. + if (Files.exists(Paths.get("/foo"))) { + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); + System.out.println( + new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); + System.out.println( + new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); + System.out.println(Files.getLastModifiedTime(Paths.get("/foo")).toString()); + System.out.println(Files.getLastModifiedTime(Paths.get("/bar/cat")).toString()); + } + // Prints the contents of the files in the second extra directory. + if (Files.exists(Paths.get("/target/on/container/baz"))) { + System.out.println( + new String( + Files.readAllBytes(Paths.get("/target/on/container/baz")), StandardCharsets.UTF_8)); + System.out.println( + Files.getLastModifiedTime(Paths.get("/target/on/container/baz")).toString()); + } + + // Prints jvm flags + for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + System.out.println(jvmFlag); + } + + if (System.getenv("env1") != null) { + System.out.println(System.getenv("env1")); + } + if (System.getenv("env2") != null) { + System.out.println(System.getenv("env2")); + } + + Package pack = HelloWorld.class.getPackage(); + if (pack.getImplementationTitle() != null) { + System.out.println("Implementation-Title: " + pack.getImplementationTitle()); + System.out.println("Implementation-Version: " + pack.getImplementationVersion()); + } + } + } +} diff --git a/jib-maven-plugin/src/test/resources/application/output/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/resources/world similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle new file mode 100644 index 0000000000..2dda8a4d31 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'org.springframework.boot' version '2.1.6.RELEASE' + id 'io.spring.dependency-management' version '1.0.6.RELEASE' + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +jib { + from.image = 'gcr.io/distroless/java:debug' + to.image = System.getProperty('_TARGET_IMAGE') + to.credHelper = 'gcr' + containerizingMode='packaged' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle new file mode 100644 index 0000000000..17fb2a2bfa --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} +rootProject.name = 'spring-boot' \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java new file mode 100644 index 0000000000..8c26fec153 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..3527662c07 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Hello world"; + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle new file mode 100644 index 0000000000..c8687a8ec5 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0' + moreLibs 'jakarta.annotation:jakarta.annotation-api:2.1.0' // random extra JAR +} + +war { + from ('src/extra_static') + from ('src/extra_js', { into 'js' }) + classpath configurations.moreLibs +} + +jib { + from.image = 'tomcat:10-jre8-temurin-focal' + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container.appRoot = '/usr/local/tomcat/webapps/ROOT' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle new file mode 100644 index 0000000000..9d64daa74e --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0' + moreLibs 'jakarta.annotation:jakarta.annotation-api:2.1.0' // random extra JAR +} + +war { + from ('src/extra_static') + from ('src/extra_js', { into 'js' }) + classpath configurations.moreLibs +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + credHelper = 'gcr' + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..ae65db32f0 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/resources/world similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/src/main/resources/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e3c07ab38a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

    Hello World!

    + + + + + + + + +
    Available Servlets:
    The HelloWorld servlet
    + + diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle deleted file mode 100644 index 87edcf9f3b..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib { - container { - args = ['An argument.'] - ports = ['1000/tcp', '2000-2003/udp'] - } - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true -} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle deleted file mode 100644 index 966d53be7e..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'com.google.cloud.tools.jib' -} - -// Applies the java plugin after Jib to make sure it works in this order. -apply plugin: 'java' - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -jib { - to { - image = 'gcr.io/jib-integration-testing/emptyimage:gradle' - credHelper = 'gcr' - } - container { - ports = ['1000/tcp', '2000-2003/udp'] - } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true -} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle deleted file mode 100644 index 24ea16fd7b..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib { - to { - image = 'gcr.io/jib-integration-testing/simpleimage:gradle' - credHelper = 'gcr' - } - container { - args = ['An argument.'] - ports = ['1000/tcp', '2000-2003/udp'] - } - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java deleted file mode 100644 index 65d6f48fc5..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import javax.annotation.Nullable; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Optional; - -/** - * A bean that configures authorization credentials to be used for a registry. This is configurable - * with Groovy closures and can be validated when used as a task input. - */ -public class AuthConfiguration { - - @Nullable private String username; - @Nullable private String password; - - @Input - @Nullable - @Optional - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - @Input - @Nullable - @Optional - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java new file mode 100644 index 0000000000..44ef74062d --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; + +/** + * A bean that configures authorization credentials to be used for a registry. This is configurable + * with Groovy closures and can be validated when used as a task input. + */ +public class AuthParameters implements AuthProperty { + + private Property username; + private Property password; + private final String source; + + @Inject + public AuthParameters(ObjectFactory objectFactory, String source) { + username = objectFactory.property(String.class); + password = objectFactory.property(String.class); + this.source = source; + } + + @Input + @Optional + @Override + @Nullable + public String getUsername() { + return username.getOrNull(); + } + + public void setUsername(String username) { + this.username.set(username); + } + + public void setUsername(Provider username) { + this.username.set(username); + } + + @Input + @Optional + @Override + @Nullable + public String getPassword() { + return password.getOrNull(); + } + + public void setPassword(String password) { + this.password.set(password); + } + + public void setPassword(Provider password) { + this.password.set(password); + } + + @Internal + @Override + public String getAuthDescriptor() { + return source; + } + + @Internal + @Override + public String getUsernameDescriptor() { + return getAuthDescriptor() + ".username"; + } + + @Internal + @Override + public String getPasswordDescriptor() { + return getAuthDescriptor() + ".password"; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java new file mode 100644 index 0000000000..0dad3f2a49 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -0,0 +1,121 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; + +/** Object in {@link JibExtension} that configures the base image. */ +public class BaseImageParameters { + + private final AuthParameters auth; + private final Property image; + private final CredHelperParameters credHelper; + private final PlatformParametersSpec platformParametersSpec; + private final ListProperty platforms; + + @Inject + public BaseImageParameters(ObjectFactory objectFactory) { + auth = objectFactory.newInstance(AuthParameters.class, "from.auth"); + platforms = objectFactory.listProperty(PlatformParameters.class); + image = objectFactory.property(String.class); + platformParametersSpec = objectFactory.newInstance(PlatformParametersSpec.class, platforms); + credHelper = + objectFactory.newInstance(CredHelperParameters.class, PropertyNames.FROM_CRED_HELPER); + + PlatformParameters amd64Linux = new PlatformParameters(); + amd64Linux.setArchitecture("amd64"); + amd64Linux.setOs("linux"); + platforms.add(amd64Linux); + } + + @Nested + @Optional + public ListProperty getPlatforms() { + String property = System.getProperty(PropertyNames.FROM_PLATFORMS); + if (property != null) { + List parsed = + ConfigurationPropertyValidator.parseListProperty(property).stream() + .map(PlatformParameters::of) + .collect(Collectors.toList()); + if (!parsed.equals(platforms.get())) { + platforms.set(parsed); + } + } + return platforms; + } + + public void platforms(Action action) { + platforms.empty(); + action.execute(platformParametersSpec); + } + + @Input + @Nullable + @Optional + public String getImage() { + if (System.getProperty(PropertyNames.FROM_IMAGE) != null) { + return System.getProperty(PropertyNames.FROM_IMAGE); + } + return image.getOrNull(); + } + + public void setImage(String image) { + this.image.set(image); + } + + public void setImage(Provider image) { + this.image.set(image); + } + + @Nested + @Optional + public CredHelperParameters getCredHelper() { + return credHelper; + } + + public void setCredHelper(String helper) { + this.credHelper.setHelper(helper); + } + + public void credHelper(Action action) { + action.execute(credHelper); + } + + @Nested + @Optional + public AuthParameters getAuth() { + // System properties are handled in ConfigurationPropertyValidator + return auth; + } + + public void auth(Action action) { + action.execute(auth); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index e3e95db1c5..692449db03 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,27 +16,31 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.docker.CliDockerClient; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.ExtraDirectoryNotFoundException; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidPlatformException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.globalconfig.InvalidGlobalConfigException; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -45,13 +49,9 @@ import org.gradle.api.tasks.options.Option; /** Builds a container image and exports to the default Docker daemon. */ -public class BuildDockerTask extends DefaultTask { +public class BuildDockerTask extends DefaultTask implements JibTask { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build to Docker daemon failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; @Nullable private JibExtension jibExtension; @@ -77,106 +77,125 @@ public void setTargetImage(String targetImage) { Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); } + /** + * Task Action, builds an image to docker daemon. + * + * @throws IOException if an error occurs creating the jib runner + * @throws BuildStepsExecutionException if an error occurs while executing build steps + * @throws CacheDirectoryCreationException if a new cache directory could not be created + * @throws MainClassInferenceException if a main class could not be found + * @throws InvalidGlobalConfigException if the global config file is invalid + */ @TaskAction - public void buildDocker() throws InvalidImageReferenceException, IOException { - if (!new DockerClient().isDockerInstalled()) { - throw new GradleException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); - } - - // Asserts required @Input parameters are not null. + public void buildDocker() + throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, + MainClassInferenceException, InvalidGlobalConfigException { Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); - - RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); - } - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); - - ImageReference targetImage = getDockerTag(gradleBuildLogger); - - // Builds the BuildConfiguration. - // TODO: Consolidate with BuildImageTask. - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleBuildLogger) - .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) - .setTargetImage(targetImage) - .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setMainClass(mainClass) - .setJavaArguments(jibExtension.getArgs()) - .setJvmFlags(jibExtension.getJvmFlags()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setAllowHttp(jibExtension.getAllowInsecureRegistries()); - if (Files.exists(jibExtension.getExtraDirectory().toPath())) { - try (Stream extraFilesLayerDirectoryFiles = - Files.list(jibExtension.getExtraDirectory().toPath())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - } - } - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); + // Check deprecated parameters + Path dockerExecutable = jibExtension.getDockerClient().getExecutablePath(); + boolean isDockerInstalled = + dockerExecutable == null + ? CliDockerClient.isDefaultDockerInstalled() + : CliDockerClient.isDockerInstalled(dockerExecutable); + if (!isDockerInstalled) { + throw new GradleException( + HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - // TODO: Instead of disabling logging, have authentication credentials be provided - GradleBuildLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); - - // Uses a directory in the Gradle build cache as the Jib cache. + TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + tempDirectoryProvider, + jibExtension.getConfigurationName().get()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, globalConfig, getLogger()); try { - BuildStepsRunner.forBuildToDockerDaemon( - buildConfiguration, gradleProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { - throw new GradleException(ex.getMessage(), ex.getCause()); + PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( + new GradleRawConfiguration(jibExtension), + ignored -> java.util.Optional.empty(), + projectProperties, + globalConfig, + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); + + } catch (InvalidAppRootException ex) { + throw new GradleException( + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); + + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); + + } catch (InvalidPlatformException ex) { + throw new GradleException( + "from.platforms contains a platform configuration that is missing required values or has invalid values: " + + ex.getMessage() + + ": " + + ex.getInvalidPlatform(), + ex); + + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + + } catch (JibPluginExtensionException ex) { + String extensionName = ex.getExtensionClass().getName(); + throw new GradleException( + "error running extension '" + extensionName + "': " + ex.getMessage(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); + + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (ExtraDirectoryNotFoundException ex) { + throw new GradleException( + "extraDirectories.paths contain \"from\" directory that doesn't exist locally: " + + ex.getPath(), + ex); + } finally { + tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); + projectProperties.waitForLoggingThread(); } } - BuildDockerTask setJibExtension(JibExtension jibExtension) { + @Override + public BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Returns an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - * - * @param gradleBuildLogger the logger used to notify users of the target image parameter - * @return an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - */ - ImageReference getDockerTag(GradleBuildLogger gradleBuildLogger) - throws InvalidImageReferenceException { - Preconditions.checkNotNull(jibExtension); - if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { - // TODO: Validate that project name and version are valid repository/tag - // TODO: Use HelpfulSuggestions - gradleBuildLogger.lifecycle( - "Tagging image with generated image reference " - + getProject().getName() - + ":" - + getProject().getVersion().toString() - + ". If you'd like to specify a different tag, you can set the jib.to.image " - + "parameter in your build.gradle, or use the --image= commandline flag."); - return ImageReference.of(null, getProject().getName(), getProject().getVersion().toString()); - } else { - return ImageReference.parse(jibExtension.getTargetImage()); - } - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 90aa14d58b..49265559a6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,26 +16,30 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.ExtraDirectoryNotFoundException; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidPlatformException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.globalconfig.InvalidGlobalConfigException; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -43,14 +47,10 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; -/** Builds a container image. */ -public class BuildImageTask extends DefaultTask { +/** Builds a container image to registry. */ +public class BuildImageTask extends DefaultTask implements JibTask { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build image failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build image failed"; @Nullable private JibExtension jibExtension; @@ -76,85 +76,120 @@ public void setTargetImage(String targetImage) { Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); } + /** + * Task Action, builds an image to remote registry. + * + * @throws IOException if an error occurs creating the jib runner + * @throws BuildStepsExecutionException if an error occurs while executing build steps + * @throws CacheDirectoryCreationException if a new cache directory could not be created + * @throws MainClassInferenceException if a main class could not be found + * @throws InvalidGlobalConfigException if the global config file is invalid + */ @TaskAction - public void buildImage() throws InvalidImageReferenceException, IOException { + public void buildImage() + throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, + MainClassInferenceException, InvalidGlobalConfigException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); + TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + tempDirectoryProvider, + jibExtension.getConfigurationName().get()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, globalConfig, getLogger()); + try { + if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { + throw new GradleException( + HelpfulSuggestions.forToNotConfigured( + "Missing target image parameter", + "'jib.to.image'", + "build.gradle", + "gradle jib --image ")); + } + + PluginConfigurationProcessor.createJibBuildRunnerForRegistryImage( + new GradleRawConfiguration(jibExtension), + ignored -> Optional.empty(), + projectProperties, + globalConfig, + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); - if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { + } catch (InvalidAppRootException ex) { throw new GradleException( - HelpfulSuggestionsProvider.get("Missing target image parameter") - .forToNotConfigured( - "'jib.to.image'", "build.gradle", "gradle jib --image ")); - } + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); - RegistryCredentials knownBaseRegistryCredentials = null; - RegistryCredentials knownTargetRegistryCredentials = null; - Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); - } - Authorization toAuthorization = jibExtension.getTo().getImageAuthorization(); - if (toAuthorization != null) { - knownTargetRegistryCredentials = new RegistryCredentials("jib.to.auth", toAuthorization); - } + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); - - // Builds the BuildConfiguration. - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleBuildLogger) - .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) - .setTargetImage(ImageReference.parse(jibExtension.getTargetImage())) - .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setTargetImageCredentialHelperName(jibExtension.getTo().getCredHelper()) - .setKnownTargetRegistryCredentials(knownTargetRegistryCredentials) - .setMainClass(mainClass) - .setJavaArguments(jibExtension.getArgs()) - .setJvmFlags(jibExtension.getJvmFlags()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setTargetFormat(jibExtension.getFormat()) - .setAllowHttp(jibExtension.getAllowInsecureRegistries()); - if (Files.exists(jibExtension.getExtraDirectory().toPath())) { - try (Stream extraFilesLayerDirectoryFiles = - Files.list(jibExtension.getExtraDirectory().toPath())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - } - } - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); + } catch (InvalidPlatformException ex) { + throw new GradleException( + "from.platforms contains a platform configuration that is missing required values or has invalid values: " + + ex.getMessage() + + ": " + + ex.getInvalidPlatform(), + ex); - // TODO: Instead of disabling logging, have authentication credentials be provided - GradleBuildLogger.disableHttpLogging(); + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); - try { - BuildStepsRunner.forBuildImage( - buildConfiguration, gradleProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + + } catch (JibPluginExtensionException ex) { + String extensionName = ex.getExtensionClass().getName(); + throw new GradleException( + "error running extension '" + extensionName + "': " + ex.getMessage(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { - throw new GradleException(ex.getMessage(), ex.getCause()); + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (ExtraDirectoryNotFoundException ex) { + throw new GradleException( + "extraDirectories.paths contain \"from\" directory that doesn't exist locally: " + + ex.getPath(), + ex); + } finally { + tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); + projectProperties.waitForLoggingThread(); } } - BuildImageTask setJibExtension(JibExtension jibExtension) { + @Override + public BuildImageTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java new file mode 100644 index 0000000000..1a77856b6b --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -0,0 +1,218 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.ExtraDirectoryNotFoundException; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; +import com.google.cloud.tools.jib.plugins.common.InvalidPlatformException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.cloud.tools.jib.plugins.common.globalconfig.InvalidGlobalConfigException; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; + +/** Builds a container image to a tarball. */ +public class BuildTarTask extends DefaultTask implements JibTask { + + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Building image tarball failed"; + + @Nullable private JibExtension jibExtension; + + /** + * This will call the property {@code "jib"} so that it is the same name as the extension. This + * way, the user would see error messages for missing configuration with the prefix {@code jib.}. + * + * @return the {@link JibExtension}. + */ + @Nested + @Nullable + public JibExtension getJib() { + return jibExtension; + } + + /** + * The target image can be overridden with the {@code --image} command line option. + * + * @param targetImage the name of the 'to' image. + */ + @Option(option = "image", description = "The image reference for the target image") + public void setTargetImage(String targetImage) { + Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); + } + + /** + * Returns a collection of all the files that jib includes in the image. Only used to calculate + * UP-TO-DATE. + * + * @return a list of paths of input files + */ + @InputFiles + public FileCollection getInputFiles() { + List extraDirectories = + Preconditions.checkNotNull(jibExtension).getExtraDirectories().getPaths().stream() + .map(ExtraDirectoryParameters::getFrom) + .collect(Collectors.toList()); + return GradleProjectProperties.getInputFiles( + getProject(), extraDirectories, jibExtension.getConfigurationName().get()); + } + + /** + * The output file to check for task up-to-date. + * + * @return the output path + */ + @OutputFile + public String getOutputFile() { + return Preconditions.checkNotNull(jibExtension).getOutputPaths().getTarPath().toString(); + } + + /** + * Task Action, builds an image to tar file. + * + * @throws IOException if an error occurs creating the jib runner + * @throws BuildStepsExecutionException if an error occurs while executing build steps + * @throws CacheDirectoryCreationException if a new cache directory could not be created + * @throws MainClassInferenceException if a main class could not be found + * @throws InvalidGlobalConfigException if the global config file is invalid + */ + @TaskAction + public void buildTar() + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, InvalidGlobalConfigException { + // Asserts required @Input parameters are not null. + Preconditions.checkNotNull(jibExtension); + TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + tempDirectoryProvider, + jibExtension.getConfigurationName().get()); + GlobalConfig globalConfig = GlobalConfig.readConfig(); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, globalConfig, getLogger()); + try { + PluginConfigurationProcessor.createJibBuildRunnerForTarImage( + new GradleRawConfiguration(jibExtension), + ignored -> Optional.empty(), + projectProperties, + globalConfig, + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); + + } catch (InvalidAppRootException ex) { + throw new GradleException( + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); + + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); + } catch (InvalidPlatformException ex) { + throw new GradleException( + "from.platforms contains a platform configuration that is missing required values or has invalid values: " + + ex.getMessage() + + ": " + + ex.getInvalidPlatform(), + ex); + + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + + } catch (JibPluginExtensionException ex) { + String extensionName = ex.getExtensionClass().getName(); + throw new GradleException( + "error running extension '" + extensionName + "': " + ex.getMessage(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); + + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (ExtraDirectoryNotFoundException ex) { + throw new GradleException( + "extraDirectories.paths contain \"from\" directory that doesn't exist locally: " + + ex.getPath(), + ex); + } finally { + tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); + projectProperties.waitForLoggingThread(); + } + } + + @Override + public BuildTarTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 8b9aa39461..095da5008a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,12 +16,20 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -31,36 +39,141 @@ */ public class ContainerParameters { - private List jvmFlags = Collections.emptyList(); - @Nullable private String mainClass; - private List args = Collections.emptyList(); + private final ListProperty jvmFlags; + private Map environment = Collections.emptyMap(); + private ListProperty entrypoint; + private List extraClasspath = Collections.emptyList(); + private boolean expandClasspathDependencies; + private final Property mainClass; + @Nullable private List args; private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); + private List volumes = Collections.emptyList(); + private MapProperty labels; + private String appRoot = ""; + @Nullable private String user; + @Nullable private String workingDirectory; + private final Property filesModificationTime; + private final Property creationTime; + + @Inject + public ContainerParameters(ObjectFactory objectFactory) { + labels = objectFactory.mapProperty(String.class, String.class).empty(); + filesModificationTime = objectFactory.property(String.class).convention("EPOCH_PLUS_SECOND"); + creationTime = objectFactory.property(String.class).convention("EPOCH"); + mainClass = objectFactory.property(String.class); + jvmFlags = objectFactory.listProperty(String.class); + entrypoint = objectFactory.listProperty(String.class); + } + + @Input + @Nullable + @Optional + public List getEntrypoint() { + if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT)); + } + return entrypoint.getOrNull(); + } + + public void setEntrypoint(List entrypoint) { + this.entrypoint.set(entrypoint); + } + + public void setEntrypoint(Provider> entrypoint) { + this.entrypoint.set(entrypoint); + } + + public void setEntrypoint(String entrypoint) { + this.entrypoint.set(Collections.singletonList(entrypoint)); + } @Input @Optional public List getJvmFlags() { - return jvmFlags; + String jvmFlagsSystemProperty = System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS); + if (jvmFlagsSystemProperty != null) { + return ConfigurationPropertyValidator.parseListProperty(jvmFlagsSystemProperty); + } + return jvmFlags.getOrElse(Collections.emptyList()); } public void setJvmFlags(List jvmFlags) { - this.jvmFlags = jvmFlags; + this.jvmFlags.set(jvmFlags); + } + + public void setJvmFlags(Provider> jvmFlags) { + this.jvmFlags.set(jvmFlags); + } + + @Input + @Optional + public Map getEnvironment() { + if (System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT)); + } + return environment; + } + + public void setEnvironment(Map environment) { + this.environment = environment; + } + + @Input + @Optional + public List getExtraClasspath() { + if (System.getProperty(PropertyNames.CONTAINER_EXTRA_CLASSPATH) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_EXTRA_CLASSPATH)); + } + return extraClasspath; + } + + public void setExtraClasspath(List classpath) { + extraClasspath = classpath; + } + + @Input + public boolean getExpandClasspathDependencies() { + if (System.getProperty(PropertyNames.EXPAND_CLASSPATH_DEPENDENCIES) != null) { + return Boolean.valueOf(System.getProperty(PropertyNames.EXPAND_CLASSPATH_DEPENDENCIES)); + } + return expandClasspathDependencies; + } + + public void setExpandClasspathDependencies(boolean expand) { + expandClasspathDependencies = expand; } @Input @Nullable @Optional public String getMainClass() { - return mainClass; + String mainClassProperty = System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS); + if (mainClassProperty != null) { + return mainClassProperty; + } + return mainClass.getOrNull(); } public void setMainClass(String mainClass) { - this.mainClass = mainClass; + this.mainClass.set(mainClass); + } + + public void setMainClass(Provider mainClass) { + this.mainClass.set(mainClass); } @Input + @Nullable @Optional public List getArgs() { + if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_ARGS)); + } return args; } @@ -70,21 +183,121 @@ public void setArgs(List args) { @Input @Optional - public Class getFormat() { - return Preconditions.checkNotNull(format).getManifestTemplateClass(); + public ImageFormat getFormat() { + if (System.getProperty(PropertyNames.CONTAINER_FORMAT) != null) { + return ImageFormat.valueOf(System.getProperty(PropertyNames.CONTAINER_FORMAT)); + } + return Preconditions.checkNotNull(format); } public void setFormat(ImageFormat format) { this.format = format; } + public void setFormat(String format) { + this.format = ImageFormat.valueOf(format); + } + @Input @Optional public List getPorts() { + if (System.getProperty(PropertyNames.CONTAINER_PORTS) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_PORTS)); + } return ports; } public void setPorts(List ports) { this.ports = ports; } + + @Input + @Optional + public List getVolumes() { + if (System.getProperty(PropertyNames.CONTAINER_VOLUMES) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_VOLUMES)); + } + return volumes; + } + + public void setVolumes(List volumes) { + this.volumes = volumes; + } + + @Input + @Optional + public MapProperty getLabels() { + String labelsProperty = System.getProperty(PropertyNames.CONTAINER_LABELS); + if (labelsProperty != null) { + Map parsedLabels = + ConfigurationPropertyValidator.parseMapProperty(labelsProperty); + if (!parsedLabels.equals(labels.get())) { + labels.set(parsedLabels); + } + } + return labels; + } + + @Input + @Optional + public String getAppRoot() { + if (System.getProperty(PropertyNames.CONTAINER_APP_ROOT) != null) { + return System.getProperty(PropertyNames.CONTAINER_APP_ROOT); + } + return appRoot; + } + + public void setAppRoot(String appRoot) { + this.appRoot = appRoot; + } + + @Input + @Nullable + @Optional + public String getUser() { + if (System.getProperty(PropertyNames.CONTAINER_USER) != null) { + return System.getProperty(PropertyNames.CONTAINER_USER); + } + return user; + } + + public void setUser(String user) { + this.user = user; + } + + @Input + @Nullable + @Optional + public String getWorkingDirectory() { + if (System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY) != null) { + return System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY); + } + return workingDirectory; + } + + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + + @Input + @Optional + public Property getFilesModificationTime() { + String property = System.getProperty(PropertyNames.CONTAINER_FILES_MODIFICATION_TIME); + if (property != null && !property.equals(filesModificationTime.get())) { + filesModificationTime.set(property); + } + return filesModificationTime; + } + + @Input + @Optional + public Property getCreationTime() { + String property = System.getProperty(PropertyNames.CONTAINER_CREATION_TIME); + if (property != null && !property.equals(creationTime.get())) { + creationTime.set(property); + } + return creationTime; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CredHelperParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CredHelperParameters.java new file mode 100644 index 0000000000..8a972f719a --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CredHelperParameters.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.CredHelperConfiguration; +import java.util.Map; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; + +/** Configuration for a credential helper. */ +public class CredHelperParameters implements CredHelperConfiguration { + private final String propertyName; + private final MapProperty environment; + @Nullable private String helper; + + @Inject + public CredHelperParameters(ObjectFactory objectFactory, String propertyName) { + this.propertyName = propertyName; + environment = objectFactory.mapProperty(String.class, String.class).empty(); + } + + @Input + @Nullable + @Optional + public String getHelper() { + if (System.getProperty(propertyName) != null) { + return System.getProperty(propertyName); + } + return helper; + } + + @Internal + @Override + public java.util.Optional getHelperName() { + return java.util.Optional.ofNullable(getHelper()); + } + + public void setHelper(String helper) { + this.helper = helper; + } + + @Override + @Input + @Optional + public Map getEnvironment() { + return environment.get(); + } + + public void setEnvironment(Map environment) { + this.environment.set(environment); + } + + public void setEnvironment(Provider> environment) { + this.environment.set(environment); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java new file mode 100644 index 0000000000..90af55c2c0 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nullable; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; + +/** + * Object that configures the Docker executable and the additional environment variables to use when + * executing the executable. + */ +public class DockerClientParameters { + + @Nullable private Path executable; + private Map environment = Collections.emptyMap(); + + @Input + @Nullable + @Optional + public String getExecutable() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE) != null) { + return System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE); + } + return executable == null ? null : executable.toString(); + } + + @Internal + @Nullable + Path getExecutablePath() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE) != null) { + return Paths.get(System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE)); + } + return executable; + } + + public void setExecutable(String executable) { + this.executable = Paths.get(executable); + } + + @Input + @Optional + public Map getEnvironment() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT)); + } + return environment; + } + + public void setEnvironment(Map environment) { + this.environment = environment; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java deleted file mode 100644 index 7a57bc17f3..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.docker.DockerContextGenerator; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.common.base.Preconditions; -import com.google.common.io.InsecureRecursiveDeleteException; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import javax.annotation.Nullable; -import org.gradle.api.DefaultTask; -import org.gradle.api.GradleException; -import org.gradle.api.Task; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.Nested; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.options.Option; - -public class DockerContextTask extends DefaultTask { - - @Nullable private String targetDir; - @Nullable private JibExtension jibExtension; - - /** - * This will call the property {@code "jib"} so that it is the same name as the extension. This - * way, the user would see error messages for missing configuration with the prefix {@code jib.}. - * - * @return the {@link JibExtension}. - */ - @Nested - @Nullable - public JibExtension getJib() { - return jibExtension; - } - - /** - * @return the input files to this task are all the output files for all the dependencies of the - * {@code classes} task. - */ - @InputFiles - public FileCollection getInputFiles() { - Task classesTask = getProject().getTasks().getByPath("classes"); - Set classesDependencies = - classesTask.getTaskDependencies().getDependencies(classesTask); - - List dependencyFileCollections = new ArrayList<>(); - for (Task task : classesDependencies) { - dependencyFileCollections.add(task.getOutputs().getFiles()); - } - return getProject().files(dependencyFileCollections); - } - - /** - * The output directory to check for task up-to-date. - * - * @return the output directory - */ - @OutputDirectory - public String getOutputDirectory() { - return getTargetDir(); - } - - /** - * Returns the output directory for the Docker context. By default, it is {@code - * build/jib-docker-context}. - * - * @return the output directory - */ - @Input - public String getTargetDir() { - if (targetDir == null) { - return getProject().getBuildDir().toPath().resolve("jib-docker-context").toString(); - } - return targetDir; - } - - /** - * The output directory can be overriden with the {@code --targetDir} command line option. - * - * @param targetDir the output directory. - */ - @Option(option = "targetDir", description = "Directory to output the Docker context to") - public void setTargetDir(String targetDir) { - this.targetDir = targetDir; - } - - @TaskAction - public void generateDockerContext() { - Preconditions.checkNotNull(jibExtension); - - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); - - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); - - String targetDir = getTargetDir(); - - try { - // Validate port input, but don't save the output because we don't want the ranges expanded - // here. - ExposedPortsParser.parse(jibExtension.getExposedPorts()); - - // TODO: Add support for extra files layer. - new DockerContextGenerator(gradleProjectProperties.getSourceFilesConfiguration()) - .setBaseImage(jibExtension.getBaseImage()) - .setJvmFlags(jibExtension.getJvmFlags()) - .setMainClass(mainClass) - .setJavaArguments(jibExtension.getArgs()) - .setExposedPorts(jibExtension.getExposedPorts()) - .generate(Paths.get(targetDir)); - - gradleBuildLogger.lifecycle("Created Docker context at " + targetDir); - - } catch (InsecureRecursiveDeleteException ex) { - throw new GradleException( - HelpfulSuggestionsProvider.get( - "Export Docker context failed because cannot clear directory '" - + getTargetDir() - + "' safely") - .forDockerContextInsecureRecursiveDelete(getTargetDir()), - ex); - - } catch (IOException ex) { - throw new GradleException( - HelpfulSuggestionsProvider.get("Export Docker context failed") - .suggest("check if the command-line option `--jib.dockerDir` is set correctly"), - ex); - } - } - - DockerContextTask setJibExtension(JibExtension jibExtension) { - this.jibExtension = jibExtension; - return this; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParameters.java new file mode 100644 index 0000000000..af3e987c59 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParameters.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.ExtensionConfiguration; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nullable; +import org.gradle.api.Action; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Configuration of a plugin extension. */ +public class ExtensionParameters implements ExtensionConfiguration { + + private String implementation = ""; + private Map properties = Collections.emptyMap(); + @Nullable private Action action; + + @Input + public String getImplementation() { + return getExtensionClass(); + } + + @Internal + @Override + public String getExtensionClass() { + return implementation; + } + + public void setImplementation(String implementation) { + this.implementation = implementation; + } + + @Input + @Override + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + @Internal + @Override + public java.util.Optional getExtraConfiguration() { + return java.util.Optional.ofNullable(action); + } + + public void configuration(Action action) { + this.action = action; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParametersSpec.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParametersSpec.java new file mode 100644 index 0000000000..b4cbdd04c3 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParametersSpec.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; + +/** Allows to add {@link ExtensionParameters} objects to the list property of the same type. */ +public class ExtensionParametersSpec { + + private final ObjectFactory objectFactory; + private final ListProperty pluginExtensions; + + @Inject + public ExtensionParametersSpec( + ObjectFactory objectFactory, ListProperty pluginExtensions) { + this.objectFactory = objectFactory; + this.pluginExtensions = pluginExtensions; + } + + /** + * Adds a new plugin extension configuration to the extensions list. + * + * @param action closure representing an extension configuration + */ + public void pluginExtension(Action action) { + ExtensionParameters extension = objectFactory.newInstance(ExtensionParameters.class); + action.execute(extension); + pluginExtensions.add(extension); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java new file mode 100644 index 0000000000..30dc526e89 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java @@ -0,0 +1,142 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.io.File; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Object in {@link JibExtension} that configures the extra directories. */ +public class ExtraDirectoriesParameters { + + private final ObjectFactory objects; + private final Project project; + + private ListProperty paths; + private ExtraDirectoryParametersSpec spec; + private MapProperty permissions; + + @Inject + public ExtraDirectoriesParameters(ObjectFactory objects, Project project) { + this.objects = objects; + this.project = project; + paths = objects.listProperty(ExtraDirectoryParameters.class).empty(); + spec = objects.newInstance(ExtraDirectoryParametersSpec.class, project, paths); + permissions = objects.mapProperty(String.class, String.class).empty(); + } + + public void paths(Action action) { + action.execute(spec); + } + + @Input + public List getPathStrings() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + return getPaths().stream() + .map(extraDirectoryParameters -> extraDirectoryParameters.getFrom().toString()) + .collect(Collectors.toList()); + } + + @Internal + public List getPaths() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + String property = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); + if (property != null) { + List pathStrings = ConfigurationPropertyValidator.parseListProperty(property); + return pathStrings.stream() + .map(path -> new ExtraDirectoryParameters(objects, project, Paths.get(path), "/")) + .collect(Collectors.toList()); + } + if (paths.get().isEmpty()) { + return Collections.singletonList( + new ExtraDirectoryParameters( + objects, + project, + project.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib"), + "/")); + } + return paths.get(); + } + + /** + * Sets paths. {@code paths} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). + * + * @param paths paths to set. + */ + public void setPaths(Object paths) { + this.paths.set(convertToExtraDirectoryParametersList(paths)); + } + + /** + * Sets paths, for lazy evaluation where {@code paths} is a {@link Provider} of a suitable object. + * + * @param paths provider of paths to set + * @see #setPaths(Object) + */ + public void setPaths(Provider paths) { + this.paths.set(paths.map(this::convertToExtraDirectoryParametersList)); + } + + /** + * Helper method to convert {@code Object} to {@code List} in {@code + * setFrom}. + */ + @Nonnull + private List convertToExtraDirectoryParametersList(Object obj) { + return project.files(obj).getFiles().stream() + .map(file -> new ExtraDirectoryParameters(objects, project, file.toPath(), "/")) + .collect(Collectors.toList()); + } + + /** + * Gets the permissions for files in the extra layer on the container. Maps from absolute path on + * the container to a 3-digit octal string representation of the file permission bits (e.g. {@code + * "/path/on/container" -> "755"}). + * + * @return the permissions map from path on container to file permissions + */ + @Input + public MapProperty getPermissions() { + String property = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); + if (property != null) { + Map parsedPermissions = + ConfigurationPropertyValidator.parseMapProperty(property); + if (!parsedPermissions.equals(permissions.get())) { + permissions.set(parsedPermissions); + } + } + return permissions; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java new file mode 100644 index 0000000000..537b51f085 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.ExtraDirectoriesConfiguration; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Configuration of an extra directory. */ +public class ExtraDirectoryParameters implements ExtraDirectoriesConfiguration { + + private Project project; + private Property from; + private Property into; + private ListProperty includes; + private ListProperty excludes; + + @Inject + public ExtraDirectoryParameters(ObjectFactory objects, Project project) { + this.project = project; + this.from = objects.property(Path.class).value(Paths.get("")); + this.into = objects.property(String.class).value("/"); + this.includes = objects.listProperty(String.class).empty(); + this.excludes = objects.listProperty(String.class).empty(); + } + + ExtraDirectoryParameters(ObjectFactory objects, Project project, Path from, String into) { + this(objects, project); + this.from = objects.property(Path.class).value(from); + this.into = objects.property(String.class).value(into); + } + + @Input + public String getFromString() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + return from.get().toString(); + } + + @Override + @Internal + public Path getFrom() { + return from.get(); + } + + public void setFrom(Object from) { + this.from.set(project.file(from).toPath()); + } + + public void setFrom(Provider from) { + this.from.set(from.map(obj -> project.file(obj).toPath())); + } + + @Override + @Input + public String getInto() { + return into.get(); + } + + public void setInto(String into) { + this.into.set(into); + } + + public void setInto(Provider into) { + this.into.set(into); + } + + @Input + public ListProperty getIncludes() { + return includes; + } + + @Input + public ListProperty getExcludes() { + return excludes; + } + + @Override + @Internal + public List getIncludesList() { + return includes.get(); + } + + @Override + @Internal + public List getExcludesList() { + return excludes.get(); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParametersSpec.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParametersSpec.java new file mode 100644 index 0000000000..53c57779c6 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParametersSpec.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; + +/** Allows to add {@link ExtraDirectoryParameters} objects to the list property of the same type. */ +public class ExtraDirectoryParametersSpec { + + private final Project project; + private final ListProperty paths; + + @Inject + public ExtraDirectoryParametersSpec( + Project project, ListProperty paths) { + this.project = project; + this.paths = paths; + } + + /** + * Adds a new extra directory configuration to the list. + * + * @param action closure representing an extra directory configuration + */ + public void path(Action action) { + ExtraDirectoryParameters extraDirectory = + project.getObjects().newInstance(ExtraDirectoryParameters.class, project); + action.execute(extraDirectory); + paths.add(extraDirectory); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java deleted file mode 100644 index 75c365ae2e..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.builder.BuildLogger; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import org.gradle.api.logging.Logger; -import org.gradle.internal.logging.events.LogEvent; -import org.gradle.internal.logging.events.OutputEventListener; -import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; -import org.slf4j.LoggerFactory; - -/** Implementation of {@link BuildLogger} for Gradle plugins. */ -// We don't care about the return values of the logging futures. -@SuppressWarnings("FutureReturnValueIgnored") -class GradleBuildLogger implements BuildLogger { - - /** This executor keeps all log messages in order. */ - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - /** - * Disables annoying Apache/Google HTTP client logging. Note that this is a hack and depends on - * internal Gradle classes. - */ - static void disableHttpLogging() { - // Disables Apache HTTP client logging. - OutputEventListenerBackedLoggerContext context = - (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); - OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); - context.setOutputEventListener( - event -> { - LogEvent logEvent = (LogEvent) event; - if (!logEvent.getCategory().contains("org.apache")) { - defaultOutputEventListener.onOutput(event); - } - }); - - // Disables Google HTTP client logging. - java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); - } - - private final Logger logger; - - GradleBuildLogger(Logger logger) { - this.logger = logger; - } - - @Override - public void lifecycle(CharSequence message) { - executorService.submit(() -> logger.lifecycle(message.toString())); - } - - @Override - public void debug(CharSequence message) { - executorService.submit(() -> logger.debug(message.toString())); - } - - @Override - public void info(CharSequence message) { - executorService.submit(() -> logger.info(message.toString())); - } - - @Override - public void warn(CharSequence message) { - executorService.submit(() -> logger.warn("warning: " + message)); - } - - @Override - public void error(CharSequence message) { - executorService.submit(() -> logger.error(message.toString())); - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java new file mode 100644 index 0000000000..dc6135c080 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; + +/** Gradle-specific {@link HelpfulSuggestions}. */ +class GradleHelpfulSuggestions extends HelpfulSuggestions { + + GradleHelpfulSuggestions(String messagePrefix) { + super(messagePrefix, "gradle clean", "jib.to.image", "--image", "build.gradle"); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 258c15fc7c..6a1978fe19 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,70 +16,340 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.frontend.MainClassFinder; -import com.google.cloud.tools.jib.frontend.MainClassInferenceException; -import com.google.cloud.tools.jib.frontend.ProjectProperties; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.gradle.extension.JibGradlePluginExtension; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; +import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; +import com.google.cloud.tools.jib.plugins.common.PluginExtensionLogger; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.ExtensionConfiguration; +import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; +import com.google.cloud.tools.jib.plugins.extension.NullExtension; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.tools.ant.taskdefs.condition.Os; +import org.gradle.api.Action; import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ -class GradleProjectProperties implements ProjectProperties { +public class GradleProjectProperties implements ProjectProperties { + /** Used to generate the User-Agent header and history metadata. */ + private static final String TOOL_NAME = "jib-gradle-plugin"; + + /** Used to generate the User-Agent header and history metadata and verify versions. */ + static final String TOOL_VERSION = + GradleProjectProperties.class.getPackage().getImplementationVersion(); + + /** Used for logging during main class inference. */ private static final String PLUGIN_NAME = "jib"; + + /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'jar' task"; - /** @return a GradleProjectProperties from the given project and logger. */ - static GradleProjectProperties getForProject( - Project project, GradleBuildLogger gradleBuildLogger) { - try { - return new GradleProjectProperties( - project, - gradleBuildLogger, - GradleSourceFilesConfiguration.getForProject(project, gradleBuildLogger)); + /** Name of the `main` {@link SourceSet} to use as source files. */ + private static final String MAIN_SOURCE_SET_NAME = "main"; - } catch (IOException ex) { - throw new GradleException("Obtaining project build output files failed", ex); + private static final Duration LOGGING_THREAD_SHUTDOWN_TIMEOUT = Duration.ofSeconds(1); + + /** + * Generate an instance for a gradle project. + * + * @param project a gradle project + * @param logger a gradle logging instance to use for logging during the build + * @param tempDirectoryProvider for scratch space during the build + * @param configurationName the configuration of which the dependencies should be packed into the + * container + * @return a GradleProjectProperties instance to use in a jib build + */ + public static GradleProjectProperties getForProject( + Project project, + Logger logger, + TempDirectoryProvider tempDirectoryProvider, + String configurationName) { + Supplier>> extensionLoader = + () -> { + List> extensions = new ArrayList<>(); + for (JibGradlePluginExtension extension : + ServiceLoader.load(JibGradlePluginExtension.class)) { + extensions.add(extension); + } + return extensions; + }; + return new GradleProjectProperties( + project, logger, tempDirectoryProvider, extensionLoader, configurationName); + } + + String getWarFilePath() { + TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + if (bootWarTask != null && bootWarTask.get().getEnabled()) { + return bootWarTask.get().getOutputs().getFiles().getAsPath(); + } + + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); + return Verify.verifyNotNull(warTask).get().getOutputs().getFiles().getAsPath(); + } + + private static boolean isProgressFooterEnabled(Project project) { + if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { + return false; + } + + switch (project.getGradle().getStartParameter().getConsoleOutput()) { + case Plain: + return false; + + case Auto: + // Enables progress footer when ANSI is supported (Windows or TERM not 'dumb'). + // Unlike jib-maven-plugin, we cannot test "System.console() != null". + // https://github.com/GoogleContainerTools/jib/issues/2920#issuecomment-749234458 + return Os.isFamily(Os.FAMILY_WINDOWS) || !"dumb".equals(System.getenv("TERM")); + + default: + return true; } } private final Project project; - private final GradleBuildLogger gradleBuildLogger; - private final SourceFilesConfiguration sourceFilesConfiguration; + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); + private final ConsoleLogger consoleLogger; + private final TempDirectoryProvider tempDirectoryProvider; + private final Supplier>> extensionLoader; + private final String configurationName; @VisibleForTesting GradleProjectProperties( Project project, - GradleBuildLogger gradleBuildLogger, - SourceFilesConfiguration sourceFilesConfiguration) { + Logger logger, + TempDirectoryProvider tempDirectoryProvider, + Supplier>> extensionLoader, + String configurationName) { this.project = project; - this.gradleBuildLogger = gradleBuildLogger; - this.sourceFilesConfiguration = sourceFilesConfiguration; + this.tempDirectoryProvider = tempDirectoryProvider; + this.extensionLoader = extensionLoader; + this.configurationName = configurationName; + ConsoleLoggerBuilder consoleLoggerBuilder = + (isProgressFooterEnabled(project) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor, false) + : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) + .lifecycle(logger::lifecycle); + if (logger.isDebugEnabled()) { + consoleLoggerBuilder.debug(logger::debug); + } + if (logger.isInfoEnabled()) { + consoleLoggerBuilder.info(logger::info); + } + if (logger.isWarnEnabled()) { + consoleLoggerBuilder.warn(logger::warn); + } + if (logger.isErrorEnabled()) { + consoleLoggerBuilder.error(logger::error); + } + consoleLogger = consoleLoggerBuilder.build(); + } + + @Override + public JibContainerBuilder createJibContainerBuilder( + JavaContainerBuilder javaContainerBuilder, ContainerizingMode containerizingMode) { + try { + FileCollection projectDependencies = + project.files( + project.getConfigurations().getByName(configurationName).getResolvedConfiguration() + .getResolvedArtifacts().stream() + .filter( + artifact -> + artifact.getId().getComponentIdentifier() + instanceof ProjectComponentIdentifier) + .map(ResolvedArtifact::getFile) + .collect(Collectors.toList())); + + if (isWarProject()) { + String warFilePath = getWarFilePath(); + log(LogEvent.info("WAR project identified, creating WAR image from: " + warFilePath)); + Path explodedWarPath = tempDirectoryProvider.newDirectory(); + ZipUtil.unzip(Paths.get(warFilePath), explodedWarPath); + return JavaContainerBuilderHelper.fromExplodedWar( + javaContainerBuilder, + explodedWarPath, + projectDependencies.getFiles().stream().map(File::getName).collect(Collectors.toSet())); + } + + SourceSet mainSourceSet = getMainSourceSet(); + FileCollection classesOutputDirectories = + mainSourceSet.getOutput().getClassesDirs().filter(File::exists); + Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); + FileCollection allFiles = + project.getConfigurations().getByName(configurationName).filter(File::exists); + + FileCollection nonProjectDependencies = + allFiles + .minus(classesOutputDirectories) + .minus(projectDependencies) + .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); + + FileCollection snapshotDependencies = + nonProjectDependencies.filter(file -> file.getName().contains("SNAPSHOT")); + FileCollection dependencies = nonProjectDependencies.minus(snapshotDependencies); + + // Adds dependency files + javaContainerBuilder + .addDependencies( + dependencies.getFiles().stream().map(File::toPath).collect(Collectors.toList())) + .addSnapshotDependencies( + snapshotDependencies.getFiles().stream() + .map(File::toPath) + .collect(Collectors.toList())) + .addProjectDependencies( + projectDependencies.getFiles().stream() + .map(File::toPath) + .collect(Collectors.toList())); + + switch (containerizingMode) { + case EXPLODED: + // Adds resource files + if (Files.exists(resourcesOutputDirectory)) { + javaContainerBuilder.addResources(resourcesOutputDirectory); + } + + // Adds class files + for (File classesOutputDirectory : classesOutputDirectories) { + javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); + } + if (classesOutputDirectories.isEmpty()) { + log(LogEvent.warn("No classes files were found - did you compile your project?")); + } + break; + + case PACKAGED: + // Add a JAR + Jar jarTask = (Jar) project.getTasks().findByName("jar"); + Path jarPath = jarTask.getArchiveFile().get().getAsFile().toPath(); + log(LogEvent.debug("Using JAR: " + jarPath)); + javaContainerBuilder.addToClasspath(jarPath); + break; + + default: + throw new IllegalStateException("unknown containerizing mode: " + containerizingMode); + } + + return javaContainerBuilder.toContainerBuilder(); + + } catch (IOException ex) { + throw new GradleException("Obtaining project build output files failed", ex); + } + } + + @Override + public List getClassFiles() throws IOException { + // TODO: Consolidate with createJibContainerBuilder + FileCollection classesOutputDirectories = + getMainSourceSet().getOutput().getClassesDirs().filter(File::exists); + List classFiles = new ArrayList<>(); + for (File classesOutputDirectory : classesOutputDirectories) { + classFiles.addAll(new DirectoryWalker(classesOutputDirectory.toPath()).walk()); + } + return classFiles; + } + + @Override + public List getDependencies() { + List dependencies = new ArrayList<>(); + FileCollection runtimeClasspath = project.getConfigurations().getByName(configurationName); + // To be on the safe side with the order, calling "forEach" first (no filtering operations). + runtimeClasspath.forEach( + file -> { + if (file.exists() + && file.isFile() + && file.getName().toLowerCase(Locale.US).endsWith(".jar")) { + dependencies.add(file.toPath()); + } + }); + return dependencies; + } + + @Override + public void waitForLoggingThread() { + singleThreadedExecutor.shutDownAndAwaitTermination(LOGGING_THREAD_SHUTDOWN_TIMEOUT); } @Override - public SourceFilesConfiguration getSourceFilesConfiguration() { - return sourceFilesConfiguration; + public void configureEventHandlers(Containerizer containerizer) { + containerizer + .addEventHandler(LogEvent.class, this::log) + .addEventHandler( + TimerEvent.class, new TimerEventHandler(message -> log(LogEvent.debug(message)))) + .addEventHandler( + ProgressEvent.class, + new ProgressEventHandler( + update -> { + List footer = + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedLeafTasks()); + footer.add(""); + consoleLogger.setFooter(footer); + })); } @Override - public HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix) { - return HelpfulSuggestionsProvider.get(prefix); + public void log(LogEvent logEvent) { + consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); } @Override - public BuildLogger getLogger() { - return gradleBuildLogger; + public String getToolName() { + return TOOL_NAME; + } + + @Override + public String getToolVersion() { + return TOOL_VERSION; } @Override @@ -89,16 +359,31 @@ public String getPluginName() { @Nullable @Override - public String getMainClassFromJar() { - List jarTasks = new ArrayList<>(project.getTasksByName("jar", false)); - if (jarTasks.size() != 1) { + public String getMainClassFromJarPlugin() { + Jar jarTask = (Jar) project.getTasks().findByName("jar"); + if (jarTask == null) { + return null; + } + + Object value = jarTask.getManifest().getAttributes().get("Main-Class"); + + if (value instanceof Provider) { + value = ((Provider) value).getOrNull(); + } + + if (value instanceof String) { + return (String) value; + } + + if (value == null) { return null; } - return (String) ((Jar) jarTasks.get(0)).getManifest().getAttributes().get("Main-Class"); + + return String.valueOf(value); } @Override - public Path getCacheDirectory() { + public Path getDefaultCacheDirectory() { return project.getBuildDir().toPath().resolve(CACHE_DIRECTORY_NAME); } @@ -107,16 +392,161 @@ public String getJarPluginName() { return JAR_PLUGIN_NAME; } + @Override + public boolean isWarProject() { + return project.getPlugins().hasPlugin(WarPlugin.class); + } + /** - * Tries to resolve the main class. + * Returns the input files for a task. These files include the gradle {@link + * org.gradle.api.artifacts.Configuration}, output directories (classes, resources, etc.) of the + * main {@link org.gradle.api.tasks.SourceSet}, and any extraDirectories defined by the user to + * include in the container. * - * @throws GradleException if resolving the main class fails. + * @param project the gradle project + * @param extraDirectories the image's configured extra directories + * @return the input files */ - String getMainClass(JibExtension jibExtension) { + @VisibleForTesting + static FileCollection getInputFiles( + Project project, List extraDirectories, String configurationName) { + List dependencyFileCollections = new ArrayList<>(); + dependencyFileCollections.add(project.getConfigurations().getByName(configurationName)); + // Output directories (classes and resources) from main SourceSet are added + // so that BuildTarTask picks up changes in these and do not skip task + SourceSetContainer sourceSetContainer = + project.getExtensions().getByType(SourceSetContainer.class); + SourceSet mainSourceSet = sourceSetContainer.getByName(MAIN_SOURCE_SET_NAME); + dependencyFileCollections.add(mainSourceSet.getOutput()); + + extraDirectories.stream() + .filter(Files::exists) + .map(Path::toFile) + .map(project::files) + .forEach(dependencyFileCollections::add); + + return project.files(dependencyFileCollections); + } + + @Override + public String getName() { + return project.getName(); + } + + @Override + public String getVersion() { + return project.getVersion().toString(); + } + + @Override + public int getMajorJavaVersion() { + JavaVersion version = JavaVersion.current(); + JavaPluginExtension javaPluginExtension = + project.getExtensions().findByType(JavaPluginExtension.class); + if (javaPluginExtension != null) { + version = javaPluginExtension.getTargetCompatibility(); + } + return Integer.valueOf(version.getMajorVersion()); + } + + @Override + public boolean isOffline() { + return project.getGradle().getStartParameter().isOffline(); + } + + @Override + public JibContainerBuilder runPluginExtensions( + List extensionConfigs, + JibContainerBuilder jibContainerBuilder) + throws JibPluginExtensionException { + if (extensionConfigs.isEmpty()) { + log(LogEvent.debug("No Jib plugin extensions configured to load")); + return jibContainerBuilder; + } + + List> loadedExtensions = extensionLoader.get(); + JibGradlePluginExtension extension = null; + ContainerBuildPlan buildPlan = jibContainerBuilder.toContainerBuildPlan(); try { - return MainClassFinder.resolveMainClass(jibExtension.getMainClass(), this); - } catch (MainClassInferenceException ex) { - throw new GradleException(ex.getMessage(), ex); + for (ExtensionConfiguration config : extensionConfigs) { + extension = findConfiguredExtension(loadedExtensions, config); + + log(LogEvent.lifecycle("Running extension: " + config.getExtensionClass())); + buildPlan = + runPluginExtension(extension.getExtraConfigType(), extension, config, buildPlan); + ImageReference.parse(buildPlan.getBaseImage()); // to validate image reference + } + return jibContainerBuilder.applyContainerBuildPlan(buildPlan); + + } catch (InvalidImageReferenceException ex) { + throw new JibPluginExtensionException( + Verify.verifyNotNull(extension).getClass(), + "invalid base image reference: " + buildPlan.getBaseImage(), + ex); + } + } + + // Unchecked casting: "getExtraConfiguration().get()" (Object) to Action and "extension" + // (JibGradlePluginExtension) to JibGradlePluginExtension where T is the extension-defined + // config type (as requested by "JibGradlePluginExtension.getExtraConfigType()"). + @SuppressWarnings({"unchecked"}) + private ContainerBuildPlan runPluginExtension( + Optional> extraConfigType, + JibGradlePluginExtension extension, + ExtensionConfiguration config, + ContainerBuildPlan buildPlan) + throws JibPluginExtensionException { + T extraConfig = null; + Optional configs = config.getExtraConfiguration(); + if (configs.isPresent()) { + if (!extraConfigType.isPresent()) { + throw new IllegalArgumentException( + "extension " + + extension.getClass().getSimpleName() + + " does not expect extension-specific configuration; remove the inapplicable " + + "'pluginExtension.configuration' from Gradle build script"); + } else { + // configs.get() is of type Action, so this cast always succeeds. + // (Note generic is erased at runtime.) + Action action = (Action) configs.get(); + extraConfig = project.getObjects().newInstance(extraConfigType.get(), project); + action.execute(extraConfig); + } + } + + try { + return ((JibGradlePluginExtension) extension) + .extendContainerBuildPlan( + buildPlan, + config.getProperties(), + Optional.ofNullable(extraConfig), + () -> project, + new PluginExtensionLogger(this::log)); + } catch (RuntimeException ex) { + throw new JibPluginExtensionException( + extension.getClass(), "extension crashed: " + ex.getMessage(), ex); + } + } + + private JibGradlePluginExtension findConfiguredExtension( + List> extensions, ExtensionConfiguration config) + throws JibPluginExtensionException { + Predicate> matchesClassName = + extension -> extension.getClass().getName().equals(config.getExtensionClass()); + Optional> found = + extensions.stream().filter(matchesClassName).findFirst(); + if (!found.isPresent()) { + throw new JibPluginExtensionException( + NullExtension.class, + "extension configured but not discovered on Jib runtime classpath: " + + config.getExtensionClass()); } + return found.get(); + } + + private SourceSet getMainSourceSet() { + SourceSetContainer sourceSetContainer = + project.getExtensions().getByType(SourceSetContainer.class); + return sourceSetContainer.getByName(MAIN_SOURCE_SET_NAME); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java new file mode 100644 index 0000000000..10195cc756 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -0,0 +1,225 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Gradle-specific adapter for providing raw configuration parameter values. */ +public class GradleRawConfiguration implements RawConfiguration { + + private final JibExtension jibExtension; + + public GradleRawConfiguration(JibExtension jibExtension) { + this.jibExtension = jibExtension; + } + + @Override + public Optional getFromImage() { + return Optional.ofNullable(jibExtension.getFrom().getImage()); + } + + @Override + public AuthProperty getFromAuth() { + return jibExtension.getFrom().getAuth(); + } + + @Override + public CredHelperConfiguration getFromCredHelper() { + return jibExtension.getFrom().getCredHelper(); + } + + @Override + public Optional getToImage() { + return Optional.ofNullable(jibExtension.getTo().getImage()); + } + + @Override + public AuthProperty getToAuth() { + return jibExtension.getTo().getAuth(); + } + + @Override + public CredHelperConfiguration getToCredHelper() { + return jibExtension.getTo().getCredHelper(); + } + + @Override + public Set getToTags() { + return jibExtension.getTo().getTags(); + } + + @Override + public Optional> getEntrypoint() { + return Optional.ofNullable(jibExtension.getContainer().getEntrypoint()); + } + + @Override + public Optional> getProgramArguments() { + return Optional.ofNullable(jibExtension.getContainer().getArgs()); + } + + @Override + public List getExtraClasspath() { + return jibExtension.getContainer().getExtraClasspath(); + } + + @Override + public boolean getExpandClasspathDependencies() { + return jibExtension.getContainer().getExpandClasspathDependencies(); + } + + @Override + public Optional getMainClass() { + return Optional.ofNullable(jibExtension.getContainer().getMainClass()); + } + + @Override + public List getJvmFlags() { + return jibExtension.getContainer().getJvmFlags(); + } + + @Override + public String getAppRoot() { + return jibExtension.getContainer().getAppRoot(); + } + + @Override + public Map getEnvironment() { + return jibExtension.getContainer().getEnvironment(); + } + + @Override + public Map getLabels() { + return jibExtension.getContainer().getLabels().get(); + } + + @Override + public List getVolumes() { + return jibExtension.getContainer().getVolumes(); + } + + @Override + public List getPorts() { + return jibExtension.getContainer().getPorts(); + } + + @Override + public Optional getUser() { + return Optional.ofNullable(jibExtension.getContainer().getUser()); + } + + @Override + public Optional getWorkingDirectory() { + return Optional.ofNullable(jibExtension.getContainer().getWorkingDirectory()); + } + + @Override + public boolean getAllowInsecureRegistries() { + return jibExtension.getAllowInsecureRegistries(); + } + + @Override + public ImageFormat getImageFormat() { + return jibExtension.getContainer().getFormat(); + } + + @Override + public Optional getProperty(String propertyName) { + return Optional.ofNullable(System.getProperty(propertyName)); + } + + @Override + public String getFilesModificationTime() { + return jibExtension.getContainer().getFilesModificationTime().get(); + } + + @Override + public String getCreationTime() { + return jibExtension.getContainer().getCreationTime().get(); + } + + @Override + public List getExtraDirectories() { + for (ExtraDirectoryParameters path : jibExtension.getExtraDirectories().getPaths()) { + if (path.getFrom().equals(Paths.get(""))) { + throw new IllegalArgumentException( + "Incomplete extraDirectories.paths configuration; source directory must be set"); + } + } + return jibExtension.getExtraDirectories().getPaths(); + } + + @Override + public Map getExtraDirectoryPermissions() { + return TaskCommon.convertPermissionsMap( + jibExtension.getExtraDirectories().getPermissions().get()); + } + + @Override + public Optional getDockerExecutable() { + return Optional.ofNullable(jibExtension.getDockerClient().getExecutablePath()); + } + + @Override + public Map getDockerEnvironment() { + return jibExtension.getDockerClient().getEnvironment(); + } + + @Override + public String getContainerizingMode() { + return jibExtension.getContainerizingMode(); + } + + @Override + public Path getTarOutputPath() { + return jibExtension.getOutputPaths().getTarPath(); + } + + @Override + public Path getDigestOutputPath() { + return jibExtension.getOutputPaths().getDigestPath(); + } + + @Override + public Path getImageIdOutputPath() { + return jibExtension.getOutputPaths().getImageIdPath(); + } + + @Override + public Path getImageJsonOutputPath() { + return jibExtension.getOutputPaths().getImageJsonPath(); + } + + @Override + public List getPluginExtensions() { + return jibExtension.getPluginExtensions().get(); + } + + @Override + public List getPlatforms() { + return jibExtension.getFrom().getPlatforms().get(); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java deleted file mode 100644 index 1f22997656..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2018 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.common.collect.ImmutableList; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.SourceSet; - -/** {@link SourceFilesConfiguration} implementation based on inputs from a {@link Project}. */ -class GradleSourceFilesConfiguration implements SourceFilesConfiguration { - - /** Name of the `main` {@link SourceSet} to use as source files. */ - private static final String MAIN_SOURCE_SET_NAME = "main"; - - /** Resolves the source files configuration for a Gradle {@link Project}. */ - static GradleSourceFilesConfiguration getForProject( - Project project, GradleBuildLogger gradleBuildLogger) throws IOException { - return new GradleSourceFilesConfiguration(project, gradleBuildLogger); - } - - private final ImmutableList dependenciesFiles; - private final ImmutableList resourcesFiles; - private final ImmutableList classesFiles; - - /** Instantiate with {@link #getForProject}. */ - private GradleSourceFilesConfiguration(Project project, GradleBuildLogger gradleBuildLogger) - throws IOException { - JavaPluginConvention javaPluginConvention = - project.getConvention().getPlugin(JavaPluginConvention.class); - - SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); - - List dependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); - List classesFiles = new ArrayList<>(); - - // Adds each file in each classes output directory to the classes files list. - FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); - for (File classesOutputDirectory : classesOutputDirectories) { - if (Files.notExists(classesOutputDirectory.toPath())) { - // Warns that output directory was not found. - gradleBuildLogger.warn( - "Could not find build output directory '" + classesOutputDirectory + "'"); - continue; - } - try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { - classFileStream.forEach(classesFiles::add); - } - } - if (classesFiles.isEmpty()) { - gradleBuildLogger.warn("No classes files were found - did you compile your project?"); - } - - // Adds each file in the resources output directory to the resources files list. - Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); - if (Files.exists(resourcesOutputDirectory)) { - try (Stream resourceFileStream = Files.list(resourcesOutputDirectory)) { - resourceFileStream.forEach(resourcesFiles::add); - } - } - - // Adds all other files to the dependencies files list. - FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); - // Removes the classes output directories. - allFiles = allFiles.minus(classesOutputDirectories); - for (File dependencyFile : allFiles) { - // Removes the resources output directory. - if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { - continue; - } - dependenciesFiles.add(dependencyFile.toPath()); - } - - // Sorts all files by path for consistent ordering. - this.dependenciesFiles = ImmutableList.sortedCopyOf(dependenciesFiles); - this.resourcesFiles = ImmutableList.sortedCopyOf(resourcesFiles); - this.classesFiles = ImmutableList.sortedCopyOf(classesFiles); - } - - @Override - public ImmutableList getDependenciesFiles() { - return dependenciesFiles; - } - - @Override - public ImmutableList getResourcesFiles() { - return resourcesFiles; - } - - @Override - public ImmutableList getClassesFiles() { - return classesFiles; - } - - @Override - public String getDependenciesPathOnImage() { - return DEFAULT_DEPENDENCIES_PATH_ON_IMAGE; - } - - @Override - public String getResourcesPathOnImage() { - return DEFAULT_RESOURCES_PATH_ON_IMAGE; - } - - @Override - public String getClassesPathOnImage() { - return DEFAULT_CLASSES_PATH_ON_IMAGE; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java deleted file mode 100644 index 9bd8d5893a..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; - -/** Provider for Maven-specific {@link HelpfulSuggestions}. */ -class HelpfulSuggestionsProvider { - - /** - * @param messagePrefix the prefix - * @return a new {@link HelpfulSuggestions} with the specified message prefix - */ - static HelpfulSuggestions get(String messagePrefix) { - return new HelpfulSuggestions( - messagePrefix, - "gradle clean", - "from.credHelper", - ignored -> "from.auth", - "to.credHelper", - ignored -> "to.auth"); - } - - private HelpfulSuggestionsProvider() {} -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java deleted file mode 100644 index 4170306afc..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import javax.annotation.Nullable; -import javax.inject.Inject; -import org.gradle.api.Action; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Internal; -import org.gradle.api.tasks.Nested; -import org.gradle.api.tasks.Optional; - -/** - * A bean that configures an image to be used in the build steps. This is configurable with Groovy - * closures and can be validated when used as a task input. - * - *

    {@code image} (required) is the image reference and {@code credHelper} (optional) is the name - * (after {@code docker-credential} of the credential helper for accessing the {@code image}. - */ -public class ImageConfiguration { - - private AuthConfiguration auth; - - @Nullable private String image; - @Nullable private String credHelper; - - @Inject - public ImageConfiguration(ObjectFactory objectFactory) { - auth = objectFactory.newInstance(AuthConfiguration.class); - } - - @Input - @Nullable - @Optional - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - @Input - @Nullable - @Optional - public String getCredHelper() { - return credHelper; - } - - public void setCredHelper(String credHelper) { - this.credHelper = credHelper; - } - - @Nested - @Optional - AuthConfiguration getAuth() { - return auth; - } - - public void auth(Action action) { - action.execute(auth); - } - - /** Converts the {@link ImageConfiguration} to an {@link Authorization}. */ - @Internal - @Nullable - Authorization getImageAuthorization() { - if (auth.getUsername() == null || auth.getPassword() == null) { - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 7ba35097f7..fbcea4c006 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,23 +16,15 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import java.io.File; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; +import com.google.cloud.tools.jib.gradle.skaffold.SkaffoldParameters; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; @@ -44,19 +36,46 @@ *

    {@code
      * jib {
      *   from {
    - *     image = ‘gcr.io/my-gcp-project/my-base-image’
    - *     credHelper = ‘gcr’
    + *     image = 'gcr.io/my-gcp-project/my-base-image'
    + *     credHelper = 'gcr'
    + *     platforms {
    + *       platform {
    + *         os = 'linux'
    + *         architecture = 'amd64'
    + *       }
    + *     }
      *   }
      *   to {
    - *     image = ‘gcr.io/gcp-project/my-app:built-with-jib’
    - *     credHelper = ‘ecr-login’
    + *     image = 'gcr.io/gcp-project/my-app:built-with-jib'
    + *     credHelper = 'ecr-login'
      *   }
      *   container {
    - *     jvmFlags = [‘-Xms512m’, ‘-Xdebug’]
    - *     mainClass = ‘com.mycompany.myproject.Main’
    + *     jvmFlags = ['-Xms512m', '-Xdebug']
    + *     mainClass = 'com.mycompany.myproject.Main'
      *     args = ['arg1', 'arg2']
    - *     exposedPorts = ['1000', '2000-2010', '3000']
    + *     ports = ['1000', '2000-2010', '3000']
      *     format = OCI
    + *     appRoot = '/app'
    + *   }
    + *   extraDirectories {
    + *     paths = ['/path/to/extra/dir', 'can/be/relative/to/project/root']
    + *     permissions = [
    + *       '/path/on/container/file1': 744,
    + *       '/path/on/container/file2': 123
    + *     ]
    + *   }
    + *   outputPaths {
    + *     tar = file('reative/to/project/root/jib-image.tar')
    + *     digest = file('/absolute/path/jib-image.digest')
    + *     imageId = file("$buildDir/jib-image.id")
    + *   }
    + *   allowInsecureRegistries = false
    + *   containerizingMode = 'exploded'
    + *   pluginExtensions {
    + *     pluginExtension {
    + *       implementation = 'com.example.ThirdPartyJibGradleExtension'
    + *       properties = [customKey: 'value]
    + *     }
      *   }
      * }
      * }
    @@ -64,96 +83,56 @@ public class JibExtension { // Defines default configuration values. - private static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; - private static final boolean DEFAULT_USE_ONLY_PROJECT_CACHE = false; private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; + private static final String DEFAULT_CONTAINERIZING_MODE = "exploded"; - private static Path resolveDefaultExtraDirectory(Path projectDirectory) { - return projectDirectory.resolve("src").resolve("main").resolve("jib"); - } - - private final ImageConfiguration from; - private final ImageConfiguration to; + private final BaseImageParameters from; + private final TargetImageParameters to; private final ContainerParameters container; - private final Property useOnlyProjectCache; + private final ExtraDirectoriesParameters extraDirectories; + private final DockerClientParameters dockerClient; + private final OutputPathsParameters outputPaths; + private final SkaffoldParameters skaffold; private final Property allowInsecureRegistries; - private final Property extraDirectory; - - // TODO: Deprecated parameters; remove these 4 - private final ListProperty jvmFlags; - private final Property mainClass; - private final ListProperty args; - private final Property format; - - public JibExtension(Project project) { - ObjectFactory objectFactory = project.getObjects(); - - from = objectFactory.newInstance(ImageConfiguration.class); - to = objectFactory.newInstance(ImageConfiguration.class); - container = objectFactory.newInstance(ContainerParameters.class); - - jvmFlags = objectFactory.listProperty(String.class); - mainClass = objectFactory.property(String.class); - args = objectFactory.listProperty(String.class); - format = objectFactory.property(ImageFormat.class); - - useOnlyProjectCache = objectFactory.property(Boolean.class); - allowInsecureRegistries = objectFactory.property(Boolean.class); - extraDirectory = objectFactory.property(File.class); - - // Sets defaults. - from.setImage(DEFAULT_FROM_IMAGE); - jvmFlags.set(Collections.emptyList()); - args.set(Collections.emptyList()); - useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); - allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); - extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath()).toFile()); - } + private final Property containerizingMode; + private final Property configurationName; + private final ListProperty pluginExtensions; + private final ExtensionParametersSpec extensionParametersSpec; /** - * Warns about deprecated parameters in use. + * Should be called using {@link org.gradle.api.plugins.ExtensionContainer#create}. * - * @param logger The logger used to print the warnings + * @param project the injected gradle project */ - void handleDeprecatedParameters(BuildLogger logger) { - StringBuilder deprecatedParams = new StringBuilder(); - if (!jvmFlags.get().isEmpty()) { - deprecatedParams.append(" jvmFlags -> container.jvmFlags\n"); - if (container.getJvmFlags().isEmpty()) { - container.setJvmFlags(jvmFlags.get()); - } - } - if (!Strings.isNullOrEmpty(mainClass.getOrNull())) { - deprecatedParams.append(" mainClass -> container.mainClass\n"); - if (Strings.isNullOrEmpty(container.getMainClass())) { - container.setMainClass(mainClass.getOrNull()); - } - } - if (!args.get().isEmpty()) { - deprecatedParams.append(" args -> container.args\n"); - if (container.getArgs().isEmpty()) { - container.setArgs(args.get()); - } - } - if (format.getOrNull() != null) { - deprecatedParams.append(" format -> container.format\n"); - container.setFormat(format.get()); - } - - if (deprecatedParams.length() > 0) { - logger.warn( - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your build.gradle to avoid issues in the future:\n" - + deprecatedParams - + "You may also wrap the parameters in a container{} block."); - } - } + public JibExtension(Project project) { + ObjectFactory objectFactory = project.getObjects(); - public void from(Action action) { + from = objectFactory.newInstance(BaseImageParameters.class); + to = objectFactory.newInstance(TargetImageParameters.class); + container = objectFactory.newInstance(ContainerParameters.class); + extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project); + dockerClient = objectFactory.newInstance(DockerClientParameters.class); + outputPaths = objectFactory.newInstance(OutputPathsParameters.class, project); + skaffold = objectFactory.newInstance(SkaffoldParameters.class, project); + + pluginExtensions = objectFactory.listProperty(ExtensionParameters.class).empty(); + extensionParametersSpec = + objectFactory.newInstance(ExtensionParametersSpec.class, pluginExtensions); + allowInsecureRegistries = + objectFactory.property(Boolean.class).convention(DEFAULT_ALLOW_INSECURE_REGISTIRIES); + containerizingMode = + objectFactory.property(String.class).convention(DEFAULT_CONTAINERIZING_MODE); + configurationName = + objectFactory + .property(String.class) + .convention(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + } + + public void from(Action action) { action.execute(from); } - public void to(Action action) { + public void to(Action action) { action.execute(to); } @@ -161,54 +140,43 @@ public void container(Action action) { action.execute(container); } - public void setJvmFlags(List jvmFlags) { - this.jvmFlags.set(jvmFlags); + public void extraDirectories(Action action) { + action.execute(extraDirectories); } - public void setMainClass(String mainClass) { - this.mainClass.set(mainClass); + public void dockerClient(Action action) { + action.execute(dockerClient); } - public void setArgs(List args) { - this.args.set(args); + public void outputPaths(Action action) { + action.execute(outputPaths); } - public void setFormat(ImageFormat format) { - this.format.set(format); + public void skaffold(Action action) { + action.execute(skaffold); } - public void setUseOnlyProjectCache(boolean useOnlyProjectCache) { - this.useOnlyProjectCache.set(useOnlyProjectCache); + public void pluginExtensions(Action action) { + action.execute(extensionParametersSpec); } public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } - public void setExtraDirectory(File extraDirectory) { - this.extraDirectory.set(extraDirectory); - } - - @Internal - String getBaseImage() { - return Preconditions.checkNotNull(from.getImage()); - } - - @Internal - @Nullable - String getTargetImage() { - return to.getImage(); + public void setContainerizingMode(String containerizingMode) { + this.containerizingMode.set(containerizingMode); } @Nested @Optional - public ImageConfiguration getFrom() { + public BaseImageParameters getFrom() { return from; } @Nested @Optional - public ImageConfiguration getTo() { + public TargetImageParameters getTo() { return to; } @@ -218,56 +186,64 @@ public ContainerParameters getContainer() { return container; } - // TODO: Make @Internal (deprecated) - @Input + @Nested @Optional - List getJvmFlags() { - return container.getJvmFlags(); + public ExtraDirectoriesParameters getExtraDirectories() { + return extraDirectories; } - // TODO: Make @Internal (deprecated) - @Input - @Nullable + @Nested @Optional - String getMainClass() { - return container.getMainClass(); + public DockerClientParameters getDockerClient() { + return dockerClient; } - // TODO: Make @Internal (deprecated) - @Input + @Nested @Optional - List getArgs() { - return container.getArgs(); + public OutputPathsParameters getOutputPaths() { + return outputPaths; } - // TODO: Make @Internal (deprecated) - @Input + @Nested @Optional - Class getFormat() { - return container.getFormat(); + public SkaffoldParameters getSkaffold() { + return skaffold; } - @Internal - @Optional - List getExposedPorts() { - return container.getPorts(); + @Input + boolean getAllowInsecureRegistries() { + if (System.getProperty(PropertyNames.ALLOW_INSECURE_REGISTRIES) != null) { + return Boolean.getBoolean(PropertyNames.ALLOW_INSECURE_REGISTRIES); + } + return allowInsecureRegistries.get(); } @Input @Optional - boolean getUseOnlyProjectCache() { - return useOnlyProjectCache.get(); + public String getContainerizingMode() { + String property = System.getProperty(PropertyNames.CONTAINERIZING_MODE); + return property != null ? property : containerizingMode.get(); } + /** + * Returns the configurationName property while setting it to the value of the system property if + * present. + * + * @return The configurationName property + */ @Input @Optional - boolean getAllowInsecureRegistries() { - return allowInsecureRegistries.get(); + public Property getConfigurationName() { + String property = System.getProperty(PropertyNames.CONFIGURATION_NAME); + if (property != null && !property.equals(configurationName.get())) { + configurationName.set(property); + } + return configurationName; } - @Input - File getExtraDirectory() { - // TODO: Should inform user about nonexistent directory if using custom directory. - return extraDirectory.get(); + @Nested + @Optional + public ListProperty getPluginExtensions() { + return pluginExtensions; } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index d98cabf990..6f2d9d16d1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,65 +16,42 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.gradle.skaffold.CheckJibVersionTask; +import com.google.cloud.tools.jib.gradle.skaffold.FilesTaskV2; +import com.google.cloud.tools.jib.gradle.skaffold.InitTask; +import com.google.cloud.tools.jib.gradle.skaffold.SyncMapTask; +import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.UnknownTaskException; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { - @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("4.6"); + @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("5.1"); - @VisibleForTesting static final String JIB_EXTENSION_NAME = "jib"; - @VisibleForTesting static final String BUILD_IMAGE_TASK_NAME = "jib"; - @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; - @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; + public static final String JIB_EXTENSION_NAME = "jib"; + public static final String BUILD_IMAGE_TASK_NAME = "jib"; + public static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; + public static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; + public static final String SKAFFOLD_FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; + public static final String SKAFFOLD_INIT_TASK_NAME = "_jibSkaffoldInit"; + public static final String SKAFFOLD_SYNC_MAP_TASK_NAME = "_jibSkaffoldSyncMap"; + public static final String SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME = + "_skaffoldFailIfJibOutOfDate"; - @Override - public void apply(Project project) { - checkGradleVersion(); - - JibExtension jibExtension = - project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); - - Task buildImageTask = - project - .getTasks() - .create(BUILD_IMAGE_TASK_NAME, BuildImageTask.class) - .setJibExtension(jibExtension); - Task dockerContextTask = - project - .getTasks() - .create(DOCKER_CONTEXT_TASK_NAME, DockerContextTask.class) - .setJibExtension(jibExtension); - Task buildDockerTask = - project - .getTasks() - .create(BUILD_DOCKER_TASK_NAME, BuildDockerTask.class) - .setJibExtension(jibExtension); - - // Has all tasks depend on the 'classes' task. - project.afterEvaluate( - projectAfterEvaluation -> { - try { - Task classesTask = projectAfterEvaluation.getTasks().getByPath("classes"); - - buildImageTask.dependsOn(classesTask); - dockerContextTask.dependsOn(classesTask); - buildDockerTask.dependsOn(classesTask); - - } catch (UnknownTaskException ex) { - throw new GradleException( - "Could not find task 'classes' on project " - + projectAfterEvaluation.getDisplayName() - + " - perhaps you did not apply the 'java' plugin?", - ex); - } - }); - } + public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; private static void checkGradleVersion() { if (GRADLE_MIN_VERSION.compareTo(GradleVersion.current()) > 0) { @@ -88,4 +65,139 @@ private static void checkGradleVersion() { + "'."); } } + + /** Check the Jib version matches the required version (if specified). */ + private static void checkJibVersion() { + // todo: should retrieve from project properties? + String requiredVersion = System.getProperty(REQUIRED_VERSION_PROPERTY_NAME); + if (requiredVersion == null) { + return; + } + String actualVersion = GradleProjectProperties.TOOL_VERSION; + if (actualVersion == null) { + throw new GradleException("Could not determine Jib plugin version"); + } + VersionChecker checker = new VersionChecker<>(GradleVersion::version); + if (!checker.compatibleVersion(requiredVersion, actualVersion)) { + String failure = + String.format( + "Jib plugin version is %s but is required to be %s", actualVersion, requiredVersion); + throw new GradleException(failure); + } + } + + @Override + public void apply(Project project) { + checkGradleVersion(); + checkJibVersion(); + + JibExtension jibExtension = + project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); + + TaskContainer tasks = project.getTasks(); + TaskProvider buildImageTask = + tasks.register( + BUILD_IMAGE_TASK_NAME, + BuildImageTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a registry."); + task.setJibExtension(jibExtension); + }); + + TaskProvider buildDockerTask = + tasks.register( + BUILD_DOCKER_TASK_NAME, + BuildDockerTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a Docker daemon."); + task.setJibExtension(jibExtension); + }); + + TaskProvider buildTarTask = + tasks.register( + BUILD_TAR_TASK_NAME, + BuildTarTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a tarball."); + task.setJibExtension(jibExtension); + }); + + tasks + .register(SKAFFOLD_FILES_TASK_V2_NAME, FilesTaskV2.class) + .configure(task -> task.setJibExtension(jibExtension)); + tasks + .register(SKAFFOLD_INIT_TASK_NAME, InitTask.class) + .configure(task -> task.setJibExtension(jibExtension)); + TaskProvider syncMapTask = + tasks.register( + SKAFFOLD_SYNC_MAP_TASK_NAME, + SyncMapTask.class, + task -> task.setJibExtension(jibExtension)); + + // A check to catch older versions of Jib. This can be removed once we are certain people + // are using Jib 1.3.1 or later. + tasks.register(SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); + + project.afterEvaluate( + projectAfterEvaluation -> { + TaskProvider warTask = TaskCommon.getWarTaskProvider(projectAfterEvaluation); + TaskProvider bootWarTask = + TaskCommon.getBootWarTaskProvider(projectAfterEvaluation); + List jibDependencies = new ArrayList<>(); + if (warTask != null || bootWarTask != null) { + // Have all tasks depend on the 'war' and/or 'bootWar' task. + if (warTask != null) { + jibDependencies.add(warTask); + } + if (bootWarTask != null) { + jibDependencies.add(bootWarTask); + } + } else if ("packaged".equals(jibExtension.getContainerizingMode())) { + // Have all tasks depend on the 'jar' task. + TaskProvider jarTask = projectAfterEvaluation.getTasks().named("jar"); + jibDependencies.add(jarTask); + + if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { + Task bootJarTask = projectAfterEvaluation.getTasks().getByName("bootJar"); + + if (bootJarTask.getEnabled()) { + String bootJarPath = bootJarTask.getOutputs().getFiles().getAsPath(); + String jarPath = jarTask.get().getOutputs().getFiles().getAsPath(); + if (bootJarPath.equals(jarPath)) { + if (!jarTask.get().getEnabled()) { + ((Jar) jarTask.get()).getArchiveClassifier().set("original"); + } else { + throw new GradleException( + "Both 'bootJar' and 'jar' tasks are enabled, but they write their jar file " + + "into the same location at " + + jarPath + + ". Did you forget to set 'archiveClassifier' on either task?"); + } + } + } + jarTask.get().setEnabled(true); + } + } + + SourceSet mainSourceSet = + projectAfterEvaluation + .getExtensions() + .getByType(SourceSetContainer.class) + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + jibDependencies.add(mainSourceSet.getRuntimeClasspath()); + jibDependencies.add( + projectAfterEvaluation + .getConfigurations() + .getByName(jibExtension.getConfigurationName().get())); + + Set> jibTaskProviders = + ImmutableSet.of(buildImageTask, buildDockerTask, buildTarTask, syncMapTask); + jibTaskProviders.forEach( + provider -> + provider.configure(task -> jibDependencies.forEach(dep -> task.dependsOn(dep)))); + }); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java similarity index 58% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java index 368e0a37e4..e742988c7f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,22 +14,18 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib.gradle; -public interface BuildLogger { +import org.gradle.api.Task; - void error(CharSequence message); +/** A task with a {@link JibExtension}. */ +public interface JibTask extends Task { /** - * Logs messages as part of normal execution (default log level). + * Sets the task's {@link JibExtension}. * - * @param message the message to log + * @param jibExtension the {@link JibExtension} + * @return this */ - void lifecycle(CharSequence message); - - void warn(CharSequence message); - - void info(CharSequence message); - - void debug(CharSequence message); + Task setJibExtension(JibExtension jibExtension); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java new file mode 100644 index 0000000000..46b4b6e608 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Object that configures where Jib should create its build output files. */ +public class OutputPathsParameters { + + private final Project project; + + private Path digest; + private Path tar; + private Path imageId; + private Path imageJson; + + @Inject + public OutputPathsParameters(Project project) { + this.project = project; + digest = project.getBuildDir().toPath().resolve("jib-image.digest"); + imageId = project.getBuildDir().toPath().resolve("jib-image.id"); + imageJson = project.getBuildDir().toPath().resolve("jib-image.json"); + tar = project.getBuildDir().toPath().resolve("jib-image.tar"); + } + + @Input + public String getDigest() { + return getRelativeToProjectRoot(digest, PropertyNames.OUTPUT_PATHS_DIGEST).toString(); + } + + @Internal + Path getDigestPath() { + return getRelativeToProjectRoot(digest, PropertyNames.OUTPUT_PATHS_DIGEST); + } + + public void setDigest(String digest) { + this.digest = Paths.get(digest); + } + + @Input + public String getImageId() { + return getRelativeToProjectRoot(imageId, PropertyNames.OUTPUT_PATHS_IMAGE_ID).toString(); + } + + @Internal + Path getImageIdPath() { + return getRelativeToProjectRoot(imageId, PropertyNames.OUTPUT_PATHS_IMAGE_ID); + } + + public void setImageId(String id) { + this.imageId = Paths.get(id); + } + + @Input + public String getImageJson() { + return getRelativeToProjectRoot(imageJson, PropertyNames.OUTPUT_PATHS_IMAGE_JSON).toString(); + } + + @Internal + Path getImageJsonPath() { + return getRelativeToProjectRoot(imageJson, PropertyNames.OUTPUT_PATHS_IMAGE_JSON); + } + + public void setImageJson(String imageJson) { + this.imageJson = Paths.get(imageJson); + } + + @Input + public String getTar() { + return getRelativeToProjectRoot(tar, PropertyNames.OUTPUT_PATHS_TAR).toString(); + } + + @Internal + Path getTarPath() { + return getRelativeToProjectRoot(tar, PropertyNames.OUTPUT_PATHS_TAR); + } + + public void setTar(String tar) { + this.tar = Paths.get(tar); + } + + private Path getRelativeToProjectRoot(Path configuration, String propertyName) { + String property = System.getProperty(propertyName); + Path path = property != null ? Paths.get(property) : configuration; + return path.isAbsolute() ? path : project.getProjectDir().toPath().resolve(path); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParameters.java new file mode 100644 index 0000000000..7b65978e59 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParameters.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.PlatformConfiguration; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Configuration of a platform. */ +public class PlatformParameters implements PlatformConfiguration { + + static PlatformParameters of(String osArchitecture) { + Matcher matcher = Pattern.compile("([^/ ]+)/([^/ ]+)").matcher(osArchitecture); + if (!matcher.matches()) { + throw new IllegalArgumentException("Platform must be of form os/architecture."); + } + PlatformParameters platformParameters = new PlatformParameters(); + platformParameters.os = matcher.group(1); + platformParameters.architecture = matcher.group(2); + return platformParameters; + } + + @Nullable private String os; + @Nullable private String architecture; + + @Input + @Nullable + public String getOs() { + return os; + } + + @Internal + @Override + public Optional getOsName() { + return Optional.ofNullable(os); + } + + public void setOs(String os) { + this.os = os; + } + + @Input + @Nullable + public String getArchitecture() { + return architecture; + } + + @Internal + @Override + public Optional getArchitectureName() { + return Optional.ofNullable(architecture); + } + + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PlatformParameters)) { + return false; + } + PlatformParameters otherPlatform = (PlatformParameters) other; + return Objects.equals(architecture, otherPlatform.getArchitecture()) + && Objects.equals(os, otherPlatform.getOs()); + } + + @Override + public int hashCode() { + return Objects.hash(architecture, os); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParametersSpec.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParametersSpec.java new file mode 100644 index 0000000000..a6de7c1901 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParametersSpec.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; + +/** Allows to add {@link PlatformParameters} objects to the list property of the same type. */ +public class PlatformParametersSpec { + + private final ObjectFactory objectFactory; + private final ListProperty platforms; + + @Inject + public PlatformParametersSpec( + ObjectFactory objectFactory, ListProperty platforms) { + this.objectFactory = objectFactory; + this.platforms = platforms; + } + + /** + * Adds a new platform configuration to the platforms list. + * + * @param action closure representing a platform configuration + */ + public void platform(Action action) { + PlatformParameters platform = objectFactory.newInstance(PlatformParameters.class); + action.execute(platform); + platforms.add(platform); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java new file mode 100644 index 0000000000..684f7f4b21 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; + +/** Object in {@link JibExtension} that configures the target image. */ +public class TargetImageParameters { + + private final AuthParameters auth; + private final Property image; + private final SetProperty tags; + private final CredHelperParameters credHelper; + + @Inject + public TargetImageParameters(ObjectFactory objectFactory) { + auth = objectFactory.newInstance(AuthParameters.class, "to.auth"); + image = objectFactory.property(String.class); + tags = objectFactory.setProperty(String.class).empty(); + credHelper = + objectFactory.newInstance(CredHelperParameters.class, PropertyNames.TO_CRED_HELPER); + } + + @Input + @Nullable + @Optional + public String getImage() { + if (System.getProperty(PropertyNames.TO_IMAGE) != null) { + return System.getProperty(PropertyNames.TO_IMAGE); + } + return image.getOrNull(); + } + + public void setImage(String image) { + this.image.set(image); + } + + public void setImage(Provider image) { + this.image.set(image); + } + + @Input + @Optional + public Set getTags() { + String property = System.getProperty(PropertyNames.TO_TAGS); + Set tagsValue; + if (property != null) { + tagsValue = ImmutableSet.copyOf(ConfigurationPropertyValidator.parseListProperty(property)); + } else { + try { + tagsValue = tags.get(); + } catch (NullPointerException ex) { + throw new IllegalArgumentException("jib.to.tags contains null tag"); + } + } + if (tagsValue.stream().anyMatch(str -> str.isEmpty())) { + throw new IllegalArgumentException("jib.to.tags contains empty tag"); + } + return tagsValue; + } + + public void setTags(List tags) { + this.tags.set(tags); + } + + public void setTags(Set tags) { + this.tags.set(tags); + } + + public void setTags(Provider> tags) { + this.tags.set(tags); + } + + @Nested + @Optional + public CredHelperParameters getCredHelper() { + return credHelper; + } + + public void setCredHelper(String helper) { + this.credHelper.setHelper(helper); + } + + public void credHelper(Action action) { + action.execute(credHelper); + } + + @Nested + @Optional + public AuthParameters getAuth() { + // System properties are handled in ConfigurationPropertyValidator + return auth; + } + + public void auth(Action action) { + action.execute(auth); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java new file mode 100644 index 0000000000..ca7fc761eb --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -0,0 +1,151 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.UpdateChecker; +import com.google.cloud.tools.jib.plugins.common.globalconfig.GlobalConfig; +import com.google.common.util.concurrent.Futures; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import javax.annotation.Nullable; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.UnknownTaskException; +import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.internal.logging.events.OutputEventListener; +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; +import org.slf4j.LoggerFactory; + +/** Collection of common methods to share between Gradle tasks. */ +class TaskCommon { + + public static final String VERSION_URL = "https://storage.googleapis.com/jib-versions/jib-gradle"; + + static Future> newUpdateChecker( + ProjectProperties projectProperties, GlobalConfig globalConfig, Logger logger) { + if (projectProperties.isOffline() + || !logger.isLifecycleEnabled() + || globalConfig.isDisableUpdateCheck()) { + return Futures.immediateFuture(Optional.empty()); + } + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + return UpdateChecker.checkForUpdate( + executorService, + VERSION_URL, + projectProperties.getToolName(), + projectProperties.getToolVersion(), + projectProperties::log); + } finally { + executorService.shutdown(); + } + } + + static void finishUpdateChecker( + ProjectProperties projectProperties, Future> updateCheckFuture) { + UpdateChecker.finishUpdateCheck(updateCheckFuture) + .ifPresent( + latestVersion -> { + String changelogUrl = + ProjectInfo.GITHUB_URL + "/blob/master/jib-gradle-plugin/CHANGELOG.md"; + String privacyUrl = ProjectInfo.GITHUB_URL + "/blob/master/docs/privacy.md"; + String message = + String.format( + "\n\u001B[33mA new version of %s (%s) is available (currently using %s). Update your" + + " build configuration to use the latest features and fixes!\n%s\u001B[0m\n\nPlease see" + + " %s for info on disabling this update check.\n", + projectProperties.getToolName(), + latestVersion, + projectProperties.getToolVersion(), + changelogUrl, + privacyUrl); + projectProperties.log(LogEvent.lifecycle(message)); + }); + } + + @Nullable + static TaskProvider getWarTaskProvider(Project project) { + if (project.getPlugins().hasPlugin(WarPlugin.class)) { + return project.getTasks().named(WarPlugin.WAR_TASK_NAME); + } + return null; + } + + @Nullable + static TaskProvider getBootWarTaskProvider(Project project) { + if (project.getPlugins().hasPlugin("org.springframework.boot")) { + try { + return project.getTasks().named("bootWar"); + } catch (UnknownTaskException ignored) { // fall through + } + } + return null; + } + + /** Disables annoying Apache HTTP client logging. */ + static void disableHttpLogging() { + // Disables Apache HTTP client logging. + OutputEventListenerBackedLoggerContext context = + (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); + OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); + context.setOutputEventListener( + event -> { + org.gradle.internal.logging.events.LogEvent logEvent = + (org.gradle.internal.logging.events.LogEvent) event; + if (!logEvent.getCategory().contains("org.apache")) { + defaultOutputEventListener.onOutput(event); + } + }); + + // Disable Google HTTP Client network logging only when 'java.util.logging.config.file' system + // property is undefined: https://github.com/GoogleContainerTools/jib/issues/2356 + if (System.getProperty("java.util.logging.config.file") == null) { + // Disables Google HTTP client logging. + java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); + } + } + + /** + * Converts a {@code String->String} file-path-to-file-permissions map to an equivalent {@code + * String->FilePermission} map. + * + * @param stringMap the map to convert (example entry: {@code "/path/on/container" -> "755"}) + * @return the converted map + */ + static Map convertPermissionsMap(Map stringMap) { + // Order is important, so use a LinkedHashMap + Map permissionsMap = new LinkedHashMap<>(); + for (Map.Entry entry : stringMap.entrySet()) { + permissionsMap.put(entry.getKey(), FilePermissions.fromOctalString(entry.getValue())); + } + return permissionsMap; + } + + private TaskCommon() {} +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java new file mode 100644 index 0000000000..d25faebc3a --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.common.base.Strings; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; + +/** + * This internal Skaffold-related goal checks that the Jib plugin version is within some specified + * range. It is only required so that older versions of Jib (prior to the introduction of the {@code + * jib.requiredVersion} property) will error in such a way that it indicates the jib version is out + * of date. This goal can be removed once there are no users of Jib prior to 1.4.0. + * + *

    Expected use: {@code ./gradlew _skaffoldFailIfJibOutOfDate -Djib.requiredVersion='[1.4,2)' + * jibDockerBuild --image=xxx} + */ +public class CheckJibVersionTask extends DefaultTask { + + /** Task Action, check if jib and skaffold versions are compatible. */ + @TaskAction + public void checkVersion() { + if (Strings.isNullOrEmpty(System.getProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME))) { + throw new GradleException( + JibPlugin.SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME + + " requires " + + JibPlugin.REQUIRED_VERSION_PROPERTY_NAME + + " to be set"); + } + // no-op as Jib version compatibility is actually checked in JibPlugin + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java new file mode 100644 index 0000000000..6a72637225 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -0,0 +1,279 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.gradle.ExtraDirectoryParameters; +import com.google.cloud.tools.jib.gradle.JibExtension; +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.base.Preconditions; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.PublishArtifact; +import org.gradle.api.initialization.Settings; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out changing source dependencies on a project. + * + *

    Expected use: {@code ./gradlew _jibSkaffoldFilesV2 -q} or {@code ./gradlew + * ::_jibSkaffoldFilesV2 -q} + */ +public class FilesTaskV2 extends DefaultTask { + + private final SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); + + @Nullable private JibExtension jibExtension; + + public FilesTaskV2 setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + /** + * Task Action, print files. + * + * @throws IOException if an error occurs generating the json string + */ + @TaskAction + public void listFiles() throws IOException { + Preconditions.checkNotNull(jibExtension); + Project project = getProject(); + + // If this is not the root project, add the root project's build.gradle and settings.gradle + if (project != project.getRootProject()) { + addGradleFiles(project.getRootProject()); + } + + addProjectFiles(project); + + // Add extra layer + List extraDirectories = + jibExtension.getExtraDirectories().getPaths().stream() + .map(ExtraDirectoryParameters::getFrom) + .collect(Collectors.toList()); + extraDirectories.stream().filter(Files::exists).forEach(skaffoldFilesOutput::addInput); + + // Find project dependencies + Set projectDependencies = findProjectDependencies(project); + + Set projectDependencyJars = new HashSet<>(); + for (ProjectDependency projectDependency : projectDependencies) { + Project dependentProject = getDependentProject(projectDependency); + addProjectFiles(dependentProject); + + // Keep track of project dependency jars for filtering out later + String configurationName = projectDependency.getTargetConfiguration(); + if (configurationName == null) { + configurationName = "default"; + } + for (Configuration targetConfiguration : + dependentProject.getConfigurations().getByName(configurationName).getHierarchy()) { + for (PublishArtifact artifact : targetConfiguration.getArtifacts()) { + projectDependencyJars.add(artifact.getFile()); + } + } + } + + // Add SNAPSHOT, non-project dependency jars + for (File file : + project.getConfigurations().getByName(jibExtension.getConfigurationName().get())) { + if (!projectDependencyJars.contains(file) && file.toString().contains("SNAPSHOT")) { + skaffoldFilesOutput.addInput(file.toPath()); + projectDependencyJars.add(file); // Add to set to avoid printing the same files twice + } + } + + // Configure other files from config + SkaffoldWatchParameters watch = jibExtension.getSkaffold().getWatch(); + watch.getBuildIncludes().forEach(skaffoldFilesOutput::addBuild); + watch.getIncludes().forEach(skaffoldFilesOutput::addInput); + // we don't do any special pre-processing for ignore (input and ignore can overlap with exact + // matches) + watch.getExcludes().forEach(skaffoldFilesOutput::addIgnore); + + // Print files + System.out.println(); + System.out.println("BEGIN JIB JSON"); + System.out.println(skaffoldFilesOutput.getJsonString()); + } + + /** + * Adds the locations of a project's build.gradle, settings.gradle, and gradle.properties. + * + * @param project the project + */ + private void addGradleFiles(Project project) { + Path projectPath = project.getProjectDir().toPath(); + + // Add build.gradle + skaffoldFilesOutput.addBuild(project.getBuildFile().toPath()); + + // Add settings.gradle + addSettingsFile(project, projectPath); + + // Add gradle.properties + if (Files.exists(projectPath.resolve("gradle.properties"))) { + skaffoldFilesOutput.addBuild(projectPath.resolve("gradle.properties")); + } + } + + /** + * Adds the settings.gradle file for a project. + * + *

    Uses reflection to call getSettingsFile() for compatibility with both Gradle 6 and 9 + * (getSettingsFile() was removed in Gradle 9). + * + * @param project the project + * @param projectPath the project directory path + */ + private void addSettingsFile(Project project, Path projectPath) { + boolean settingsFileAdded = false; + try { + Object startParameter = project.getGradle().getStartParameter(); + java.lang.reflect.Method getSettingsFileMethod = + startParameter.getClass().getMethod("getSettingsFile"); + File settingsFile = (File) getSettingsFileMethod.invoke(startParameter); + if (settingsFile != null) { + skaffoldFilesOutput.addBuild(settingsFile.toPath()); + settingsFileAdded = true; + } + } catch (ReflectiveOperationException e) { + // Fall through to default settings file check + } + + // Fall back to default settings file location if not already added + if (!settingsFileAdded && Files.exists(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE))) { + skaffoldFilesOutput.addBuild(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE)); + } + } + + /** + * Prints build files, sources, and resources associated with a project. + * + * @param project the project + */ + private void addProjectFiles(Project project) { + // Add build config, settings, etc. + addGradleFiles(project); + + // Add sources + resources + SourceSetContainer sourceSetContainer = + project.getExtensions().findByType(SourceSetContainer.class); + if (sourceSetContainer != null) { + SourceSet mainSourceSet = sourceSetContainer.findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet != null) { + mainSourceSet + .getAllSource() + .getSourceDirectories() + .forEach( + sourceDirectory -> { + if (sourceDirectory.exists()) { + skaffoldFilesOutput.addInput(sourceDirectory.toPath()); + } + }); + } + } + } + + /** + * Collects a project's project dependencies, including all transitive project dependencies. + * + * @param project the project to find the project dependencies for + * @return the set of project dependencies + */ + private Set findProjectDependencies(Project project) { + Preconditions.checkNotNull(jibExtension); + + Set projectDependencies = new HashSet<>(); + Deque projects = new ArrayDeque<>(); + projects.push(project); + + String configurationName = jibExtension.getConfigurationName().get(); + + while (!projects.isEmpty()) { + Project currentProject = projects.pop(); + + // Search through all dependencies + Configuration runtimeClasspath = + currentProject.getConfigurations().findByName(configurationName); + if (runtimeClasspath != null) { + for (Configuration configuration : runtimeClasspath.getHierarchy()) { + for (Dependency dependency : configuration.getDependencies()) { + if (dependency instanceof ProjectDependency) { + // If this is a project dependency, save it + ProjectDependency projectDependency = (ProjectDependency) dependency; + if (!projectDependencies.contains(projectDependency)) { + projects.push(getDependentProject(projectDependency)); + projectDependencies.add(projectDependency); + } + } + } + } + } + } + return projectDependencies; + } + + /** + * Resolves a {@link ProjectDependency} to its corresponding {@link Project} instance. + * + *

    Uses reflection to handle both Gradle 6 (getDependencyProject()) and Gradle 9+ (getPath()). + * + * @param projectDependency the project dependency to resolve + * @return the resolved project + * @throws RuntimeException if the dependent project could not be resolved + */ + private Project getDependentProject(ProjectDependency projectDependency) { + // Try getDependencyProject() first (Gradle 6-8) + try { + java.lang.reflect.Method getDependencyProjectMethod = + projectDependency.getClass().getMethod("getDependencyProject"); + return (Project) getDependencyProjectMethod.invoke(projectDependency); + } catch (NoSuchMethodException e) { + // Fall through to getPath() approach (Gradle 9+) + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Failed to resolve dependent project from " + projectDependency, e); + } + + // Try getPath() approach (Gradle 9+) + try { + java.lang.reflect.Method getPathMethod = projectDependency.getClass().getMethod("getPath"); + String path = (String) getPathMethod.invoke(projectDependency); + return getProject().project(path); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Failed to resolve dependent project from " + projectDependency, e); + } + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java new file mode 100644 index 0000000000..5ccbea77a8 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.gradle.JibExtension; +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import com.google.common.base.Preconditions; +import java.io.IOException; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out to.image configuration and project name, used for Jib project detection in Skaffold. + * + *

    Expected use: {@code ./gradlew _jibSkaffoldInit -q} + */ +public class InitTask extends DefaultTask { + + @Nullable private JibExtension jibExtension; + + public InitTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + /** + * Task Action, lists modules and targets. + * + * @throws IOException if an error occurs generating the json string + */ + @TaskAction + public void listModulesAndTargets() throws IOException { + Project project = getProject(); + // Ignore parent projects + if (!project.getSubprojects().isEmpty()) { + return; + } + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); + skaffoldInitOutput.setImage(Preconditions.checkNotNull(jibExtension).getTo().getImage()); + if (!project.equals(project.getRootProject())) { + skaffoldInitOutput.setProject(project.getName()); + } + System.out.println(); + System.out.println("BEGIN JIB JSON"); + System.out.println(skaffoldInitOutput.getJsonString()); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldParameters.java new file mode 100644 index 0000000000..58c6124a8c --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldParameters.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.common.base.Preconditions; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.Nested; + +/** Skaffold specific JibExtension parameters. */ +public class SkaffoldParameters { + + private final SkaffoldWatchParameters watch; + private final SkaffoldSyncParameters sync; + + @Inject + public SkaffoldParameters(Project project) { + ObjectFactory objectFactory = project.getObjects(); + + watch = objectFactory.newInstance(SkaffoldWatchParameters.class, project); + sync = objectFactory.newInstance(SkaffoldSyncParameters.class, project); + + Preconditions.checkNotNull(watch); + } + + public void watch(Action action) { + action.execute(watch); + } + + public void sync(Action action) { + action.execute(sync); + } + + @Nested + public SkaffoldWatchParameters getWatch() { + return watch; + } + + @Nested + public SkaffoldSyncParameters getSync() { + return sync; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldSyncParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldSyncParameters.java new file mode 100644 index 0000000000..ceeca5d172 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldSyncParameters.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.tasks.Internal; + +/** Skaffold specific JibExtension parameters for configuring files to sync. */ +public class SkaffoldSyncParameters { + private final Project project; + + private Set excludes = Collections.emptySet(); + + @Inject + public SkaffoldSyncParameters(Project project) { + this.project = project; + } + + /** + * Get the excludes directive for sync functionality in skaffold. + * + * @return a set of absolute paths + */ + @Internal + public Set getExcludes() { + return excludes; + } + + /** + * Sets excludes. {@code excludes} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). + * + * @param paths paths to set on excludes + */ + public void setExcludes(Object paths) { + this.excludes = + project.files(paths).getFiles().stream() + .map(File::toPath) + .map(Path::toAbsolutePath) + .collect(Collectors.toSet()); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldWatchParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldWatchParameters.java new file mode 100644 index 0000000000..5f294100b5 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldWatchParameters.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.tasks.Internal; + +/** Skaffold specific JibExtension parameters for configuring files to watch. */ +public class SkaffoldWatchParameters { + + private final Project project; + + private Set buildIncludes = Collections.emptySet(); + private Set includes = Collections.emptySet(); + private Set excludes = Collections.emptySet(); + + @Inject + public SkaffoldWatchParameters(Project project) { + this.project = project; + } + + /** + * A set of absolute paths to include with skaffold watching. + * + * @return a set of absolute paths + */ + @Internal + public Set getBuildIncludes() { + return buildIncludes; + } + + /** + * Sets includes. {@code includes} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). + * + * @param paths paths to set on includes + */ + public void setBuildIncludes(Object paths) { + this.buildIncludes = + project.files(paths).getFiles().stream() + .map(File::toPath) + .map(Path::toAbsolutePath) + .collect(Collectors.toSet()); + } + + /** + * A set of absolute paths to include with skaffold watching. + * + * @return a set of absolute paths + */ + @Internal + public Set getIncludes() { + return includes; + } + + /** + * Sets includes. {@code includes} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). + * + * @param paths paths to set on includes + */ + public void setIncludes(Object paths) { + this.includes = + project.files(paths).getFiles().stream() + .map(File::toPath) + .map(Path::toAbsolutePath) + .collect(Collectors.toSet()); + } + + /** + * A set of absolute paths to exclude from skaffold watching. + * + * @return a set of absolute paths + */ + @Internal + public Set getExcludes() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + return excludes; + } + + /** + * Sets excludes. {@code excludes} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). + * + * @param paths paths to set on excludes + */ + public void setExcludes(Object paths) { + this.excludes = + project.files(paths).getFiles().stream() + .map(File::toPath) + .map(Path::toAbsolutePath) + .collect(Collectors.toSet()); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java new file mode 100644 index 0000000000..7d8d9e4b02 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.gradle.GradleProjectProperties; +import com.google.cloud.tools.jib.gradle.GradleRawConfiguration; +import com.google.cloud.tools.jib.gradle.JibExtension; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out a map of local files to their location on the container. + * + *

    Expected use: {@code ./gradlew _jibSkaffoldSyncMap -q} or {@code ./gradlew + * ::_jibSkaffoldSyncMap -q} + */ +public class SyncMapTask extends DefaultTask { + + @Nullable private JibExtension jibExtension; + + public SyncMapTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + /** Task Action, lists files and container targets. */ + @TaskAction + public void listFilesAndTargets() { + Preconditions.checkNotNull(jibExtension); + + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + tempDirectoryProvider, + jibExtension.getConfigurationName().get()); + + GradleRawConfiguration configuration = new GradleRawConfiguration(jibExtension); + + // TODO: move these shared checks with SyncMapMojo into plugins-common + if (projectProperties.isWarProject()) { + throw new GradleException( + "Skaffold sync is currently only available for 'jar' style Jib projects, but the project " + + getProject().getName() + + " is configured to generate a 'war'"); + } + try { + if (!ContainerizingMode.EXPLODED.equals( + ContainerizingMode.from(jibExtension.getContainerizingMode()))) { + throw new GradleException( + "Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of " + + getProject().getName() + + " is '" + + jibExtension.getContainerizingMode() + + "'"); + } + } catch (InvalidContainerizingModeException ex) { + throw new GradleException("Invalid containerizing mode", ex); + } + + try { + String syncMapJson = + PluginConfigurationProcessor.getSkaffoldSyncMap( + configuration, + projectProperties, + jibExtension.getSkaffold().getSync().getExcludes()); + + System.out.println(); + System.out.println("BEGIN JIB JSON: SYNCMAP/1"); + System.out.println(syncMapJson); + + } catch (Exception ex) { + throw new GradleException("Failed to generate a Jib file map for sync with Skaffold", ex); + } + } + } +} diff --git a/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties b/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties deleted file mode 100644 index 20d8a6de70..0000000000 --- a/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2018 Google LLC. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy of -# the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. -# - -implementation-class=com.google.cloud.tools.jib.gradle.JibPlugin diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java deleted file mode 100644 index 142332ee7e..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class BuildDockerTaskTest { - - @Mock private JibExtension mockJibExtension; - @Mock private GradleBuildLogger mockBuildLogger; - - private BuildDockerTask task; - - @Before - public void setup() { - Project tempProject = ProjectBuilder.builder().withName("project-name").build(); - tempProject.setVersion("project-version"); - task = tempProject.getTasks().create("tempDockerTask", BuildDockerTask.class); - task.setJibExtension(mockJibExtension); - } - - @Test - public void testGetDockerTag_configured() throws InvalidImageReferenceException { - Mockito.when(mockJibExtension.getTargetImage()).thenReturn("a/b:c"); - ImageReference result = task.getDockerTag(mockBuildLogger); - Assert.assertEquals("a/b", result.getRepository()); - Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); - } - - @Test - public void testGetDockerTag_notConfigured() throws InvalidImageReferenceException { - Mockito.when(mockJibExtension.getTargetImage()).thenReturn(null); - ImageReference result = task.getDockerTag(mockBuildLogger); - Assert.assertEquals("project-name", result.getRepository()); - Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockBuildLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the jib.to.image parameter in your " - + "build.gradle, or use the --image= commandline flag."); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesExtensionTest.java new file mode 100644 index 0000000000..63718ce0e5 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesExtensionTest.java @@ -0,0 +1,458 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.gradle.extension.GradleData; +import com.google.cloud.tools.jib.gradle.extension.JibGradlePluginExtension; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration.ExtensionConfiguration; +import com.google.cloud.tools.jib.plugins.extension.ExtensionLogger; +import com.google.cloud.tools.jib.plugins.extension.ExtensionLogger.LogLevel; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; +import com.google.common.collect.ImmutableMap; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.configuration.ConsoleOutput; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPlugin; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Plugin extension test for {@link GradleProjectProperties}. */ +@RunWith(MockitoJUnitRunner.class) +public class GradleProjectPropertiesExtensionTest { + + // Interface to conveniently provide the main extension body using lambda. + @FunctionalInterface + private interface MainExtensionBody { + + ContainerBuildPlan extendContainerBuildPlan( + ContainerBuildPlan buildPlan, + Map properties, + Optional extraConfig, + GradleData gradleData, + ExtensionLogger logger) + throws JibPluginExtensionException; + } + + private static class BaseExtension implements JibGradlePluginExtension { + + private final MainExtensionBody extensionBody; + private final Class extraConfigType; + + private BaseExtension(MainExtensionBody extensionBody, Class extraConfigType) { + this.extensionBody = extensionBody; + this.extraConfigType = extraConfigType; + } + + @Override + public Optional> getExtraConfigType() { + return Optional.ofNullable(extraConfigType); + } + + @Override + public ContainerBuildPlan extendContainerBuildPlan( + ContainerBuildPlan buildPlan, + Map properties, + Optional extraConfig, + GradleData gradleData, + ExtensionLogger logger) + throws JibPluginExtensionException { + return extensionBody.extendContainerBuildPlan( + buildPlan, properties, extraConfig, gradleData, logger); + } + } + + private static class FooExtension extends BaseExtension { + + private FooExtension(MainExtensionBody extensionBody) { + super(extensionBody, ExtensionDefinedFooConfig.class); + } + } + + private static class BarExtension extends BaseExtension { + + private BarExtension(MainExtensionBody extensionBody) { + super(extensionBody, ExtensionDefinedBarConfig.class); + } + } + + private static class BaseExtensionConfig implements ExtensionConfiguration { + + private final String extensionClass; + private final Map properties; + private final Action extraConfig; + + private BaseExtensionConfig( + String extensionClass, Map properties, Action extraConfig) { + this.extensionClass = extensionClass; + this.properties = properties; + this.extraConfig = extraConfig; + } + + @Override + public Map getProperties() { + return properties; + } + + @Override + public String getExtensionClass() { + return extensionClass; + } + + @Override + public Optional getExtraConfiguration() { + return Optional.ofNullable(extraConfig); + } + } + + private static class FooExtensionConfig extends BaseExtensionConfig { + + private FooExtensionConfig() { + super(FooExtension.class.getName(), Collections.emptyMap(), null); + } + + private FooExtensionConfig(Map properties) { + super(FooExtension.class.getName(), properties, null); + } + + private FooExtensionConfig(ExtensionDefinedFooConfig extraConfig) { + super( + FooExtension.class.getName(), + Collections.emptyMap(), + new Action() { + @Override + public void execute(ExtensionDefinedFooConfig instance) { + instance.fooParam = extraConfig.fooParam; + } + }); + } + } + + private static class BarExtensionConfig extends BaseExtensionConfig { + + private BarExtensionConfig() { + super(BarExtension.class.getName(), Collections.emptyMap(), null); + } + + private BarExtensionConfig(ExtensionDefinedBarConfig extraConfig) { + super( + BarExtension.class.getName(), + Collections.emptyMap(), + new Action() { + @Override + public void execute(ExtensionDefinedBarConfig instance) { + instance.barParam = extraConfig.barParam; + } + }); + } + } + + // Not to be confused with Jib's plugin extension config. This class is for an extension-defined + // config specific to a third-party extension. + private static class ExtensionDefinedFooConfig { + + private String fooParam; + + private ExtensionDefinedFooConfig(String fooParam) { + this.fooParam = fooParam; + } + } + + private static class ExtensionDefinedBarConfig { + + private String barParam; + + private ExtensionDefinedBarConfig(String barParam) { + this.barParam = barParam; + } + } + + @Mock private TempDirectoryProvider mockTempDirectoryProvider; + @Mock private Logger mockLogger; + @Mock private ObjectFactory mockObjectFactory; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Project mockProject; + + private List> loadedExtensions = Collections.emptyList(); + private final JibContainerBuilder containerBuilder = Jib.fromScratch(); + + private GradleProjectProperties gradleProjectProperties; + + @Before + public void setUp() { + Mockito.when(mockLogger.isDebugEnabled()).thenReturn(true); + Mockito.when(mockLogger.isInfoEnabled()).thenReturn(true); + Mockito.when(mockLogger.isWarnEnabled()).thenReturn(true); + Mockito.when(mockLogger.isErrorEnabled()).thenReturn(true); + + Mockito.when(mockProject.getGradle().getStartParameter().getConsoleOutput()) + .thenReturn(ConsoleOutput.Plain); + Mockito.when(mockProject.getObjects()).thenReturn(mockObjectFactory); + Mockito.when( + mockObjectFactory.newInstance( + Mockito.eq(ExtensionDefinedFooConfig.class), Mockito.any())) + .thenReturn(new ExtensionDefinedFooConfig("uninitialized")); + Mockito.when( + mockObjectFactory.newInstance( + Mockito.eq(ExtensionDefinedBarConfig.class), Mockito.any())) + .thenReturn(new ExtensionDefinedBarConfig("uninitialized")); + + gradleProjectProperties = + new GradleProjectProperties( + mockProject, + mockLogger, + mockTempDirectoryProvider, + () -> loadedExtensions, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + } + + @Test + public void testRunPluginExtensions_noExtensionsConfigured() throws JibPluginExtensionException { + FooExtension extension = + new FooExtension((buildPlan, properties, extraConfig, gradleData, logger) -> buildPlan); + loadedExtensions = Arrays.asList(extension); + + JibContainerBuilder extendedBuilder = + gradleProjectProperties.runPluginExtensions(Collections.emptyList(), containerBuilder); + Assert.assertSame(extendedBuilder, containerBuilder); + + gradleProjectProperties.waitForLoggingThread(); + Mockito.verify(mockLogger).debug("No Jib plugin extensions configured to load"); + } + + @Test + public void testRunPluginExtensions_configuredExtensionNotFound() { + try { + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig()), containerBuilder); + Assert.fail(); + } catch (JibPluginExtensionException ex) { + Assert.assertEquals( + "extension configured but not discovered on Jib runtime classpath: com.google.cloud." + + "tools.jib.gradle.GradleProjectPropertiesExtensionTest$FooExtension", + ex.getMessage()); + } + } + + @Test + public void testRunPluginExtensions() throws JibPluginExtensionException { + FooExtension extension = + new FooExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> { + logger.log(LogLevel.ERROR, "awesome error from my extension"); + return buildPlan.toBuilder().setUser("user from extension").build(); + }); + loadedExtensions = Arrays.asList(extension); + + JibContainerBuilder extendedBuilder = + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig()), containerBuilder); + Assert.assertEquals("user from extension", extendedBuilder.toContainerBuildPlan().getUser()); + + gradleProjectProperties.waitForLoggingThread(); + Mockito.verify(mockLogger).error("awesome error from my extension"); + Mockito.verify(mockLogger) + .lifecycle( + Mockito.startsWith( + "Running extension: com.google.cloud.tools.jib.gradle.GradleProjectProperties")); + } + + @Test + public void testRunPluginExtensions_exceptionFromExtension() { + FileNotFoundException fakeException = new FileNotFoundException(); + FooExtension extension = + new FooExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> { + throw new JibPluginExtensionException( + JibGradlePluginExtension.class, "exception from extension", fakeException); + }); + loadedExtensions = Arrays.asList(extension); + + try { + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig()), containerBuilder); + Assert.fail(); + } catch (JibPluginExtensionException ex) { + Assert.assertEquals("exception from extension", ex.getMessage()); + Assert.assertSame(fakeException, ex.getCause()); + } + } + + @Test + public void testRunPluginExtensions_invalidBaseImageFromExtension() { + FooExtension extension = + new FooExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> + buildPlan.toBuilder().setBaseImage(" in*val+id").build()); + loadedExtensions = Arrays.asList(extension); + + try { + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig()), containerBuilder); + Assert.fail(); + } catch (JibPluginExtensionException ex) { + Assert.assertEquals("invalid base image reference: in*val+id", ex.getMessage()); + MatcherAssert.assertThat( + ex.getCause(), CoreMatchers.instanceOf(InvalidImageReferenceException.class)); + } + } + + @Test + public void testRunPluginExtensions_extensionOrder() throws JibPluginExtensionException { + FooExtension fooExtension = + new FooExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> + buildPlan.toBuilder().setBaseImage("foo").build()); + BarExtension barExtension = + new BarExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> + buildPlan.toBuilder().setBaseImage("bar").build()); + loadedExtensions = Arrays.asList(fooExtension, barExtension); + + JibContainerBuilder extendedBuilder1 = + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig(), new BarExtensionConfig()), containerBuilder); + Assert.assertEquals("bar", extendedBuilder1.toContainerBuildPlan().getBaseImage()); + + JibContainerBuilder extendedBuilder2 = + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new BarExtensionConfig(), new FooExtensionConfig()), containerBuilder); + Assert.assertEquals("foo", extendedBuilder2.toContainerBuildPlan().getBaseImage()); + } + + @Test + public void testRunPluginExtensions_customProperties() throws JibPluginExtensionException { + FooExtension extension = + new FooExtension( + (buildPlan, properties, extraConfig, gradleData, logger) -> + buildPlan.toBuilder().setUser(properties.get("user")).build()); + loadedExtensions = Arrays.asList(extension); + + JibContainerBuilder extendedBuilder = + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig(ImmutableMap.of("user", "65432"))), + containerBuilder); + Assert.assertEquals("65432", extendedBuilder.toContainerBuildPlan().getUser()); + } + + @Test + public void testRunPluginExtensions_extensionDefinedConfigurations_emptyConfig() + throws JibPluginExtensionException { + FooExtension fooExtension = + new FooExtension( + (buildPlan, properties, extraConfig, mavenData, logger) -> { + Assert.assertEquals(Optional.empty(), extraConfig); + return buildPlan; + }); + BarExtension barExtension = + new BarExtension( + (buildPlan, properties, extraConfig, mavenData, logger) -> { + Assert.assertEquals(Optional.empty(), extraConfig); + return buildPlan; + }); + loadedExtensions = Arrays.asList(fooExtension, barExtension); + + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig(), new BarExtensionConfig()), containerBuilder); + } + + @Test + public void testRunPluginExtensions_extensionDefinedConfigurations() + throws JibPluginExtensionException { + FooExtension fooExtension = + new FooExtension( + (buildPlan, properties, extraConfig, mavenData, logger) -> { + Assert.assertEquals("fooParamValue", extraConfig.get().fooParam); + return buildPlan; + }); + BarExtension barExtension = + new BarExtension( + (buildPlan, properties, extraConfig, mavenData, logger) -> { + Assert.assertEquals("barParamValue", extraConfig.get().barParam); + return buildPlan; + }); + loadedExtensions = Arrays.asList(fooExtension, barExtension); + + gradleProjectProperties.runPluginExtensions( + Arrays.asList( + new FooExtensionConfig(new ExtensionDefinedFooConfig("fooParamValue")), + new BarExtensionConfig(new ExtensionDefinedBarConfig("barParamValue"))), + containerBuilder); + } + + @Test + public void testRunPluginExtensions_ignoreUnexpectedExtraConfig() + throws JibPluginExtensionException { + BaseExtension extension = + new BaseExtension<>( + (buildPlan, properties, extraConfig, mavenData, logger) -> buildPlan, null); + loadedExtensions = Arrays.asList(extension); + + ExtensionConfiguration extensionConfig = + new BaseExtensionConfig<>( + BaseExtension.class.getName(), Collections.emptyMap(), (ignored) -> {}); + try { + gradleProjectProperties.runPluginExtensions(Arrays.asList(extensionConfig), containerBuilder); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "extension BaseExtension does not expect extension-specific configuration; remove the " + + "inapplicable 'pluginExtension.configuration' from Gradle build script", + ex.getMessage()); + } + } + + @Test + public void testRunPluginExtensions_runtimeExceptionFromExtension() { + FooExtension extension = + new FooExtension( + (buildPlan, properties, extraConfig, mavenData, logger) -> { + throw new IndexOutOfBoundsException("buggy extension"); + }); + loadedExtensions = Arrays.asList(extension); + + try { + gradleProjectProperties.runPluginExtensions( + Arrays.asList(new FooExtensionConfig()), containerBuilder); + Assert.fail(); + } catch (JibPluginExtensionException ex) { + Assert.assertEquals(FooExtension.class, ex.getExtensionClass()); + Assert.assertEquals("extension crashed: buggy extension", ex.getMessage()); + } + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index f7c439d556..7698228ab0 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,65 +16,564 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; +import com.google.cloud.tools.jib.api.buildplan.FileEntry; +import com.google.cloud.tools.jib.api.buildplan.FilePermissions; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.gradle.extension.JibGradlePluginExtension; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import java.util.Collections; +import com.google.common.io.ByteStreams; +import com.google.common.io.Resources; +import com.google.common.truth.Correspondence; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; -import org.gradle.api.internal.file.FileResolver; -import org.gradle.api.java.archives.Manifest; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.FileCollection; import org.gradle.api.java.archives.internal.DefaultManifest; +import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; -import org.junit.Assert; +import org.gradle.testfixtures.ProjectBuilder; +import org.hamcrest.CoreMatchers; +import org.junit.Assume; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Test for {@link GradleProjectProperties}. */ @RunWith(MockitoJUnitRunner.class) public class GradleProjectPropertiesTest { - @Mock private FileResolver mockFileResolver; - @Mock private Jar mockJar; - @Mock private Jar mockJar2; - @Mock private Project mockProject; - @Mock private GradleBuildLogger mockGradleBuildLogger; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; + private static final Correspondence SOURCE_FILE_OF = + Correspondence.transforming(FileEntry::getSourceFile, "has sourceFile of"); + private static final Correspondence EXTRACTION_PATH_OF = + Correspondence.transforming( + entry -> entry.getExtractionPath().toString(), "has extractionPath of"); + private static final Correspondence FILE_PATH_OF = + Correspondence.transforming(File::toPath, "has Path of"); + + private static final Instant EPOCH_PLUS_32 = Instant.ofEpochSecond(32); + + /** Helper for reading back layers in a {@link BuildContext}. */ + private static class ContainerBuilderLayers { + + private final FileEntriesLayer resourcesLayer; + private final FileEntriesLayer classesLayer; + private final FileEntriesLayer dependenciesLayer; + private final FileEntriesLayer snapshotsLayer; + + private ContainerBuilderLayers(BuildContext buildContext) { + resourcesLayer = getLayerByName(buildContext, LayerType.RESOURCES.getName()); + classesLayer = getLayerByName(buildContext, LayerType.CLASSES.getName()); + dependenciesLayer = getLayerByName(buildContext, LayerType.DEPENDENCIES.getName()); + snapshotsLayer = getLayerByName(buildContext, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + } + + private static FileEntriesLayer getLayerByName(BuildContext buildContext, String name) { + List layers = buildContext.getLayerConfigurations(); + return layers.stream().filter(layer -> layer.getName().equals(name)).findFirst().get(); + } + } + + private static Path getResource(String path) throws URISyntaxException { + return Paths.get(Resources.getResource(path).toURI()); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private TempDirectoryProvider mockTempDirectoryProvider; + @Mock private Supplier>> mockExtensionLoader; + @Mock private Logger mockLogger; - private Manifest manifest; private GradleProjectProperties gradleProjectProperties; + private Project project; @Before - public void setup() { - manifest = new DefaultManifest(mockFileResolver); - Mockito.when(mockJar.getManifest()).thenReturn(manifest); + public void setUp() throws URISyntaxException, IOException { + when(mockLogger.isDebugEnabled()).thenReturn(true); + when(mockLogger.isInfoEnabled()).thenReturn(true); + when(mockLogger.isWarnEnabled()).thenReturn(true); + when(mockLogger.isErrorEnabled()).thenReturn(true); + + Path projectDir = getResource("gradle/application"); + project = + ProjectBuilder.builder() + .withName("my-app") + .withProjectDir(projectDir.toFile()) + .withGradleUserHomeDir(temporaryFolder.newFolder()) + .build(); + project.getPlugins().apply("java"); + + DependencyHandler dependencies = project.getDependencies(); + dependencies.add( + "implementation", + project.files( + "dependencies/library.jarC.jar", + "dependencies/libraryB.jar", + "dependencies/libraryA.jar", + "dependencies/dependency-1.0.0.jar", + "dependencies/more/dependency-1.0.0.jar", + "dependencies/another/one/dependency-1.0.0.jar", + "dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); + + // We can't commit an empty directory in Git, so create (if not exist). + Path emptyDirectory = getResource("gradle/webapp").resolve("WEB-INF/classes/empty_dir"); + Files.createDirectories(emptyDirectory); gradleProjectProperties = new GradleProjectProperties( - mockProject, mockGradleBuildLogger, mockSourceFilesConfiguration); + project, + mockLogger, + mockTempDirectoryProvider, + mockExtensionLoader, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); } @Test public void testGetMainClassFromJar_success() { - manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); - Mockito.when(mockProject.getTasksByName("jar", false)).thenReturn(ImmutableSet.of(mockJar)); - Assert.assertEquals("some.main.class", gradleProjectProperties.getMainClassFromJar()); + Jar jar = project.getTasks().withType(Jar.class).getByName("jar"); + jar.setManifest( + new DefaultManifest(null).attributes(ImmutableMap.of("Main-Class", "some.main.class"))); + + assertThat(gradleProjectProperties.getMainClassFromJarPlugin()).isEqualTo("some.main.class"); } @Test public void testGetMainClassFromJar_missing() { - Mockito.when(mockProject.getTasksByName("jar", false)).thenReturn(Collections.emptySet()); - Assert.assertEquals(null, gradleProjectProperties.getMainClassFromJar()); + assertThat(gradleProjectProperties.getMainClassFromJarPlugin()).isNull(); + } + + @Test + public void testGetMainClassFromJarAsProperty_success() { + Property mainClass = + project.getObjects().property(String.class).value("some.main.class"); + + Jar jar = project.getTasks().withType(Jar.class).getByName("jar"); + jar.setManifest(new DefaultManifest(null).attributes(ImmutableMap.of("Main-Class", mainClass))); + + assertThat(gradleProjectProperties.getMainClassFromJarPlugin()).isEqualTo("some.main.class"); + } + + @Test + public void testGetMainClassFromJarAsPropertyWithValueNull_missing() { + Property mainClass = project.getObjects().property(String.class).value((String) null); + + Jar jar = project.getTasks().withType(Jar.class).getByName("jar"); + jar.setManifest(new DefaultManifest(null).attributes(ImmutableMap.of("Main-Class", mainClass))); + + assertThat(gradleProjectProperties.getMainClassFromJarPlugin()).isNull(); + } + + @Test + public void testIsWarProject() { + project.getPlugins().apply("war"); + assertThat(gradleProjectProperties.isWarProject()).isTrue(); + } + + @Test + public void testGetInputFiles() throws URISyntaxException { + Path applicationDirectory = getResource("gradle/application"); + + List extraDirectories = Arrays.asList(applicationDirectory.resolve("extra-directory")); + FileCollection fileCollection = + GradleProjectProperties.getInputFiles( + project, extraDirectories, JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + + assertThat(fileCollection) + .comparingElementsUsing(FILE_PATH_OF) + .containsExactly( + applicationDirectory.resolve("build/classes/java/main"), + applicationDirectory.resolve("build/resources/main"), + applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar"), + applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/more/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/another/one/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/libraryA.jar"), + applicationDirectory.resolve("dependencies/libraryB.jar"), + applicationDirectory.resolve("dependencies/library.jarC.jar"), + applicationDirectory.resolve("extra-directory")); + } + + @Test + public void testConvertPermissionsMap() { + Map map = ImmutableMap.of("/test/folder/file1", "123", "/test/file2", "456"); + assertThat(TaskCommon.convertPermissionsMap(map)) + .containsExactly( + "/test/folder/file1", + FilePermissions.fromOctalString("123"), + "/test/file2", + FilePermissions.fromOctalString("456")) + .inOrder(); + + Exception exception = + assertThrows( + IllegalArgumentException.class, + () -> TaskCommon.convertPermissionsMap(ImmutableMap.of("path", "invalid permission"))); + assertThat(exception) + .hasMessageThat() + .isEqualTo("octalPermissions must be a 3-digit octal number (000-777)"); + } + + @Test + public void testGetMajorJavaVersion() { + JavaPluginExtension extension = project.getExtensions().findByType(JavaPluginExtension.class); + + extension.setTargetCompatibility(JavaVersion.VERSION_1_3); + assertThat(gradleProjectProperties.getMajorJavaVersion()).isEqualTo(3); + + extension.setTargetCompatibility(JavaVersion.VERSION_11); + assertThat(gradleProjectProperties.getMajorJavaVersion()).isEqualTo(11); + + extension.setTargetCompatibility(JavaVersion.VERSION_1_9); + assertThat(gradleProjectProperties.getMajorJavaVersion()).isEqualTo(9); + } + + @Test + public void testGetMajorJavaVersion_jvm8() { + Assume.assumeThat(JavaVersion.current(), CoreMatchers.is(JavaVersion.VERSION_1_8)); + + assertThat(gradleProjectProperties.getMajorJavaVersion()).isEqualTo(8); + } + + @Test + public void testGetMajorJavaVersion_jvm11() { + Assume.assumeThat(JavaVersion.current(), CoreMatchers.is(JavaVersion.VERSION_11)); + + assertThat(gradleProjectProperties.getMajorJavaVersion()).isEqualTo(11); + } + + @Test + public void testCreateContainerBuilder_correctSourceFiles() + throws URISyntaxException, InvalidImageReferenceException, CacheDirectoryCreationException { + ContainerBuilderLayers layers = new ContainerBuilderLayers(setupBuildContext()); + + Path applicationDirectory = getResource("gradle/application"); + assertThat(layers.snapshotsLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); + assertThat(layers.dependenciesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/more/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/another/one/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/libraryA.jar"), + applicationDirectory.resolve("dependencies/libraryB.jar"), + applicationDirectory.resolve("dependencies/library.jarC.jar")); + assertThat(layers.resourcesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + applicationDirectory.resolve("build/resources/main/resourceA"), + applicationDirectory.resolve("build/resources/main/resourceB"), + applicationDirectory.resolve("build/resources/main/world")); + assertThat(layers.classesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + applicationDirectory.resolve("build/classes/java/main/HelloWorld.class"), + applicationDirectory.resolve("build/classes/java/main/some.class")); + + List allFileEntries = new ArrayList<>(); + allFileEntries.addAll(layers.snapshotsLayer.getEntries()); + allFileEntries.addAll(layers.dependenciesLayer.getEntries()); + allFileEntries.addAll(layers.classesLayer.getEntries()); + allFileEntries.addAll(layers.resourcesLayer.getEntries()); + Set modificationTimes = + allFileEntries.stream().map(FileEntry::getModificationTime).collect(Collectors.toSet()); + assertThat(modificationTimes).containsExactly(EPOCH_PLUS_32); + } + + @Test + public void testCreateContainerBuilder_noClassesFiles() + throws InvalidImageReferenceException, IOException { + Project project = + ProjectBuilder.builder() + .withProjectDir(temporaryFolder.newFolder()) + .withGradleUserHomeDir(temporaryFolder.newFolder()) + .build(); + project.getPlugins().apply("java"); + + gradleProjectProperties = + new GradleProjectProperties( + project, + mockLogger, + mockTempDirectoryProvider, + mockExtensionLoader, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + + gradleProjectProperties.createJibContainerBuilder( + JavaContainerBuilder.from(RegistryImage.named("base")), ContainerizingMode.EXPLODED); + gradleProjectProperties.waitForLoggingThread(); + verify(mockLogger).warn("No classes files were found - did you compile your project?"); + } + + @Test + public void testCreateContainerBuilder_correctExtractionPaths() + throws InvalidImageReferenceException, CacheDirectoryCreationException { + ContainerBuilderLayers layers = new ContainerBuilderLayers(setupBuildContext()); + + assertThat(layers.dependenciesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/libs/dependency-1.0.0-770.jar", + "/my/app/libs/dependency-1.0.0-200.jar", + "/my/app/libs/dependency-1.0.0-480.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar", + "/my/app/libs/library.jarC.jar"); + assertThat(layers.snapshotsLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"); + assertThat(layers.resourcesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"); + assertThat(layers.classesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/classes/HelloWorld.class", "/my/app/classes/some.class"); + } + + @Test + public void testCreateContainerBuilder_war() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Path unzipTarget = setUpWarProject(getResource("gradle/webapp")); + + ContainerBuilderLayers layers = new ContainerBuilderLayers(setupBuildContext()); + assertThat(layers.dependenciesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")); + assertThat(layers.snapshotsLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly(unzipTarget.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); + assertThat(layers.resourcesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + unzipTarget.resolve("META-INF"), + unzipTarget.resolve("META-INF/context.xml"), + unzipTarget.resolve("Test.jsp"), + unzipTarget.resolve("WEB-INF"), + unzipTarget.resolve("WEB-INF/classes"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/test.properties"), + unzipTarget.resolve("WEB-INF/lib"), + unzipTarget.resolve("WEB-INF/web.xml")); + assertThat(layers.classesLayer.getEntries()) + .comparingElementsUsing(SOURCE_FILE_OF) + .containsExactly( + unzipTarget.resolve("WEB-INF/classes/HelloWorld.class"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/Other.class")); + + assertThat(layers.dependenciesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependency-1.0.0.jar"); + assertThat(layers.snapshotsLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"); + assertThat(layers.resourcesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/META-INF", + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", + "/my/app/WEB-INF/web.xml"); + assertThat(layers.classesLayer.getEntries()) + .comparingElementsUsing(EXTRACTION_PATH_OF) + .containsExactly( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/Other.class"); + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() + throws IOException, InvalidImageReferenceException { + temporaryFolder.newFolder("WEB-INF", "lib"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + + assertThat( + gradleProjectProperties.createJibContainerBuilder( + JavaContainerBuilder.from("ignored"), ContainerizingMode.EXPLODED)) + .isNotNull(); } @Test - public void testGetMainClassFromJar_multiple() { - manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); - Mockito.when(mockProject.getTasksByName("jar", false)) - .thenReturn(ImmutableSet.of(mockJar, mockJar2)); - Assert.assertEquals(null, gradleProjectProperties.getMainClassFromJar()); + public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() + throws IOException, InvalidImageReferenceException { + temporaryFolder.newFolder("WEB-INF", "classes"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + + assertThat( + gradleProjectProperties.createJibContainerBuilder( + JavaContainerBuilder.from("ignored"), ContainerizingMode.EXPLODED)) + .isNotNull(); + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() + throws IOException, InvalidImageReferenceException { + setUpWarProject(temporaryFolder.getRoot().toPath()); + + assertThat( + gradleProjectProperties.createJibContainerBuilder( + JavaContainerBuilder.from("ignored"), ContainerizingMode.EXPLODED)) + .isNotNull(); + } + + @Test + public void testGetWarFilePath() throws IOException { + Path outputDir = temporaryFolder.newFolder("output").toPath(); + + project.getPlugins().apply("war"); + War war = project.getTasks().withType(War.class).getByName("war"); + war.getDestinationDirectory().set(outputDir.toFile()); + + assertThat(gradleProjectProperties.getWarFilePath()) + .isEqualTo(outputDir.resolve("my-app.war").toString()); + } + + @Test + public void testGetWarFilePath_bootWar() throws IOException { + Path outputDir = temporaryFolder.newFolder("output").toPath(); + + project.getPlugins().apply("war"); + project.getPlugins().apply("org.springframework.boot"); + War bootWar = project.getTasks().withType(War.class).getByName("bootWar"); + bootWar.getDestinationDirectory().set(outputDir.toFile()); + + assertThat(gradleProjectProperties.getWarFilePath()) + .isEqualTo(outputDir.resolve("my-app.war").toString()); + } + + @Test + public void testGetWarFilePath_bootWarDisabled() throws IOException { + Path outputDir = temporaryFolder.newFolder("output").toPath(); + + project.getPlugins().apply("war"); + War war = project.getTasks().withType(War.class).getByName("war"); + war.getDestinationDirectory().set(outputDir.toFile()); + + project.getPlugins().apply("org.springframework.boot"); + project.getTasks().getByName("bootWar").setEnabled(false); + + assertThat(gradleProjectProperties.getWarFilePath()) + .isEqualTo(outputDir.resolve("my-app.war").toString()); + } + + @Test + public void testGetDependencies() throws URISyntaxException { + assertThat(gradleProjectProperties.getDependencies()) + .containsExactly( + getResource("gradle/application/dependencies/library.jarC.jar"), + getResource("gradle/application/dependencies/libraryB.jar"), + getResource("gradle/application/dependencies/libraryA.jar"), + getResource("gradle/application/dependencies/dependency-1.0.0.jar"), + getResource("gradle/application/dependencies/more/dependency-1.0.0.jar"), + getResource("gradle/application/dependencies/another/one/dependency-1.0.0.jar"), + getResource("gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar")) + .inOrder(); + } + + private BuildContext setupBuildContext() + throws InvalidImageReferenceException, CacheDirectoryCreationException { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(RegistryImage.named("base")) + .setAppRoot(AbsoluteUnixPath.get("/my/app")) + .setModificationTimeProvider((ignored1, ignored2) -> EPOCH_PLUS_32); + JibContainerBuilder jibContainerBuilder = + gradleProjectProperties.createJibContainerBuilder( + javaContainerBuilder, ContainerizingMode.EXPLODED); + return JibContainerBuilderTestHelper.toBuildContext( + jibContainerBuilder, Containerizer.to(RegistryImage.named("to"))); + } + + private Path setUpWarProject(Path webAppDirectory) throws IOException { + File warOutputDir = temporaryFolder.newFolder("output"); + zipUpDirectory(webAppDirectory, warOutputDir.toPath().resolve("my-app.war")); + + project.getPlugins().apply("war"); + War war = project.getTasks().withType(War.class).getByName("war"); + war.getDestinationDirectory().set(warOutputDir); + + // Make "GradleProjectProperties" use this folder to explode the WAR into. + Path unzipTarget = temporaryFolder.newFolder("exploded").toPath(); + when(mockTempDirectoryProvider.newDirectory()).thenReturn(unzipTarget); + return unzipTarget; + } + + private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOException { + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(targetZip))) { + for (Path source : new DirectoryWalker(sourceRoot).filterRoot().walk()) { + + StringJoiner pathJoiner = new StringJoiner("/", "", ""); + sourceRoot.relativize(source).forEach(element -> pathJoiner.add(element.toString())); + String zipEntryPath = + Files.isDirectory(source) ? pathJoiner.toString() + '/' : pathJoiner.toString(); + + ZipEntry entry = new ZipEntry(zipEntryPath); + zipOut.putNextEntry(entry); + if (!Files.isDirectory(source)) { + try (InputStream in = Files.newInputStream(source)) { + ByteStreams.copy(in, zipOut); + } + } + zipOut.closeEntry(); + } + } + return targetZip; } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java new file mode 100644 index 0000000000..8655241553 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Test for {@link GradleRawConfiguration}. */ +@RunWith(MockitoJUnitRunner.class) +public class GradleRawConfigurationTest { + + @Mock private MapProperty labels; + + @Test + public void testGetters() { + JibExtension jibExtension = Mockito.mock(JibExtension.class); + + AuthParameters authParameters = Mockito.mock(AuthParameters.class); + BaseImageParameters baseImageParameters = Mockito.mock(BaseImageParameters.class); + TargetImageParameters targetImageParameters = Mockito.mock(TargetImageParameters.class); + ContainerParameters containerParameters = Mockito.mock(ContainerParameters.class); + DockerClientParameters dockerClientParameters = Mockito.mock(DockerClientParameters.class); + OutputPathsParameters outputPathsParameters = Mockito.mock(OutputPathsParameters.class); + CredHelperParameters fromCredHelperParameters = Mockito.mock(CredHelperParameters.class); + CredHelperParameters toCredHelperParameters = Mockito.mock(CredHelperParameters.class); + Property filesModificationTime = Mockito.mock(Property.class); + Property creationTime = Mockito.mock(Property.class); + + Mockito.when(authParameters.getUsername()).thenReturn("user"); + Mockito.when(authParameters.getPassword()).thenReturn("password"); + Mockito.when(authParameters.getAuthDescriptor()).thenReturn("from.auth"); + Mockito.when(authParameters.getUsernameDescriptor()).thenReturn("from.auth.username"); + Mockito.when(authParameters.getPasswordDescriptor()).thenReturn("from.auth.password"); + + Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); + Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); + Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); + Mockito.when(jibExtension.getDockerClient()).thenReturn(dockerClientParameters); + Mockito.when(jibExtension.getOutputPaths()).thenReturn(outputPathsParameters); + Mockito.when(jibExtension.getAllowInsecureRegistries()).thenReturn(true); + + Mockito.when(fromCredHelperParameters.getHelperName()).thenReturn(Optional.of("gcr")); + Mockito.when(fromCredHelperParameters.getEnvironment()) + .thenReturn(Collections.singletonMap("ENV_VARIABLE", "Value1")); + Mockito.when(baseImageParameters.getCredHelper()).thenReturn(fromCredHelperParameters); + Mockito.when(baseImageParameters.getImage()).thenReturn("openjdk:15"); + Mockito.when(baseImageParameters.getAuth()).thenReturn(authParameters); + + Mockito.when(targetImageParameters.getTags()) + .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); + Mockito.when(toCredHelperParameters.getHelperName()).thenReturn(Optional.of("ecr-login")); + Mockito.when(toCredHelperParameters.getEnvironment()) + .thenReturn(Collections.singletonMap("ENV_VARIABLE", "Value2")); + Mockito.when(targetImageParameters.getCredHelper()).thenReturn(toCredHelperParameters); + + Mockito.when(containerParameters.getAppRoot()).thenReturn("/app/root"); + Mockito.when(containerParameters.getArgs()).thenReturn(Arrays.asList("--log", "info")); + Mockito.when(containerParameters.getEntrypoint()).thenReturn(Arrays.asList("java", "Main")); + Mockito.when(containerParameters.getEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("currency", "dollar"))); + Mockito.when(containerParameters.getJvmFlags()).thenReturn(Arrays.asList("-cp", ".")); + Mockito.when(labels.get()).thenReturn(Collections.singletonMap("unit", "cm")); + Mockito.when(containerParameters.getLabels()).thenReturn(labels); + Mockito.when(containerParameters.getMainClass()).thenReturn("com.example.Main"); + Mockito.when(containerParameters.getPorts()).thenReturn(Arrays.asList("80/tcp", "0")); + Mockito.when(containerParameters.getUser()).thenReturn("admin:wheel"); + Mockito.when(containerParameters.getFilesModificationTime()).thenReturn(filesModificationTime); + Mockito.when(filesModificationTime.get()).thenReturn("2011-12-03T22:42:05Z"); + Mockito.when(containerParameters.getCreationTime()).thenReturn(creationTime); + Mockito.when(creationTime.get()).thenReturn("2011-12-03T11:42:05Z"); + + Mockito.when(dockerClientParameters.getExecutablePath()).thenReturn(Paths.get("test")); + Mockito.when(dockerClientParameters.getEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); + + Mockito.when(outputPathsParameters.getDigestPath()).thenReturn(Paths.get("digest/path")); + Mockito.when(outputPathsParameters.getImageIdPath()).thenReturn(Paths.get("id/path")); + Mockito.when(outputPathsParameters.getImageJsonPath()).thenReturn(Paths.get("json/path")); + Mockito.when(outputPathsParameters.getTarPath()).thenReturn(Paths.get("tar/path")); + + GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + + AuthProperty fromAuth = rawConfiguration.getFromAuth(); + Assert.assertEquals("user", fromAuth.getUsername()); + Assert.assertEquals("password", fromAuth.getPassword()); + Assert.assertEquals("from.auth", fromAuth.getAuthDescriptor()); + Assert.assertEquals("from.auth.username", fromAuth.getUsernameDescriptor()); + Assert.assertEquals("from.auth.password", fromAuth.getPasswordDescriptor()); + + Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); + Assert.assertEquals("/app/root", rawConfiguration.getAppRoot()); + Assert.assertEquals(Arrays.asList("java", "Main"), rawConfiguration.getEntrypoint().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("currency", "dollar")), rawConfiguration.getEnvironment()); + Assert.assertEquals("gcr", rawConfiguration.getFromCredHelper().getHelperName().get()); + Assert.assertEquals( + Collections.singletonMap("ENV_VARIABLE", "Value1"), + rawConfiguration.getFromCredHelper().getEnvironment()); + Assert.assertEquals("openjdk:15", rawConfiguration.getFromImage().get()); + Assert.assertEquals(Arrays.asList("-cp", "."), rawConfiguration.getJvmFlags()); + Assert.assertEquals(new HashMap<>(ImmutableMap.of("unit", "cm")), rawConfiguration.getLabels()); + Assert.assertEquals("com.example.Main", rawConfiguration.getMainClass().get()); + Assert.assertEquals(Arrays.asList("80/tcp", "0"), rawConfiguration.getPorts()); + Assert.assertEquals( + Arrays.asList("--log", "info"), rawConfiguration.getProgramArguments().get()); + Assert.assertEquals( + new HashSet<>(Arrays.asList("additional", "tags")), + Sets.newHashSet(rawConfiguration.getToTags())); + Assert.assertEquals("ecr-login", rawConfiguration.getToCredHelper().getHelperName().get()); + Assert.assertEquals( + Collections.singletonMap("ENV_VARIABLE", "Value2"), + rawConfiguration.getToCredHelper().getEnvironment()); + Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); + Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); + Assert.assertEquals("2011-12-03T11:42:05Z", rawConfiguration.getCreationTime()); + Assert.assertEquals(Paths.get("test"), rawConfiguration.getDockerExecutable().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("docker", "client")), + rawConfiguration.getDockerEnvironment()); + Assert.assertEquals(Paths.get("digest/path"), rawConfiguration.getDigestOutputPath()); + Assert.assertEquals(Paths.get("id/path"), rawConfiguration.getImageIdOutputPath()); + Assert.assertEquals(Paths.get("json/path"), rawConfiguration.getImageJsonOutputPath()); + Assert.assertEquals(Paths.get("tar/path"), rawConfiguration.getTarOutputPath()); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java deleted file mode 100644 index 54376a14c5..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.io.Resources; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Set; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; -import org.gradle.api.internal.file.AbstractFileCollection; -import org.gradle.api.plugins.Convention; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.SourceSetOutput; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for {@link GradleSourceFilesConfiguration}. */ -@RunWith(MockitoJUnitRunner.class) -public class GradleSourceFilesConfigurationTest { - - /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ - private static class TestFileCollection extends AbstractFileCollection { - - private final Set files; - - private TestFileCollection(Set files) { - this.files = files; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public Set getFiles() { - return files; - } - } - - @Mock private Project mockProject; - @Mock private Convention mockConvention; - @Mock private JavaPluginConvention mockJavaPluginConvention; - @Mock private SourceSetContainer mockSourceSetContainer; - @Mock private SourceSet mockMainSourceSet; - @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private GradleBuildLogger mockGradleBuildLogger; - - private GradleSourceFilesConfiguration testGradleSourceFilesConfiguration; - - @Before - public void setUp() throws URISyntaxException, IOException { - Set classesFiles = - ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI()).toFile()); - FileCollection classesFileCollection = new TestFileCollection(classesFiles); - File resourcesOutputDir = - Paths.get(Resources.getResource("application/resources").toURI()).toFile(); - - Set allFiles = new HashSet<>(classesFiles); - allFiles.add(resourcesOutputDir); - allFiles.add( - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI()).toFile()); - allFiles.add( - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()).toFile()); - allFiles.add( - Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()) - .toFile()); - FileCollection runtimeFileCollection = new TestFileCollection(allFiles); - - Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.getPlugin(JavaPluginConvention.class)) - .thenReturn(mockJavaPluginConvention); - Mockito.when(mockJavaPluginConvention.getSourceSets()).thenReturn(mockSourceSetContainer); - Mockito.when(mockSourceSetContainer.getByName("main")).thenReturn(mockMainSourceSet); - Mockito.when(mockMainSourceSet.getOutput()).thenReturn(mockMainSourceSetOutput); - Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); - Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir); - Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - - testGradleSourceFilesConfiguration = - GradleSourceFilesConfiguration.getForProject(mockProject, mockGradleBuildLogger); - } - - @Test - public void test_correctFiles() throws URISyntaxException { - ImmutableList expectedDependenciesFiles = - ImmutableList.of( - Paths.get( - Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()), - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()), - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); - ImmutableList expectedResourcesFiles = - ImmutableList.of( - Paths.get(Resources.getResource("application/resources").toURI()).resolve("resourceA"), - Paths.get(Resources.getResource("application/resources").toURI()).resolve("resourceB"), - Paths.get(Resources.getResource("application/resources").toURI()).resolve("world")); - ImmutableList expectedClassesFiles = - ImmutableList.of( - Paths.get(Resources.getResource("application/classes").toURI()) - .resolve("HelloWorld.class"), - Paths.get(Resources.getResource("application/classes").toURI()).resolve("some.class")); - - Assert.assertEquals( - expectedDependenciesFiles, testGradleSourceFilesConfiguration.getDependenciesFiles()); - Assert.assertEquals( - expectedResourcesFiles, testGradleSourceFilesConfiguration.getResourcesFiles()); - Assert.assertEquals(expectedClassesFiles, testGradleSourceFilesConfiguration.getClassesFiles()); - } - - @Test - public void test_noClassesFiles() throws IOException { - File nonexistentFile = new File("/nonexistent/file"); - Mockito.when(mockMainSourceSetOutput.getClassesDirs()) - .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - - testGradleSourceFilesConfiguration = - GradleSourceFilesConfiguration.getForProject(mockProject, mockGradleBuildLogger); - - Mockito.verify(mockGradleBuildLogger) - .warn("Could not find build output directory '" + nonexistentFile + "'"); - Mockito.verify(mockGradleBuildLogger) - .warn("No classes files were found - did you compile your project?"); - } - - @Test - public void test_correctPathsOnImage() { - Assert.assertEquals( - "/app/libs/", testGradleSourceFilesConfiguration.getDependenciesPathOnImage()); - Assert.assertEquals( - "/app/resources/", testGradleSourceFilesConfiguration.getResourcesPathOnImage()); - Assert.assertEquals( - "/app/classes/", testGradleSourceFilesConfiguration.getClassesPathOnImage()); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 66cf5785c3..440ae17779 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,33 +16,43 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.tools.jib.api.buildplan.ImageFormat; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link JibExtension}. */ -@RunWith(MockitoJUnitRunner.class) public class JibExtensionTest { - @Mock private BuildLogger mockLogger; + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); private JibExtension testJibExtension; + private Project fakeProject; @Before public void setUp() { - Project fakeProject = ProjectBuilder.builder().build(); + fakeProject = ProjectBuilder.builder().build(); testJibExtension = fakeProject .getExtensions() @@ -51,104 +61,523 @@ public void setUp() { @Test public void testFrom() { - Assert.assertEquals("gcr.io/distroless/java", testJibExtension.getFrom().getImage()); - Assert.assertNull(testJibExtension.getFrom().getCredHelper()); + assertThat(testJibExtension.getFrom().getImage()).isNull(); + assertThat(testJibExtension.getFrom().getCredHelper().getHelper()).isNull(); + + List defaultPlatforms = testJibExtension.getFrom().getPlatforms().get(); + assertThat(defaultPlatforms).hasSize(1); + assertThat(defaultPlatforms.get(0).getArchitecture()).isEqualTo("amd64"); + assertThat(defaultPlatforms.get(0).getOs()).isEqualTo("linux"); testJibExtension.from( from -> { from.setImage("some image"); from.setCredHelper("some cred helper"); - from.auth( - auth -> { - auth.setUsername("some username"); - auth.setPassword("some password"); + from.auth(auth -> auth.setUsername("some username")); + from.auth(auth -> auth.setPassword("some password")); + from.platforms( + platformSpec -> + platformSpec.platform( + platform -> { + platform.setArchitecture("arm"); + platform.setOs("windows"); + })); + }); + assertThat(testJibExtension.getFrom().getImage()).isEqualTo("some image"); + assertThat(testJibExtension.getFrom().getCredHelper().getHelper()) + .isEqualTo("some cred helper"); + assertThat(testJibExtension.getFrom().getAuth().getUsername()).isEqualTo("some username"); + assertThat(testJibExtension.getFrom().getAuth().getPassword()).isEqualTo("some password"); + + List platforms = testJibExtension.getFrom().getPlatforms().get(); + assertThat(platforms).hasSize(1); + assertThat(platforms.get(0).getArchitecture()).isEqualTo("arm"); + assertThat(platforms.get(0).getOs()).isEqualTo("windows"); + } + + @Test + public void testFromCredHelperClosure() { + assertThat(testJibExtension.getFrom().getImage()).isNull(); + assertThat(testJibExtension.getFrom().getCredHelper().getHelper()).isNull(); + + testJibExtension.from( + from -> { + from.setImage("some image"); + from.credHelper( + credHelper -> { + credHelper.setHelper("some cred helper"); + credHelper.setEnvironment(Collections.singletonMap("ENV_VARIABLE", "Value")); }); }); - Assert.assertEquals("some image", testJibExtension.getFrom().getImage()); - Assert.assertEquals("some cred helper", testJibExtension.getFrom().getCredHelper()); - Assert.assertEquals("some username", testJibExtension.getFrom().getAuth().getUsername()); - Assert.assertEquals("some password", testJibExtension.getFrom().getAuth().getPassword()); + assertThat(testJibExtension.getFrom().getCredHelper().getHelper()) + .isEqualTo("some cred helper"); + assertThat(testJibExtension.getFrom().getCredHelper().getEnvironment()) + .isEqualTo(Collections.singletonMap("ENV_VARIABLE", "Value")); } @Test public void testTo() { - Assert.assertNull(testJibExtension.getTo().getImage()); - Assert.assertNull(testJibExtension.getTo().getCredHelper()); + assertThat(testJibExtension.getTo().getImage()).isNull(); + assertThat(testJibExtension.getTo().getCredHelper().getHelper()).isNull(); testJibExtension.to( to -> { to.setImage("some image"); to.setCredHelper("some cred helper"); - to.auth( - auth -> { - auth.setUsername("some username"); - auth.setPassword("some password"); + to.auth(auth -> auth.setUsername("some username")); + to.auth(auth -> auth.setPassword("some password")); + }); + assertThat(testJibExtension.getTo().getImage()).isEqualTo("some image"); + assertThat(testJibExtension.getTo().getCredHelper().getHelper()).isEqualTo("some cred helper"); + assertThat(testJibExtension.getTo().getAuth().getUsername()).isEqualTo("some username"); + assertThat(testJibExtension.getTo().getAuth().getPassword()).isEqualTo("some password"); + } + + @Test + public void testToCredHelperClosure() { + assertThat(testJibExtension.getTo().getImage()).isNull(); + assertThat(testJibExtension.getTo().getCredHelper().getHelper()).isNull(); + + testJibExtension.to( + to -> { + to.setImage("some image"); + to.credHelper( + credHelper -> { + credHelper.setHelper("some cred helper"); + credHelper.setEnvironment(Collections.singletonMap("ENV_VARIABLE", "Value")); }); }); - Assert.assertEquals("some image", testJibExtension.getTo().getImage()); - Assert.assertEquals("some cred helper", testJibExtension.getTo().getCredHelper()); - Assert.assertEquals("some username", testJibExtension.getTo().getAuth().getUsername()); - Assert.assertEquals("some password", testJibExtension.getTo().getAuth().getPassword()); + assertThat(testJibExtension.getTo().getCredHelper().getHelper()).isEqualTo("some cred helper"); + assertThat(testJibExtension.getTo().getCredHelper().getEnvironment()) + .isEqualTo(Collections.singletonMap("ENV_VARIABLE", "Value")); + } + + @Test + public void testToTags_noTagsPropertySet() { + assertThat(testJibExtension.getTo().getTags()).isEmpty(); + } + + @Test + public void testToTags_containsNullTag() { + TargetImageParameters testToParameters = generateTargetImageParametersWithTags(null, "tag1"); + Exception exception = + assertThrows(IllegalArgumentException.class, () -> testToParameters.getTags()); + assertThat(exception).hasMessageThat().isEqualTo("jib.to.tags contains null tag"); + } + + @Test + public void testToTags_containsEmptyTag() { + TargetImageParameters testToParameters = generateTargetImageParametersWithTags("", "tag1"); + Exception exception = + assertThrows(IllegalArgumentException.class, () -> testToParameters.getTags()); + assertThat(exception).hasMessageThat().isEqualTo("jib.to.tags contains empty tag"); } @Test public void testContainer() { - Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getJvmFlags()); - Assert.assertNull(testJibExtension.getContainer().getMainClass()); - Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getArgs()); - Assert.assertEquals(V22ManifestTemplate.class, testJibExtension.getContainer().getFormat()); - Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); + assertThat(testJibExtension.getContainer().getJvmFlags()).isEmpty(); + assertThat(testJibExtension.getContainer().getEnvironment()).isEmpty(); + assertThat(testJibExtension.getContainer().getExtraClasspath()).isEmpty(); + assertThat(testJibExtension.getContainer().getExpandClasspathDependencies()).isFalse(); + assertThat(testJibExtension.getContainer().getMainClass()).isNull(); + assertThat(testJibExtension.getContainer().getArgs()).isNull(); + assertThat(testJibExtension.getContainer().getFormat()).isSameInstanceAs(ImageFormat.Docker); + assertThat(testJibExtension.getContainer().getPorts()).isEmpty(); + assertThat(testJibExtension.getContainer().getLabels().get()).isEmpty(); + assertThat(testJibExtension.getContainer().getAppRoot()).isEmpty(); + assertThat(testJibExtension.getContainer().getFilesModificationTime().get()) + .isEqualTo("EPOCH_PLUS_SECOND"); + assertThat(testJibExtension.getContainer().getCreationTime().get()).isEqualTo("EPOCH"); testJibExtension.container( container -> { container.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); + container.setEnvironment(ImmutableMap.of("var1", "value1", "var2", "value2")); + container.setEntrypoint(Arrays.asList("foo", "bar", "baz")); + container.setExtraClasspath(Arrays.asList("/d1", "/d2", "/d3")); + container.setExpandClasspathDependencies(true); container.setMainClass("mainClass"); container.setArgs(Arrays.asList("arg1", "arg2", "arg3")); container.setPorts(Arrays.asList("1000", "2000-2010", "3000")); container.setFormat(ImageFormat.OCI); + container.setAppRoot("some invalid appRoot value"); + container.getFilesModificationTime().set("some invalid time value"); + container.getCreationTime().set("some other invalid time value"); }); - Assert.assertEquals( - Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getContainer().getJvmFlags()); - Assert.assertEquals("mainClass", testJibExtension.getContainer().getMainClass()); - Assert.assertEquals( - Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getContainer().getArgs()); - Assert.assertEquals( - Arrays.asList("1000", "2000-2010", "3000"), testJibExtension.getContainer().getPorts()); - Assert.assertEquals(OCIManifestTemplate.class, testJibExtension.getContainer().getFormat()); + ContainerParameters container = testJibExtension.getContainer(); + assertThat(container.getEntrypoint()).containsExactly("foo", "bar", "baz").inOrder(); + assertThat(container.getJvmFlags()).containsExactly("jvmFlag1", "jvmFlag2").inOrder(); + assertThat(container.getEnvironment()) + .containsExactly("var1", "value1", "var2", "value2") + .inOrder(); + assertThat(container.getExtraClasspath()).containsExactly("/d1", "/d2", "/d3").inOrder(); + assertThat(testJibExtension.getContainer().getExpandClasspathDependencies()).isTrue(); + assertThat(testJibExtension.getContainer().getMainClass()).isEqualTo("mainClass"); + assertThat(container.getArgs()).containsExactly("arg1", "arg2", "arg3").inOrder(); + assertThat(container.getPorts()).containsExactly("1000", "2000-2010", "3000").inOrder(); + assertThat(container.getFormat()).isSameInstanceAs(ImageFormat.OCI); + assertThat(container.getAppRoot()).isEqualTo("some invalid appRoot value"); + assertThat(container.getFilesModificationTime().get()).isEqualTo("some invalid time value"); + assertThat(container.getCreationTime().get()).isEqualTo("some other invalid time value"); + testJibExtension.container( + extensionContainer -> { + extensionContainer.getFilesModificationTime().set((String) null); + extensionContainer.getCreationTime().set((String) null); + }); + container = testJibExtension.getContainer(); + assertThat(container.getFilesModificationTime().get()).isEqualTo("EPOCH_PLUS_SECOND"); + assertThat(container.getCreationTime().get()).isEqualTo("EPOCH"); } @Test - public void testUseOnlyProjectCache() { - Assert.assertFalse(testJibExtension.getUseOnlyProjectCache()); + public void testSetFormat() { + testJibExtension.container( + container -> { + container.setFormat("OCI"); + }); + ContainerParameters container = testJibExtension.getContainer(); + assertThat(container.getFormat()).isSameInstanceAs(ImageFormat.OCI); + } - testJibExtension.setUseOnlyProjectCache(true); - Assert.assertTrue(testJibExtension.getUseOnlyProjectCache()); + @Test + public void testContainerizingMode() { + assertThat(testJibExtension.getContainerizingMode()).isEqualTo("exploded"); } @Test - public void testHandleDeprecatedParameters() { - testJibExtension.handleDeprecatedParameters(mockLogger); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); + public void testConfigurationName() { + assertThat(testJibExtension.getConfigurationName().get()).isEqualTo("runtimeClasspath"); + } - testJibExtension.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); - testJibExtension.setMainClass("mainClass"); - testJibExtension.setArgs(Arrays.asList("arg1", "arg2", "arg3")); - testJibExtension.setFormat(ImageFormat.OCI); + @Test + public void testExtraDirectories_default() { + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(1); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("src/main/jib")); + assertThat(testJibExtension.getExtraDirectories().getPermissions().get()).isEmpty(); + } - testJibExtension.handleDeprecatedParameters(mockLogger); + @Test + public void testExtraDirectories() { + testJibExtension.extraDirectories( + extraDirectories -> { + extraDirectories.setPaths("test/path"); + }); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(1); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + } + + @Test + public void testExtraDirectories_lazyEvaluation_setFromInto() { + testJibExtension.extraDirectories( + extraDirectories -> + extraDirectories.paths( + paths -> { + ProviderFactory providerFactory = fakeProject.getProviders(); + Provider from = providerFactory.provider(() -> "test/path"); + Provider into = providerFactory.provider(() -> "/target"); + paths.path( + path -> { + path.setFrom(from); + path.setInto(into); + }); + })); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(1); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getInto()) + .isEqualTo("/target"); + } + + @Test + public void testExtraDirectories_withTarget() { + testJibExtension.extraDirectories( + extraDirectories -> + extraDirectories.paths( + paths -> { + paths.path( + path -> { + path.setFrom("test/path"); + path.setInto("/"); + }); + paths.path( + path -> { + path.setFrom("another/path"); + path.setInto("/non/default/target"); + }); + })); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(2); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getInto()).isEqualTo("/"); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("another/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getInto()) + .isEqualTo("/non/default/target"); + } + + @Test + public void testExtraDirectories_fileForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> extraDirectories.setPaths(Paths.get("test/path").toFile())); + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(1); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + } + + @Test + public void testExtraDirectories_stringListForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> extraDirectories.setPaths(Arrays.asList("test/path", "another/path"))); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(2); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("another/path")); + } + + @Test + public void testExtraDirectories_lazyEvaluation_StringListForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> { + ProviderFactory providerFactory = fakeProject.getProviders(); + Provider paths = + providerFactory.provider(() -> Arrays.asList("test/path", "another/path")); + extraDirectories.setPaths(paths); + }); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(2); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("another/path")); + } + + @Test + public void testExtraDirectories_fileListForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> + extraDirectories.setPaths( + Arrays.asList( + Paths.get("test", "path").toFile(), Paths.get("another", "path").toFile()))); + + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(2); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("test/path")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getFrom()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("another/path")); + } + + @Test + public void testDockerClient() { + testJibExtension.dockerClient( + dockerClient -> { + dockerClient.setExecutable("test-executable"); + dockerClient.setEnvironment(ImmutableMap.of("key1", "val1", "key2", "val2")); + }); + + assertThat(testJibExtension.getDockerClient().getExecutablePath()) + .isEqualTo(Paths.get("test-executable")); + assertThat(testJibExtension.getDockerClient().getEnvironment()) + .containsExactly("key1", "val1", "key2", "val2") + .inOrder(); + } + + @Test + public void testOutputFiles() { + testJibExtension.outputPaths( + outputFiles -> { + outputFiles.setDigest("/path/to/digest"); + outputFiles.setImageId("/path/to/id"); + outputFiles.setTar("path/to/tar"); + }); + + assertThat(testJibExtension.getOutputPaths().getDigestPath()) + .isEqualTo(Paths.get("/path/to/digest").toAbsolutePath()); + assertThat(testJibExtension.getOutputPaths().getImageIdPath()) + .isEqualTo(Paths.get("/path/to/id").toAbsolutePath()); + assertThat(testJibExtension.getOutputPaths().getTarPath()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("path/to/tar")); + } + + @Test + public void testSkaffold() { + testJibExtension.skaffold( + skaffold -> { + skaffold.sync(sync -> sync.setExcludes(fakeProject.files("sync1", "sync2"))); + skaffold.watch( + watch -> { + watch.setBuildIncludes(ImmutableList.of("watch1", "watch2")); + watch.setIncludes("watch3"); + watch.setExcludes(ImmutableList.of(new File("watch4"))); + }); + }); + Path root = fakeProject.getRootDir().toPath(); + assertThat(testJibExtension.getSkaffold().getSync().getExcludes()) + .containsExactly( + root.resolve("sync1").toAbsolutePath(), root.resolve("sync2").toAbsolutePath()); + assertThat(testJibExtension.getSkaffold().getWatch().getBuildIncludes()) + .containsExactly( + root.resolve("watch1").toAbsolutePath(), root.resolve("watch2").toAbsolutePath()); + assertThat(testJibExtension.getSkaffold().getWatch().getIncludes()) + .containsExactly(root.resolve("watch3").toAbsolutePath()); + assertThat(testJibExtension.getSkaffold().getWatch().getExcludes()) + .containsExactly(root.resolve("watch4").toAbsolutePath()); + } + + @Test + public void testProperties() { + System.setProperties(new Properties()); + + System.setProperty("jib.from.image", "fromImage"); + assertThat(testJibExtension.getFrom().getImage()).isEqualTo("fromImage"); + System.setProperty("jib.from.credHelper", "credHelper"); + assertThat(testJibExtension.getFrom().getCredHelper().getHelper()).isEqualTo("credHelper"); + + System.setProperty("jib.from.platforms", "linux/amd64,darwin/arm64"); + List platforms = testJibExtension.getFrom().getPlatforms().get(); + assertThat(platforms).hasSize(2); + assertThat(platforms.get(0).getOs()).isEqualTo("linux"); + assertThat(platforms.get(0).getArchitecture()).isEqualTo("amd64"); + assertThat(platforms.get(1).getOs()).isEqualTo("darwin"); + assertThat(platforms.get(1).getArchitecture()).isEqualTo("arm64"); + + System.setProperty("jib.to.image", "toImage"); + assertThat(testJibExtension.getTo().getImage()).isEqualTo("toImage"); + System.setProperty("jib.to.tags", "tag1,tag2,tag3"); + assertThat(testJibExtension.getTo().getTags()).containsExactly("tag1", "tag2", "tag3"); + System.setProperty("jib.to.credHelper", "credHelper"); + assertThat(testJibExtension.getTo().getCredHelper().getHelper()).isEqualTo("credHelper"); + + System.setProperty("jib.container.appRoot", "appRoot"); + assertThat(testJibExtension.getContainer().getAppRoot()).isEqualTo("appRoot"); + System.setProperty("jib.container.args", "arg1,arg2,arg3"); + assertThat(testJibExtension.getContainer().getArgs()) + .containsExactly("arg1", "arg2", "arg3") + .inOrder(); + System.setProperty("jib.container.entrypoint", "entry1,entry2,entry3"); + assertThat(testJibExtension.getContainer().getEntrypoint()) + .containsExactly("entry1", "entry2", "entry3") + .inOrder(); + System.setProperty("jib.container.environment", "env1=val1,env2=val2"); + assertThat(testJibExtension.getContainer().getEnvironment()) + .containsExactly("env1", "val1", "env2", "val2") + .inOrder(); + System.setProperty("jib.container.extraClasspath", "/d1,/d2,/d3"); + assertThat(testJibExtension.getContainer().getExtraClasspath()) + .containsExactly("/d1", "/d2", "/d3") + .inOrder(); + System.setProperty("jib.container.expandClasspathDependencies", "true"); + assertTrue(testJibExtension.getContainer().getExpandClasspathDependencies()); + System.setProperty("jib.container.format", "OCI"); + assertThat(testJibExtension.getContainer().getFormat()).isSameInstanceAs(ImageFormat.OCI); + System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); + assertThat(testJibExtension.getContainer().getJvmFlags()) + .containsExactly("flag1", "flag2", "flag3") + .inOrder(); + System.setProperty("jib.container.labels", "label1=val1,label2=val2"); + assertThat(testJibExtension.getContainer().getLabels().get()) + .containsExactly("label1", "val1", "label2", "val2") + .inOrder(); + System.setProperty("jib.container.mainClass", "main"); + assertThat(testJibExtension.getContainer().getMainClass()).isEqualTo("main"); + System.setProperty("jib.container.ports", "port1,port2,port3"); + assertThat(testJibExtension.getContainer().getPorts()) + .containsExactly("port1", "port2", "port3") + .inOrder(); + System.setProperty("jib.container.user", "myUser"); + assertThat(testJibExtension.getContainer().getUser()).isEqualTo("myUser"); + System.setProperty("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); + testJibExtension + .getContainer() + .getFilesModificationTime() + .set("property should override value"); + assertThat(testJibExtension.getContainer().getFilesModificationTime().get()) + .isEqualTo("2011-12-03T22:42:05Z"); + System.setProperty("jib.container.creationTime", "2011-12-03T11:42:05Z"); + testJibExtension.getContainer().getCreationTime().set("property should override value"); + assertThat(testJibExtension.getContainer().getCreationTime().get()) + .isEqualTo("2011-12-03T11:42:05Z"); + System.setProperty("jib.containerizingMode", "packaged"); + assertThat(testJibExtension.getContainerizingMode()).isEqualTo("packaged"); + + System.setProperty("jib.extraDirectories.paths", "/foo,/bar/baz"); + assertThat(testJibExtension.getExtraDirectories().getPaths()).hasSize(2); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(0).getFrom()) + .isEqualTo(Paths.get("/foo")); + assertThat(testJibExtension.getExtraDirectories().getPaths().get(1).getFrom()) + .isEqualTo(Paths.get("/bar/baz")); + System.setProperty("jib.extraDirectories.permissions", "/foo/bar=707,/baz=456"); + assertThat(testJibExtension.getExtraDirectories().getPermissions().get()) + .containsExactly("/foo/bar", "707", "/baz", "456") + .inOrder(); + + System.setProperty("jib.dockerClient.executable", "test-exec"); + assertThat(testJibExtension.getDockerClient().getExecutablePath()) + .isEqualTo(Paths.get("test-exec")); + System.setProperty("jib.dockerClient.environment", "env1=val1,env2=val2"); + assertThat(testJibExtension.getDockerClient().getEnvironment()) + .containsExactly("env1", "val1", "env2", "val2") + .inOrder(); + } + + @Test + public void testLazyPropertiesFinalization() { + Property filesModificationTime = + testJibExtension.getContainer().getFilesModificationTime(); + filesModificationTime.set((String) null); + filesModificationTime.finalizeValue(); + System.setProperty("jib.container.filesModificationTime", "EPOCH_PLUS_SECOND"); + assertThat(testJibExtension.getContainer().getFilesModificationTime().get()) + .isEqualTo("EPOCH_PLUS_SECOND"); + Property creationTime = testJibExtension.getContainer().getCreationTime(); + creationTime.set((String) null); + creationTime.finalizeValue(); + System.setProperty("jib.container.creationTime", "EPOCH"); + assertThat(testJibExtension.getContainer().getCreationTime().get()).isEqualTo("EPOCH"); + } + + @Test + public void testSystemPropertiesWithInvalidPlatform() { + System.setProperty("jib.from.platforms", "linux /amd64"); + assertThrows(IllegalArgumentException.class, testJibExtension.getFrom()::getPlatforms); + } + + @Test + public void testPropertiesOutputPaths() { + System.setProperties(new Properties()); + // Absolute paths + System.setProperty("jib.outputPaths.digest", "/digest/path"); + assertThat(testJibExtension.getOutputPaths().getDigestPath()) + .isEqualTo(Paths.get("/digest/path").toAbsolutePath()); + System.setProperty("jib.outputPaths.imageId", "/id/path"); + assertThat(testJibExtension.getOutputPaths().getImageIdPath()) + .isEqualTo(Paths.get("/id/path").toAbsolutePath()); + System.setProperty("jib.outputPaths.tar", "/tar/path"); + assertThat(testJibExtension.getOutputPaths().getTarPath()) + .isEqualTo(Paths.get("/tar/path").toAbsolutePath()); + // Relative paths + System.setProperty("jib.outputPaths.digest", "digest/path"); + assertThat(testJibExtension.getOutputPaths().getDigestPath()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("digest/path")); + System.setProperty("jib.outputPaths.imageId", "id/path"); + assertThat(testJibExtension.getOutputPaths().getImageIdPath()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("id/path")); + System.setProperty("jib.outputPaths.tar", "tar/path"); + assertThat(testJibExtension.getOutputPaths().getTarPath()) + .isEqualTo(fakeProject.getProjectDir().toPath().resolve("tar/path")); + System.setProperty("jib.configurationName", "myConfiguration"); + assertThat(testJibExtension.getConfigurationName().get()).isEqualTo("myConfiguration"); + } - String expectedOutput = - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your build.gradle to avoid issues in the future:\n" - + " jvmFlags -> container.jvmFlags\n" - + " mainClass -> container.mainClass\n" - + " args -> container.args\n" - + " format -> container.format\n" - + "You may also wrap the parameters in a container{} block."; - Mockito.verify(mockLogger).warn(expectedOutput); - Assert.assertEquals(Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getJvmFlags()); - Assert.assertEquals("mainClass", testJibExtension.getMainClass()); - Assert.assertEquals(Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getArgs()); - Assert.assertEquals(OCIManifestTemplate.class, testJibExtension.getFormat()); + private TargetImageParameters generateTargetImageParametersWithTags(String... tags) { + HashSet set = new HashSet<>(Arrays.asList(tags)); + testJibExtension.to(to -> to.setTags(set)); + return testJibExtension.getTo(); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index c92a4d7276..fe2829d0dd 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -16,13 +16,30 @@ package com.google.cloud.tools.jib.gradle; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.truth.Correspondence; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Set; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.UnknownTaskException; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; +import org.gradle.testfixtures.ProjectBuilder; +import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; -import org.junit.Assert; +import org.junit.After; +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -30,42 +47,388 @@ /** Tests for {@link JibPlugin}. */ public class JibPluginTest { - @Rule public TemporaryFolder testProjectRoot = new TemporaryFolder(); + private static final ImmutableList KNOWN_JIB_TASKS = + ImmutableList.of( + JibPlugin.BUILD_IMAGE_TASK_NAME, + JibPlugin.BUILD_DOCKER_TASK_NAME, + JibPlugin.BUILD_TAR_TASK_NAME); + + private static final Correspondence PROVIDES_TASK_OF = + Correspondence.from( + (object, task) -> + object instanceof TaskProvider && ((TaskProvider) object).get().equals(task), + "provides task of"); + + private static boolean isJava8Runtime() { + return System.getProperty("java.version").startsWith("1.8."); + } + + @Rule public final TemporaryFolder testProjectRoot = new TemporaryFolder(); + @Rule public final TestProject testProject = new TestProject("lazy-evaluation"); + + @After + public void tearDown() { + System.clearProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME); + } @Test - public void testCheckGradleVersion_pass() { - GradleRunner.create() - .withProjectDir(testProjectRoot.getRoot()) - .withPluginClasspath() - .withGradleVersion(JibPlugin.GRADLE_MIN_VERSION.getVersion()) - .build(); - // pass + public void testCheckGradleVersion_pass() throws IOException { + Assume.assumeTrue(isJava8Runtime()); + + // Copy build file to temp dir + Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); + InputStream buildFileContent = + getClass().getClassLoader().getResourceAsStream("gradle/plugin-test/build.gradle"); + Files.copy(buildFileContent, buildFile); + + BuildResult result = + GradleRunner.create() + .withProjectDir(testProjectRoot.getRoot()) + .withPluginClasspath() + .withGradleVersion(JibPlugin.GRADLE_MIN_VERSION.getVersion()) + .build(); + assertThat(result).isNotNull(); } @Test public void testCheckGradleVersion_fail() throws IOException { - try { - // Copy build file to temp dir - Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); - InputStream buildFileContent = - getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); - Files.copy(buildFileContent, buildFile); - - GradleRunner.create() - .withProjectDir(testProjectRoot.getRoot()) - .withPluginClasspath() - .withGradleVersion("4.3") - .build(); - Assert.fail(); - } catch (UnexpectedBuildFailure ex) { - Assert.assertTrue( - ex.getMessage() - .contains( - "Detected Gradle 4.3, but jib requires " - + JibPlugin.GRADLE_MIN_VERSION - + " or higher. You can upgrade by running 'gradle wrapper --gradle-version=" - + JibPlugin.GRADLE_MIN_VERSION.getVersion() - + "'.")); + Assume.assumeTrue(isJava8Runtime()); + + // Copy build file to temp dir + Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); + InputStream buildFileContent = + getClass().getClassLoader().getResourceAsStream("gradle/plugin-test/build.gradle"); + Files.copy(buildFileContent, buildFile); + + GradleRunner gradleRunner = + GradleRunner.create() + .withProjectDir(testProjectRoot.getRoot()) + .withPluginClasspath() + .withGradleVersion("4.3"); + + Exception exception = assertThrows(UnexpectedBuildFailure.class, () -> gradleRunner.build()); + assertThat(exception) + .hasMessageThat() + .contains( + "Detected Gradle 4.3, but jib requires " + + JibPlugin.GRADLE_MIN_VERSION + + " or higher. You can upgrade by running 'gradle wrapper --gradle-version=" + + JibPlugin.GRADLE_MIN_VERSION.getVersion() + + "'."); + } + + @Test + public void testCheckJibVersionNames() { + // These identifiers will be baked into Skaffold and should not be changed + assertThat(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME).isEqualTo("jib.requiredVersion"); + assertThat(JibPlugin.SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME) + .isEqualTo("_skaffoldFailIfJibOutOfDate"); + } + + @Test + public void testCheckJibVersionInvoked() { + Project project = createProject(); + System.setProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME, "10000.0"); // not here yet + + Exception exception = + assertThrows( + GradleException.class, + () -> project.getPluginManager().apply("com.google.cloud.tools.jib")); + // Gradle tests aren't run from a jar and so don't have an identifiable plugin version + assertThat(exception) + .hasMessageThat() + .isEqualTo("Failed to apply plugin 'com.google.cloud.tools.jib'."); + assertThat(exception.getCause()) + .hasMessageThat() + .isEqualTo("Could not determine Jib plugin version"); + } + + @Test + public void testWebAppProject() { + Project project = createProject("java", "war", "com.google.cloud.tools.jib"); + + TaskContainer tasks = project.getTasks(); + Task warTask = tasks.getByPath(":war"); + assertThat(warTask).isNotNull(); + + for (String taskName : KNOWN_JIB_TASKS) { + Set taskDependencies = tasks.getByPath(taskName).getDependsOn(); + assertThat(taskDependencies).comparingElementsUsing(PROVIDES_TASK_OF).contains(warTask); + } + } + + @Test + public void testWebAppProject_bootWar() { + Project project = + createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); + + TaskContainer tasks = project.getTasks(); + Task warTask = tasks.getByPath(":war"); + Task bootWarTask = tasks.getByPath(":bootWar"); + assertThat(warTask).isNotNull(); + assertThat(bootWarTask).isNotNull(); + + for (String taskName : KNOWN_JIB_TASKS) { + Set taskDependencies = tasks.getByPath(taskName).getDependsOn(); + assertThat(taskDependencies) + .comparingElementsUsing(PROVIDES_TASK_OF) + .containsAtLeast(warTask, bootWarTask); + } + } + + @Test + public void testWebAppProject_bootWarDisabled() { + Project project = + createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); + TaskContainer tasks = project.getTasks(); + // should depend on bootWar even if disabled + tasks.named("bootWar").configure(task -> task.setEnabled(false)); + + Task warTask = tasks.getByPath(":war"); + Task bootWarTask = tasks.getByPath(":bootWar"); + assertThat(warTask).isNotNull(); + assertThat(bootWarTask).isNotNull(); + + for (String taskName : KNOWN_JIB_TASKS) { + Set taskDependencies = tasks.getByPath(taskName).getDependsOn(); + assertThat(taskDependencies) + .comparingElementsUsing(PROVIDES_TASK_OF) + .containsAtLeast(warTask, bootWarTask); } } + + @Test + public void testSpringBootJarProject_nonPackagedMode() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isFalse(); + assertThat(jar.getArchiveClassifier().get()).isEmpty(); + } + + @Test + public void testSpringBootJarProject_packagedMode() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(jar.getArchiveClassifier().get()).isEqualTo("original"); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(jar.getArchiveClassifier().get()).isEqualTo("jar-classifier"); + } + + @Test + public void testSpringBootJarProject_packagedMode_bootJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider bootJarTask = project.getTasks().named("bootJar"); + bootJarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("boot-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(jar.getArchiveClassifier().get()).isEmpty(); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabled() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + project.getTasks().named("jar").configure(task -> task.setEnabled(true)); + + TaskContainer tasks = project.getTasks(); + Exception exception = assertThrows(GradleException.class, () -> tasks.getByPath(":jar")); + assertThat(exception.getCause()) + .hasMessageThat() + .startsWith( + "Both 'bootJar' and 'jar' tasks are enabled, but they write their jar file into the " + + "same location at "); + assertThat(exception.getCause()) + .hasMessageThat() + .endsWith("root.jar. Did you forget to set 'archiveClassifier' on either task?"); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> task.setEnabled(true)); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(jar.getArchiveClassifier().get()).isEqualTo("jar-classifier"); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndBootJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider bootJarTask = project.getTasks().named("bootJar"); + bootJarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("boot-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(jar.getArchiveClassifier().get()).isEmpty(); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndBootJarDisabled() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + project.getTasks().named("jar").configure(task -> task.setEnabled(true)); + project.getTasks().named("bootJar").configure(task -> task.setEnabled(false)); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(project.getTasks().getByPath(":bootJar").getEnabled()).isFalse(); + assertThat(jar.getArchiveClassifier().get()).isEmpty(); + } + + @Test + public void + testSpringBootJarProject_packagedMode_jarEnabledAndBootJarDisabledAndJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> task.setEnabled(true)); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + project.getTasks().named("bootJar").configure(task -> task.setEnabled(false)); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + assertThat(jar.getEnabled()).isTrue(); + assertThat(project.getTasks().getByPath(":bootJar").getEnabled()).isFalse(); + assertThat(jar.getArchiveClassifier().get()).isEqualTo("jar-classifier"); + } + + @Test + public void testNonWebAppProject() { + Project project = createProject("java", "com.google.cloud.tools.jib"); + + TaskContainer tasks = project.getTasks(); + Exception exception = assertThrows(UnknownTaskException.class, () -> tasks.getByPath(":war")); + assertThat(exception).hasMessageThat().isNotNull(); + } + + @Test + public void testJibTaskGroupIsSet() { + Project project = createProject("java", "com.google.cloud.tools.jib"); + + TaskContainer tasks = project.getTasks(); + KNOWN_JIB_TASKS.forEach( + taskName -> assertThat(tasks.getByPath(taskName).getGroup()).isEqualTo("Jib")); + } + + @Test + public void testLazyEvalForImageAndTags() { + UnexpectedBuildFailure exception = + assertThrows( + UnexpectedBuildFailure.class, + () -> testProject.build(JibPlugin.BUILD_IMAGE_TASK_NAME, "-Djib.console=plain")); + + String output = exception.getBuildResult().getOutput(); + assertThat(output) + .contains( + "Containerizing application to updated-image, updated-image:updated-tag, updated-image:tag2"); + } + + @Test + public void testLazyEvalForLabels() { + BuildResult showLabels = testProject.build("showlabels", "-Djib.console=plain"); + assertThat(showLabels.getOutput()) + .contains( + "labels contain values [firstkey:updated-first-label, secondKey:updated-second-label]"); + } + + @Test + public void testLazyEvalForEntryPoint() { + BuildResult showEntrypoint = testProject.build("showentrypoint", "-Djib.console=plain"); + assertThat(showEntrypoint.getOutput()).contains("entrypoint contains updated"); + } + + @Test + public void testLazyEvalForExtraDirectories() { + BuildResult checkExtraDirectories = + testProject.build("check-extra-directories", "-Djib.console=plain"); + assertThat(checkExtraDirectories.getOutput()).contains("[/updated:755]"); + assertThat(checkExtraDirectories.getOutput()).contains("updated-custom-extra-dir"); + } + + @Test + public void testLazyEvalForExtraDirectories_individualPaths() throws IOException { + BuildResult checkExtraDirectories = + testProject.build( + "check-extra-directories", "-b=build-extra-dirs.gradle", "-Djib.console=plain"); + + Path extraDirectoryPath = + testProject + .getProjectRoot() + .resolve("src") + .resolve("main") + .resolve("updated-custom-extra-dir") + .toRealPath(); + assertThat(checkExtraDirectories.getOutput()) + .contains("extraDirectories (from): [" + extraDirectoryPath + "]"); + assertThat(checkExtraDirectories.getOutput()) + .contains("extraDirectories (into): [/updated-custom-into-dir]"); + assertThat(checkExtraDirectories.getOutput()) + .contains("extraDirectories (includes): [[include.txt]]"); + assertThat(checkExtraDirectories.getOutput()) + .contains("extraDirectories (excludes): [[exclude.txt]]"); + } + + @Test + public void testLazyEvalForContainerCreationAndFileModificationTimes() { + BuildResult showTimes = testProject.build("showtimes", "-Djib.console=plain"); + String output = showTimes.getOutput(); + assertThat(output).contains("creationTime=2022-07-19T10:23:42Z"); + assertThat(output).contains("filesModificationTime=2022-07-19T11:23:42Z"); + } + + @Test + public void testLazyEvalForMainClass() { + BuildResult showLabels = testProject.build("showMainClass"); + assertThat(showLabels.getOutput()).contains("mainClass value updated"); + } + + @Test + public void testLazyEvalForJvmFlags() { + BuildResult showLabels = testProject.build("showJvmFlags"); + assertThat(showLabels.getOutput()).contains("jvmFlags value [updated]"); + } + + private Project createProject(String... plugins) { + Project project = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + Arrays.asList(plugins).forEach(project.getPluginManager()::apply); + return project; + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java new file mode 100644 index 0000000000..aa76ba7653 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.common.util.concurrent.Futures; +import java.util.Optional; +import java.util.concurrent.Future; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.War; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.boot.gradle.plugin.SpringBootPlugin; +import org.springframework.boot.gradle.tasks.bundling.BootWar; + +/** Tests for {@link TaskCommon}. */ +@RunWith(MockitoJUnitRunner.class) +public class TaskCommonTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private ProjectProperties mockProjectProperties; + + @Before + public void setUp() { + System.clearProperty("jib.extraDirectories.paths"); + System.clearProperty("jib.extraDirectories.permissions"); + } + + @Test + public void testGetWarTask_normalJavaProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(JavaPlugin.class); + + TaskProvider warProviderTask = TaskCommon.getWarTaskProvider(project); + assertThat(warProviderTask).isNull(); + } + + @Test + public void testGetWarTask_normalWarProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(WarPlugin.class); + + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); + assertThat(warTask).isNotNull(); + assertThat(warTask.get()).isInstanceOf(War.class); + } + + @Test + public void testGetBootWarTask_bootWarProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(WarPlugin.class); + project.getPlugins().apply(SpringBootPlugin.class); + + TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + assertThat(bootWarTask).isNotNull(); + assertThat(bootWarTask.get()).isInstanceOf(BootWar.class); + } + + @Test + public void testFinishUpdateChecker_correctMessageLogged() { + when(mockProjectProperties.getToolName()).thenReturn("tool-name"); + when(mockProjectProperties.getToolVersion()).thenReturn("2.0.0"); + Future> updateCheckFuture = Futures.immediateFuture(Optional.of("2.1.0")); + TaskCommon.finishUpdateChecker(mockProjectProperties, updateCheckFuture); + + verify(mockProjectProperties) + .log( + LogEvent.lifecycle( + "\n\u001B[33mA new version of tool-name (2.1.0) is available (currently using 2.0.0). " + + "Update your build configuration to use the latest features and fixes!\n" + + ProjectInfo.GITHUB_URL + + "/blob/master/jib-gradle-plugin/CHANGELOG.md\u001B[0m\n\n" + + "Please see " + + ProjectInfo.GITHUB_URL + + "/blob/master/docs/privacy.md for info on disabling this update check.\n")); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java similarity index 74% rename from jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index 966c9ea421..ca3f6aed78 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -24,22 +24,22 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.junit.rules.TemporaryFolder; // TODO: Consolidate with TestProject in jib-maven-plugin. /** Works with the test Gradle projects in the {@code resources/projects} directory. */ -class TestProject extends TemporaryFolder implements Closeable { +public class TestProject extends TemporaryFolder implements Closeable { - private static final String PROJECTS_PATH_IN_RESOURCES = "projects/"; + private static final String PROJECTS_PATH_IN_RESOURCES = "gradle/projects/"; /** Copies test project {@code projectName} to {@code destination} folder. */ private static void copyProject(String projectName, Path destination) throws IOException, URISyntaxException { Path projectPathInResources = Paths.get(Resources.getResource(PROJECTS_PATH_IN_RESOURCES + projectName).toURI()); - // TODO: Consolidate with DockerContextMojo#copyFiles. new DirectoryWalker(projectPathInResources) .filterRoot() .walk( @@ -55,12 +55,13 @@ private static void copyProject(String projectName, Path destination) } private final String testProjectName; + private String gradleVersion = JibPlugin.GRADLE_MIN_VERSION.getVersion(); private GradleRunner gradleRunner; private Path projectRoot; /** Initialize with a specific project directory. */ - TestProject(String testProjectName) { + public TestProject(String testProjectName) { this.testProjectName = testProjectName; } @@ -76,14 +77,27 @@ protected void before() throws Throwable { projectRoot = newFolder().toPath(); copyProject(testProjectName, projectRoot); - gradleRunner = GradleRunner.create().withProjectDir(projectRoot.toFile()).withPluginClasspath(); + gradleRunner = + GradleRunner.create() + .withGradleVersion(gradleVersion) + .withProjectDir(projectRoot.toFile()) + .withPluginClasspath(); } - BuildResult build(String... gradleArguments) { + public TestProject withGradleVersion(String version) { + gradleVersion = version; + return this; + } + + public BuildResult build(String... gradleArguments) { + return gradleRunner.withArguments(gradleArguments).build(); + } + + public BuildResult build(List gradleArguments) { return gradleRunner.withArguments(gradleArguments).build(); } - Path getProjectRoot() { + public Path getProjectRoot() { return projectRoot; } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java new file mode 100644 index 0000000000..3df3a86154 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import javax.annotation.Nullable; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link FilesTaskV2}. */ +public class FilesTaskV2Test { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule + public static final TestProject skaffoldTestProject = new TestProject("skaffold-config"); + + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + + @ClassRule + public static final TestProject platformProject = + new TestProject("platform").withGradleVersion("5.2"); + + /** + * Verifies that the files task succeeded and returns the list of paths it prints out. + * + * @param project the project to run the task on + * @param moduleName the name of the sub-project, or {@code null} if no sub-project + * @return the JSON string printed by the task + */ + private static String verifyTaskSuccess(TestProject project, @Nullable String moduleName) { + String taskName = + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_FILES_TASK_V2_NAME; + BuildResult buildResult = project.build(taskName, "-q", "-D_TARGET_IMAGE=ignored"); + BuildTask jibTask = buildResult.task(taskName); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + String output = buildResult.getOutput().trim(); + MatcherAssert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); + + // Return task output with header removed + return output.replace("BEGIN JIB JSON", "").trim(); + } + + /** + * Asserts that two lists contain the same paths. Required to avoid Mac's /var/ vs. /private/var/ + * symlink issue. + * + * @param expected the expected list of paths + * @param actual the actual list of paths + * @throws IOException if checking if two files are the same fails + */ + private static void assertPathListsAreEqual(List expected, List actual) + throws IOException { + Assert.assertEquals(expected.size(), actual.size()); + for (int index = 0; index < expected.size(); index++) { + Assert.assertEquals( + expected.get(index).toRealPath(), Paths.get(actual.get(index)).toRealPath()); + } + } + + @Test + public void testFilesTask_singleProject() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(simpleTestProject, null)); + assertPathListsAreEqual( + ImmutableList.of(projectRoot.resolve("build.gradle")), result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/custom-extra-dir")), + result.getInputs()); + assertThat(result.getIgnore()).isEmpty(); + } + + @Test + public void testFilesTask_multiProjectSimpleService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path simpleServiceRoot = projectRoot.resolve("simple-service"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(multiTestProject, "simple-service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + simpleServiceRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of(simpleServiceRoot.resolve("src/main/java")), result.getInputs()); + assertThat(result.getIgnore()).isEmpty(); + } + + @Test + public void testFilesTask_multiProjectComplexService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(multiTestProject, "complex-service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + complexServiceRoot.resolve("build.gradle"), + libRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of( + complexServiceRoot.resolve("src/main/extra-resources-1"), + complexServiceRoot.resolve("src/main/extra-resources-2"), + complexServiceRoot.resolve("src/main/java"), + complexServiceRoot.resolve("src/main/other-jib"), + libRoot.resolve("src/main/resources"), + libRoot.resolve("src/main/java"), + complexServiceRoot.resolve( + "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar")), + result.getInputs()); + assertThat(result.getIgnore()).isEmpty(); + } + + @Test + public void testFilesTask_platformProject() throws IOException { + Path projectRoot = platformProject.getProjectRoot(); + Path platformRoot = projectRoot.resolve("platform"); + Path serviceRoot = projectRoot.resolve("service"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(platformProject, "service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + serviceRoot.resolve("build.gradle"), + platformRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of(serviceRoot.resolve("src/main/java")), result.getInputs()); + assertThat(result.getIgnore()).isEmpty(); + } + + @Test + public void testFilesTast_withConfigModifiers() throws IOException { + Path projectRoot = skaffoldTestProject.getProjectRoot(); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(skaffoldTestProject, null)); + assertPathListsAreEqual( + ImmutableList.of(projectRoot.resolve("build.gradle"), projectRoot.resolve("script.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/jib"), + projectRoot.resolve("other/file.txt")), + result.getInputs()); + assertPathListsAreEqual( + ImmutableList.of(projectRoot.resolve("src/main/jib/bar")), result.getIgnore()); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java new file mode 100644 index 0000000000..9aa3e8fe72 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link InitTask}. */ +public class InitTaskTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + + /** + * Verifies that the files task succeeded and returns the list of JSON strings printed by the + * task. + * + * @param project the project to run the task on + * @return the JSON strings printed by the task + */ + private static List getJsons(TestProject project) { + BuildResult buildResult = + project.build(JibPlugin.SKAFFOLD_INIT_TASK_NAME, "-q", "-D_TARGET_IMAGE=testimage"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.SKAFFOLD_INIT_TASK_NAME); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + String output = buildResult.getOutput().trim(); + MatcherAssert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); + + Pattern pattern = Pattern.compile("BEGIN JIB JSON\r?\n(\\{.*})"); + Matcher matcher = pattern.matcher(output); + List jsons = new ArrayList<>(); + while (matcher.find()) { + jsons.add(matcher.group(1)); + } + + // Return task output with header removed + return jsons; + } + + @Test + public void testFilesTask_singleProject() throws IOException { + List outputs = getJsons(simpleTestProject); + Assert.assertEquals(1, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertNull(skaffoldInitOutput.getProject()); + } + + @Test + public void testFilesTask_multiProject() throws IOException { + List outputs = getJsons(multiTestProject); + Assert.assertEquals(2, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("complex-service", skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("simple-service", skaffoldInitOutput.getProject()); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java new file mode 100644 index 0000000000..837b5dd081 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate.FileTemplate; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link SyncMapTask}. */ +public class SyncMapTaskTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + @ClassRule public static final TestProject skaffoldProject = new TestProject("skaffold-config"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + @ClassRule public static final TestProject warProject = new TestProject("war_servlet25"); + + /** + * Verifies that the sync map task succeeded and returns the parsed json. + * + * @param project the project to run the task on + * @param moduleName the name of the sub-project, or {@code null} if no sub-project + * @param params extra gradle cli params to use during the build + * @return the list of paths printed by the task + * @throws IOException if the json parser fails + */ + private static SkaffoldSyncMapTemplate generateTemplate( + TestProject project, @Nullable String moduleName, @Nullable List params) + throws IOException { + String taskName = + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_SYNC_MAP_TASK_NAME; + List buildParams = new ArrayList<>(); + buildParams.add(taskName); + buildParams.add("-q"); + buildParams.add("-D_TARGET_IMAGE=ignored"); + buildParams.add("--stacktrace"); + if (params != null) { + buildParams.addAll(params); + } + BuildResult buildResult = project.build(buildParams); + BuildTask jibTask = buildResult.task(taskName); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + + List outputLines = + Splitter.on(System.lineSeparator()).omitEmptyStrings().splitToList(buildResult.getOutput()); + Assert.assertEquals(2, outputLines.size()); + Assert.assertEquals("BEGIN JIB JSON: SYNCMAP/1", outputLines.get(0)); + return SkaffoldSyncMapTemplate.from(outputLines.get(1)); + } + + private static void assertFilePaths(Path src, AbsoluteUnixPath dest, FileTemplate template) + throws IOException { + Assert.assertEquals(src.toRealPath().toString(), template.getSrc()); + Assert.assertEquals(dest.toString(), template.getDest()); + } + + @Test + public void testSyncMapTask_singleProject() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + SkaffoldSyncMapTemplate parsed = generateTemplate(simpleTestProject, null, null); + + List generated = parsed.getGenerated(); + Assert.assertEquals(2, generated.size()); + assertFilePaths( + projectRoot.resolve("build/resources/main/world"), + AbsoluteUnixPath.get("/app/resources/world"), + generated.get(0)); + assertFilePaths( + projectRoot.resolve("build/classes/java/main/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(1)); + + List direct = parsed.getDirect(); + Assert.assertEquals(2, direct.size()); + assertFilePaths( + projectRoot.resolve("src/main/custom-extra-dir/bar/cat"), + AbsoluteUnixPath.get("/bar/cat"), + direct.get(0)); + assertFilePaths( + projectRoot.resolve("src/main/custom-extra-dir/foo"), + AbsoluteUnixPath.get("/foo"), + direct.get(1)); + } + + @Test + public void testSyncMapTask_multiProjectOutput() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + SkaffoldSyncMapTemplate parsed = generateTemplate(multiTestProject, "complex-service", null); + + List generated = parsed.getGenerated(); + Assert.assertEquals(4, generated.size()); + assertFilePaths( + libRoot.resolve("build/libs/lib.jar"), + AbsoluteUnixPath.get("/app/libs/lib.jar"), + generated.get(0)); + assertFilePaths( + complexServiceRoot.resolve("build/resources/main/resource1.txt"), + AbsoluteUnixPath.get("/app/resources/resource1.txt"), + generated.get(1)); + assertFilePaths( + complexServiceRoot.resolve("build/resources/main/resource2.txt"), + AbsoluteUnixPath.get("/app/resources/resource2.txt"), + generated.get(2)); + assertFilePaths( + complexServiceRoot.resolve("build/classes/java/main/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(3)); + + List direct = parsed.getDirect(); + Assert.assertEquals(2, direct.size()); + assertFilePaths( + complexServiceRoot.resolve( + "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + AbsoluteUnixPath.get("/app/libs/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + direct.get(0)); + assertFilePaths( + complexServiceRoot.resolve("src/main/other-jib/extra-file"), + AbsoluteUnixPath.get("/extra-file"), + direct.get(1)); + } + + @Test + public void testSyncMapTask_withSkaffoldConfig() throws IOException { + Path projectRoot = skaffoldProject.getProjectRoot(); + SkaffoldSyncMapTemplate parsed = generateTemplate(skaffoldProject, null, null); + + List generated = parsed.getGenerated(); + Assert.assertEquals(2, generated.size()); + assertFilePaths( + projectRoot.resolve("build/resources/main/world"), + AbsoluteUnixPath.get("/app/resources/world"), + generated.get(0)); + assertFilePaths( + projectRoot.resolve("build/classes/java/main/com/test2/GoodbyeWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test2/GoodbyeWorld.class"), + generated.get(1)); + // classes/java/main/com/test is ignored + + List direct = parsed.getDirect(); + Assert.assertEquals(1, direct.size()); + assertFilePaths( + projectRoot.resolve("src/main/jib/bar/cat"), + AbsoluteUnixPath.get("/bar/cat"), + direct.get(0)); + // src/main/custom-extra-dir/foo is ignored + } + + @Test + public void testSyncMapTask_failIfWar() throws IOException { + Path projectRoot = warProject.getProjectRoot(); + try { + generateTemplate(warProject, null, null); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "org.gradle.api.GradleException: Skaffold sync is currently only available for 'jar' style Jib projects, but the project " + + projectRoot.getFileName() + + " is configured to generate a 'war'")); + } + } + + @Test + public void testSyncMapTask_failIfJarContainerizationMode() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + try { + generateTemplate( + simpleTestProject, null, ImmutableList.of("-Djib.containerizingMode=packaged")); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of " + + projectRoot.getFileName() + + " is 'packaged'")); + } + } +} diff --git a/jib-gradle-plugin/src/test/resources/application/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/gradle/application/build/classes/java/main/HelloWorld.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/classes/HelloWorld.class rename to jib-gradle-plugin/src/test/resources/gradle/application/build/classes/java/main/HelloWorld.class diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/build/classes/java/main/some.class b/jib-gradle-plugin/src/test/resources/gradle/application/build/classes/java/main/some.class new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceA b/jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceA new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceB b/jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceB new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/world similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/resources/world rename to jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/world diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/another/one/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/another/one/dependency-1.0.0.jar new file mode 100644 index 0000000000..ca2ad440bb Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/another/one/dependency-1.0.0.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependency-1.0.0.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependency-1.0.0.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/library.jarC.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/library.jarC.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryA.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryA.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryB.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryB.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar new file mode 100644 index 0000000000..4d14054965 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar @@ -0,0 +1,2 @@ +]e$Ềx, +.3I݅38VKAM)=5~'qю$[- :&% Eo7Ns`iZ0MT.9J[}?\E }UvJdo(i"Mԛ_+/cI.-O=Hi \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/application/extra-directory/foo b/jib-gradle-plugin/src/test/resources/gradle/application/extra-directory/foo new file mode 100644 index 0000000000..85e4ff84b7 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/application/extra-directory/foo @@ -0,0 +1 @@ +Unused file for committing extra directory \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/plugin-test/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/plugin-test/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/plugin-test/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/plugin-test/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle new file mode 100644 index 0000000000..0b8f910658 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle @@ -0,0 +1 @@ +// this file doesn't do anything \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle new file mode 100644 index 0000000000..3f84836125 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + flatDir { + dirs 'libs' + } +} + +jib { + from { + image = 'scratch' + } + extraDirectories { + paths = file('src/main/other-jib') + } +} + +sourceSets { + main { + resources { + srcDirs 'src/main/extra-resources-1', 'src/main/extra-resources-2' + } + } +} + +dependencies { + implementation project(':lib') + implementation name: 'dependency-1.0.0' + implementation name: 'dependencyX-1.0.0-SNAPSHOT' +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle new file mode 100644 index 0000000000..313ac21fc2 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java similarity index 67% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java index 1bfe1caae5..0873623756 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,12 +14,12 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.lib; -/** Thrown when a registry did not respond. */ -public class RegistryNoResponseException extends RegistryException { +/** Shared Code! */ +public class Lib { - RegistryNoResponseException(Throwable cause) { - super(cause); + public String getThing() { + return "thing"; } } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt new file mode 100644 index 0000000000..32f95c0d12 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle new file mode 100644 index 0000000000..748c7dbf3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle @@ -0,0 +1,3 @@ +include ':simple-service' +include ':complex-service' +include ':lib' \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle new file mode 100644 index 0000000000..cbe32f50ee --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + from { + image = 'scratch' + } + // only use buildTar +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build-extra-dirs.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build-extra-dirs.gradle new file mode 100644 index 0000000000..628eb328d4 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build-extra-dirs.gradle @@ -0,0 +1,47 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.google.guava:guava:23.6-jre' +} + +project.ext.value = 'original' + +project.afterEvaluate { + project.ext.value = 'updated' + project.ext.getCustomIncludes = { -> return ['include.txt'] } + project.ext.getCustomExcludes = { -> return ['exclude.txt'] } +} + +jib { + extraDirectories { + paths { + path { + from = project.provider { 'src/main/' + project.ext.value + '-custom-extra-dir' } + into = project.provider { '/' + project.ext.value + '-custom-into-dir' } + includes = project.provider { -> project.ext.getCustomIncludes() } + excludes = project.provider { -> project.ext.getCustomExcludes() } + } + } + } +} + +tasks.register('check-extra-directories') { + List from = project.extensions.getByName('jib')['extraDirectories']['paths'].collect{ path -> path['from']} + List into = project.extensions.getByName('jib')['extraDirectories']['paths'].collect{ path -> path['into']} + List includes = project.extensions.getByName('jib')['extraDirectories']['paths'].collect{ path -> path['includes'].get()} + List excludes = project.extensions.getByName('jib')['extraDirectories']['paths'].collect{ path -> path['excludes'].get()} + println('extraDirectories (from): ' + from) + println('extraDirectories (into): ' + into) + println('extraDirectories (includes): ' + includes) + println('extraDirectories (excludes): ' + excludes) +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build.gradle new file mode 100644 index 0000000000..621baa34b5 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build.gradle @@ -0,0 +1,84 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.google.guava:guava:23.6-jre' +} + +project.ext.value = 'original' +project.ext.jibCreationTime = '1970-01-23T00:23:42Z' +project.ext.jibFilesModificationTime = '1970-01-23T01:23:42Z' + +project.afterEvaluate { + project.ext.value = 'updated' + project.ext.getCustomPermissions = { -> return ['/updated': '755'] } + project.ext.jibCreationTime = '2022-07-19T10:23:42Z' + project.ext.jibFilesModificationTime = '2022-07-19T11:23:42Z' +} + +jib { + to { + image = project.provider { project.ext.value + '-image' } + tags = project.provider { [project.ext.value + '-tag', 'tag2'] } + } + container { + labels = project.provider { + [ + firstkey : project.ext.value + '-first-label', + secondKey: project.ext.value + '-second-label' + ] + } + entrypoint = project.provider { [project.ext.value] } + creationTime = project.provider { project.ext.jibCreationTime } + filesModificationTime = project.provider { project.ext.jibFilesModificationTime } + mainClass = project.provider { project.ext.value } + jvmFlags = project.provider { [project.ext.value] } + } + extraDirectories { + paths = project.provider { ['src/main/' + project.ext.value + '-custom-extra-dir'] } + permissions = project.provider { -> project.ext.getCustomPermissions() } + } +} + +tasks.register('showlabels') { + Map prop = project.extensions.getByName('jib')['container']['labels'].get() + println('labels contain values ' + prop) +} + +tasks.register('showentrypoint') { + List prop = project.extensions.getByName('jib')['container'].getEntrypoint() + println('entrypoint contains ' + prop.join(",")) +} + +tasks.register('check-extra-directories') { + List paths = project.extensions.getByName('jib')['extraDirectories']['paths'].collect{ path -> path['from']} + Map permissions = project.extensions.getByName('jib')['extraDirectories']['permissions'].get() + println('extraDirectories paths: ' + paths) + println('extraDirectories permissions: ' + permissions) +} + +tasks.register('showtimes') { + String prop = project.extensions.jib.container.creationTime.get() + println('creationTime=' + prop) + prop = project.extensions.jib.container.filesModificationTime.get() + println('filesModificationTime=' + prop) +} + +tasks.register('showMainClass') { + String prop = project.extensions.jib.container.mainClass + println('mainClass value ' + prop) +} + +tasks.register('showJvmFlags') { + List prop = project.extensions.jib.container.jvmFlags + println('jvmFlags value ' + prop) +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..de34de9cb5 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +import java.io.IOException; +import java.net.URISyntaxException; + +public class HelloWorld { + + public static void main(String[] args) throws URISyntaxException, IOException { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/updated-custom-extra-dir/foo b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/updated-custom-extra-dir/foo new file mode 100644 index 0000000000..54315cd5f5 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/updated-custom-extra-dir/foo @@ -0,0 +1 @@ +Unused file for committing parent directory so that it exists for extraDirectories tests \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle new file mode 100644 index 0000000000..ee3d6cd81d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle new file mode 100644 index 0000000000..596d80f423 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() + // The local repo contains one tiny test library installed with + // mvn org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file \ + // -Dfile=tiny.jar -DlocalRepositoryPath=local-m2-repo/ \ + // -DgroupId=com.google.cloud.tools -DartifactId=tiny-test-lib \ + // -Dversion=0.0.1-SNAPSHOT -Dpackaging=jar + maven { + url 'file:' + project.projectDir + '/local-m2-repo' + } +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } + extraDirectories { + paths = file('src/main/other-jib') + } +} + +sourceSets { + main { + resources { + srcDirs 'src/main/extra-resources-1', 'src/main/extra-resources-2' + } + } +} + +dependencies { + implementation project(':lib') + implementation 'org.apache.commons:commons-io:1.3.2' + implementation 'com.google.cloud.tools:tiny-test-lib:0.0.1-SNAPSHOT' +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..9984010307 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,24 @@ + + + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + + + true + + 20190911205316 + + + jar + 0.0.1-SNAPSHOT + 20190911205316 + + + pom + 0.0.1-SNAPSHOT + 20190911205316 + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000000..d1517b4150 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..ea93ba9238 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + POM was created from install:install-file + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml new file mode 100644 index 0000000000..5be427872a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.google.cloud.tools + tiny-test-lib + + + 0.0.1-SNAPSHOT + + 20190911205316 + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/other-jib/extra-file b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/other-jib/extra-file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/gradle.properties b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle new file mode 100644 index 0000000000..313ac21fc2 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java new file mode 100644 index 0000000000..0873623756 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +/** Shared Code! */ +public class Lib { + + public String getThing() { + return "thing"; + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt new file mode 100644 index 0000000000..32f95c0d12 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java new file mode 100644 index 0000000000..ab7425f896 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +import org.junit.Assert; +import org.junit.Test; + +/** Unit test for simple App. */ +public class LibTest { + /** Rigorous Test :-) */ + @Test + public void testGetThing() { + Assert.assertEquals("thing", new Lib().getThing()); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle new file mode 100644 index 0000000000..748c7dbf3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle @@ -0,0 +1,3 @@ +include ':simple-service' +include ':complex-service' +include ':lib' \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle new file mode 100644 index 0000000000..ee3d6cd81d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle new file mode 100644 index 0000000000..68526576e8 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle @@ -0,0 +1,5 @@ +subprojects { + repositories { + mavenCentral() + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle new file mode 100644 index 0000000000..3b14267107 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java-platform' +} + +dependencies { + constraints { + api 'org.apache.commons:commons-io:1.3.2' + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle new file mode 100644 index 0000000000..3ee005b253 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + implementation platform(project(':platform')) + + implementation 'org.apache.commons:commons-io' +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle new file mode 100644 index 0000000000..cf52f37e32 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle @@ -0,0 +1,2 @@ +include ':platform' +include ':service' diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle new file mode 100644 index 0000000000..7f2be8ac59 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } + extraDirectories { + paths = file('src/main/custom-extra-dir') + } +} + diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/bar/cat b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/bar/cat rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/foo b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/foo rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java similarity index 74% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java index 77a3cdc4a1..8b526b4cba 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -36,13 +36,6 @@ public static void main(String[] args) throws IOException, URISyntaxException { Path worldFile = Paths.get(classLoader.getResource("world").toURI()); String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); - System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); - - // Prints the contents of the extra files. - if (Files.exists(Paths.get("/foo"))) { - System.out.println(new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); - System.out.println( - new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); - } + System.out.println(greeting + ", " + world + ". "); } } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/build.gradle new file mode 100644 index 0000000000..d38b9531ba --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } + skaffold { + watch { + buildIncludes = project.file('script.gradle') + includes = project.files('other/file.txt') + excludes = 'src/main/jib/bar/' + } + sync { + excludes = ['build/classes/java/main/com/test', 'src/main/jib/foo'] + } + } +} + diff --git a/jib-maven-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/libs/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/other/file.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/other/file.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/script.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/script.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..cf42e84818 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test2/GoodbyeWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test2/GoodbyeWorld.java new file mode 100644 index 0000000000..9703d67bbc --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test2/GoodbyeWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test2; + +public class GoodbyeWorld { + public static void goodbye() { + System.out.println("Goodbye, world!"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/bar/cat b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/bar/cat new file mode 100644 index 0000000000..5d5b306e57 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/bar/cat @@ -0,0 +1 @@ +cat \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/foo b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/foo new file mode 100644 index 0000000000..1910281566 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/foo @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle new file mode 100644 index 0000000000..553267dfb9 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0' + moreLibs 'jakarta.annotation:jakarta.annotation-api:2.1.0' // random extra JAR +} + +jib { + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml new file mode 100644 index 0000000000..7d4aecb890 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.test + servlet25 + 1 + war + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + provided + + + jakarta.annotation + jakarta.annotation-api + 2.1.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + tomcat:8.5-jre8-alpine + + + ${_TARGET_IMAGE} + + + /usr/local/tomcat/webapps/ROOT + + + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml new file mode 100644 index 0000000000..7f3106122d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.test + servlet25 + 1 + war + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + provided + + + jakarta.annotation + jakarta.annotation-api + 2.1.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..ae65db32f0 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e3c07ab38a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

    Hello World!

    + + + + + + + + +
    Available Servlets:
    The HelloWorld servlet
    + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/META-INF/context.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/META-INF/context.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/Test.jsp b/jib-gradle-plugin/src/test/resources/gradle/webapp/Test.jsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/HelloWorld.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/HelloWorld.class @@ -0,0 +1 @@ + diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/Other.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/Other.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/Other.class @@ -0,0 +1 @@ + diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/test.properties b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/test.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependency-1.0.0.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependency-1.0.0.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000..dd68926802 Binary files /dev/null and b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar differ diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin-extension-api/CHANGELOG.md b/jib-maven-plugin-extension-api/CHANGELOG.md new file mode 100644 index 0000000000..2a19f8f179 --- /dev/null +++ b/jib-maven-plugin-extension-api/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +### Changed + +### Fixed + +## 0.4.0 + +### Changed + +- Upgraded jib-build-plan to 0.4.0. ([#2660](https://github.com/GoogleContainerTools/jib/pull/2660)) diff --git a/jib-maven-plugin-extension-api/build.gradle b/jib-maven-plugin-extension-api/build.gradle new file mode 100644 index 0000000000..aa4b6ce45f --- /dev/null +++ b/jib-maven-plugin-extension-api/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' +} + +dependencies { + api dependencyStrings.BUILD_PLAN + api dependencyStrings.EXTENSION_COMMON + api dependencyStrings.MAVEN_CORE +} + +jar { + manifest { + attributes 'Implementation-Version': archiveVersion + attributes 'Automatic-Module-Name': 'com.google.cloud.tools.jib.maven.extension' + + // OSGi metadata + attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib.maven.extension' + attributes 'Bundle-Name': 'Extension API for Jib Maven Plugin' + attributes 'Bundle-Vendor': 'Google LLC' + attributes 'Bundle-DocURL': 'https://github.com/GoogleContainerTools/jib' + attributes 'Bundle-License': 'https://www.apache.org/licenses/LICENSE-2.0' + attributes 'Export-Package': 'com.google.cloud.tools.jib.maven.extension' + } +} + +/* RELEASE */ +configureMavenRelease() + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Extension API for Jib Maven Plugin' + description = 'Provides API to extend Jib Maven Plugin containerization.' + } + from components.java + } + } +} + +// Release plugin (git release commits and version updates) +release { + tagTemplate = 'v$version-maven-extension' + git { + requireBranch = /^maven-extension-release-v\d+.*$/ //regex + } +} +/* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +/* ECLIPSE */ diff --git a/jib-maven-plugin-extension-api/gradle.properties b/jib-maven-plugin-extension-api/gradle.properties new file mode 100644 index 0000000000..6b7e8ff821 --- /dev/null +++ b/jib-maven-plugin-extension-api/gradle.properties @@ -0,0 +1 @@ +version = 0.4.1-SNAPSHOT diff --git a/jib-maven-plugin-extension-api/kokoro/release_build.sh b/jib-maven-plugin-extension-api/kokoro/release_build.sh new file mode 100755 index 0000000000..a6cf4e4acd --- /dev/null +++ b/jib-maven-plugin-extension-api/kokoro/release_build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Fail on any error. +set -o errexit +# Display commands to stderr. +set -o xtrace + +cd github/jib +./gradlew :jib-maven-plugin-extension-api:prepareRelease diff --git a/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/JibMavenPluginExtension.java b/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/JibMavenPluginExtension.java new file mode 100644 index 0000000000..16b8ae9cb9 --- /dev/null +++ b/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/JibMavenPluginExtension.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.extension; + +import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; +import com.google.cloud.tools.jib.plugins.extension.ExtensionLogger; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtension; +import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; +import java.util.Map; +import java.util.Optional; + +/** + * Jib Maven plugin extension API. + * + *

    If a class implementing the interface is visible on the classpath of the Jib Maven plugin and + * the plugin is configured to load the extension class, the Jib plugin extension framework calls + * the interface method of the class. + */ +public interface JibMavenPluginExtension extends JibPluginExtension { + + /** + * The type of an custom configuration defined by this extension. The configuration object is + * mapped from {@code }. Often, it is sufficient + * to leverage {@code } and the extension may not + * wish to define a custom configuration; in that case, use {@link Void} for <T> and have + * this method return {@code Optional#empty()}. (Don't return {@code Optional.of(Void.class)}.) + * + * @return type of an extension-specific custom configuration; {@code Optional.empty()} if no need + * to define custom configuration + */ + Optional> getExtraConfigType(); + + /** + * Extends the build plan prepared by the Jib Maven plugin. + * + * @param buildPlan original build plan prepared by the Jib Maven plugin + * @param properties custom properties configured for the plugin extension + * @param extraConfig extension-specific custom configuration mapped from {@code + * } of type <T>. {@link + * Optional#empty()} when {@link #getExtraConfigType()} returns {@link Optional#empty()} or + * {@code } is not specified by the extension user. + * @param mavenData {@link MavenData} providing Maven-specific data and properties + * @param logger logger for writing log messages + * @return updated build plan + * @throws JibPluginExtensionException if an error occurs while running the plugin extension + */ + ContainerBuildPlan extendContainerBuildPlan( + ContainerBuildPlan buildPlan, + Map properties, + Optional extraConfig, + MavenData mavenData, + ExtensionLogger logger) + throws JibPluginExtensionException; +} diff --git a/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/MavenData.java b/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/MavenData.java new file mode 100644 index 0000000000..41f75e60ae --- /dev/null +++ b/jib-maven-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/maven/extension/MavenData.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.extension; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.project.MavenProject; + +/** Holds Maven-specific data and properties. */ +public interface MavenData { + + MavenProject getMavenProject(); + + MavenSession getMavenSession(); +} diff --git a/jib-maven-plugin/.gitignore b/jib-maven-plugin/.gitignore deleted file mode 100644 index b0fbf0e4a5..0000000000 --- a/jib-maven-plugin/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -build -target -out -*.iml -*.ipr -*.iws -.idea -.gradle -/.settings -/.classpath -/.project -log.txt diff --git a/jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar b/jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index f775b1c04c..0000000000 Binary files a/jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/jib-maven-plugin/.mvn/wrapper/maven-wrapper.properties b/jib-maven-plugin/.mvn/wrapper/maven-wrapper.properties deleted file mode 100755 index a447c9fa81..0000000000 --- a/jib-maven-plugin/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip \ No newline at end of file diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index fa707cb743..4e613dafd2 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,109 +9,851 @@ All notable changes to this project will be documented in this file. ### Fixed -- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) +## 3.5.2 + +### Changed +- deps: Update Jackson BOM version to 2.21.1 (#4495) + +### Fixed +- fix: ignore unknown "artifactType" field in manifest descriptors (#4509) + +## 3.5.1 + +### Added +- feat: support Java 25 main methods + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.9 + +## 3.5.0 + +### Added +- feat: add default base image for Java 25 (#4436) + +### Changed +- deps: update `org.ow2.asm:asm` to version 9.8 for java 25 support + +### Fixed + +## 3.4.6 + +### Changed +- Update retrieval of Maven properties to use the following order: user, project, system [#4344](https://github.com/GoogleContainerTools/jib/issues/4344) + +## 3.4.5 + +### Fixed +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4267) + + +## 3.4.4 +- fix: allow pushing images with different arch/os to docker daemon [#4265](https://github.com/GoogleContainerTools/jib/issues/4265) +- fix: address windows deadlock issue when determining docker environment info [#4267](https://github.com/GoogleContainerTools/jib/issues/4265) + +## 3.4.3 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + +## 3.4.2 + +### Changed +- deps: bump org.apache.commons:commons-compress from 1.21 to 1.26.0 ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) + +### Fixed +- fix: set PAX headers to address build reproducibility issue ([#4204](https://github.com/GoogleContainerTools/jib/pull/4204)) +- fix: (WAR Containerization) modify default entrypoint to `java -jar /usr/local/jetty/start.jar --module=ee10-deploy` for Jetty 12+ compatibility ([#4216](https://github.com/GoogleContainerTools/jib/pull/4216)) + +## 3.4.1 + +### Fixed +- fix: support parsing manifest JSON containing `LayerSources:` from latest Docker. ([#4171](https://github.com/GoogleContainerTools/jib/pull/4171)) + +## 3.4.0 + +### Changed +- deps: bump org.apache.maven:maven-compat from 3.9.1 to 3.9.2. ([#4017](https://github.com/GoogleContainerTools/jib/pull/4017/)) +- deps: bump com.github.luben:zstd-jni from 1.5.5-2 to 1.5.5-4. ([#4049](https://github.com/GoogleContainerTools/jib/pull/4049/)) +- deps: bump com.fasterxml.jackson:jackson-bom from 2.15.0 to 2.15.2. ([#4055](https://github.com/GoogleContainerTools/jib/pull/4055)) +- deps: bump com.google.guava:guava from 32.0.1-jre to 32.1.2-jre ([#4078](https://github.com/GoogleContainerTools/jib/pull/4078)) +- deps: bump org.slf4j:slf4j-simple from 2.0.7 to 2.0.9. ([#4098](https://github.com/GoogleContainerTools/jib/pull/4098)) + +### Fixed +- fix: fix WWW-Authenticate header parsing for Basic authentication ([#4035](https://github.com/GoogleContainerTools/jib/pull/4035/)) + +## 3.3.2 + +### Changed +- Log an info instead of warning when entrypoint makes the image to ignore jvm parameters ([#3904](https://github.com/GoogleContainerTools/jib/pull/3904)) + +Thanks to our community contributors @rmannibucau! + +## 3.3.1 + +### Changed +- Upgraded Google HTTP libraries to 1.42.2 ([#3745](https://github.com/GoogleContainerTools/jib/pull/3745)) + +## 3.3.0 + +### Added + +- Included `imagePushed` field to image metadata json output file which provides information on whether an image was pushed by Jib. Note that the output file is `build/jib-image.json` by default or configurable with `jib.outputPaths.imageJson`. ([#3641](https://github.com/GoogleContainerTools/jib/pull/3641)) +- Better error messaging when environment map in `container.environment` contains null values ([#3672](https://github.com/GoogleContainerTools/jib/pull/3672)). +- Support for OCI image index manifests ([#3715](https://github.com/GoogleContainerTools/jib/pull/3715)). +- Support for base image layer compressed with zstd ([#3717](https://github.com/GoogleContainerTools/jib/pull/3717)). + +### Changed + +- Upgraded slf4j-simple and slf4j-api to 2.0.0 ([#3734](https://github.com/GoogleContainerTools/jib/pull/3734), [#3735](https://github.com/GoogleContainerTools/jib/pull/3735)). +- Upgraded nullaway to 0.9.9. ([#3720](https://github.com/GoogleContainerTools/jib/pull/3720)) +- Jib now only checks for file existence instead of running the executable passed into `dockerClient.executable` for the purpose of verifying if docker is installed correctly. Users are responsible for ensuring that the docker executable specified through this property is valid and has the correct permissions ([#3744](https://github.com/GoogleContainerTools/jib/pull/3744)). +- Jib now throws an exception when the base image doesn't support target platforms during multi-platform build ([#3707](https://github.com/GoogleContainerTools/jib/pull/3707)). + +Thanks to our community contributors @wwadge, @oliver-brm, @rquinio and @gsquared94! + +## 3.2.1 + +### Added + +- Environment variables can now be used in configuring credential helpers. ([#2814](https://github.com/GoogleContainerTools/jib/issues/2814)) + ```xml + + myimage + + ecr-login + + profile + + + + ``` + +### Changed + +- Upgraded jackson-databind to 2.13.2.2 ([#3612](https://github.com/GoogleContainerTools/jib/pull/3612)). + +## 3.2.0 + +### Added + +- [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#from-object) parameter for multi-architecture image building can now be configured through Maven and system properties (for example, `-Djib.from.platforms=linux/amd64,linux/arm64` on the command-line). ([#2742](https://github.com/GoogleContainerTools/jib/pull/2742)) +- For retrieving credentials, Jib additionally looks for `$XDG_RUNTIME_DIR/containers/auth.json`, `$XDG_CONFIG_HOME/containers/auth.json`, and `$HOME/.config/containers/auth.json`. ([#3524](https://github.com/GoogleContainerTools/jib/issues/3524)) + + +### Changed + +- Changed the default base image of the Jib CLI `jar` command from the `adoptopenjdk` images to the [`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin) on Docker Hub. Note that Temurin (by Adoptium) is the new name of AdoptOpenJDK. ([#3483](https://github.com/GoogleContainerTools/jib/issues/3483)) +- Build will fail if `` contain `from` directory that doesn't exist locally ([#3542](https://github.com/GoogleContainerTools/jib/issues/3542)) + +### Fixed + +- Fixed incorrect parsing with comma escaping when providing Jib list or map property values on the command-line. ([#2224](https://github.com/GoogleContainerTools/jib/issues/2224)) + +## 3.1.4 + +### Changed + +- Downgraded Google HTTP libraries to 1.34.0 to resolve network issues. ([#3415](https://github.com/GoogleContainerTools/jib/pull/3415), [#3058](https://github.com/GoogleContainerTools/jib/issues/3058), [#3409](https://github.com/GoogleContainerTools/jib/issues/3409)) +- If `allowInsecureRegistries=true`, HTTP requests are retried on I/O errors only after insecure failover is finalized for each server. ([#3422](https://github.com/GoogleContainerTools/jib/issues/3422)) + +## 3.1.3 + +### Added + +- Increased robustness in registry communications by retrying HTTP requests (to the effect of retrying image pushes or pulls) on I/O exceptions with exponential backoffs. ([#3351](https://github.com/GoogleContainerTools/jib/pull/3351)) +- Now also supports `username` and `password` properties for the `auths` section in a Docker config (`~/.docker/config.json`). (Previously, only supported was a base64-encoded username and password string of the `auth` property.) ([#3365](https://github.com/GoogleContainerTools/jib/pull/3365)) + +### Changed + +- Upgraded Google HTTP libraries to 1.39.2. ([#3387](https://github.com/GoogleContainerTools/jib/pull/3387)) + +## 3.1.2 + +### Fixed + +- Fixed the bug introduced in 3.1 that constructs a wrong Java runtime classpath when two dependencies have the same artifact ID and version but different group IDs. The bug occurs only when using Java 9+ or setting ``. ([#3331](https://github.com/GoogleContainerTools/jib/pull/3331)) + +## 3.1.1 + +### Fixed + +- Fixed the regression introduced in 3.1.0 where a build may fail due to an error from main class inference even if `` is configured. ([#3295](https://github.com/GoogleContainerTools/jib/pull/3295)) + +## 3.1.0 + +### Added + +- For Google Artifact Registry (`*-docker.pkg.dev`), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) last like it has been doing for `gcr.io`. ([#3241](https://github.com/GoogleContainerTools/jib/pull/3241)) + +### Changed + +- Jib now creates an additional layer that contains two small text files: [`/app/jib-classpath-file` and `/app/jib-main-class-file`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin/README.md#custom-container-entrypoint). They hold, respectively, the final Java runtime classpath and the main class computed by Jib that are suitable for app execution on JVM. For example, with Java 9+, setting the container entrypoint to `java --class-path @/app/jib-classpath-file @/app/jib-main-class-file` will work to start the app. (This is basically the default entrypoint set by Jib when the entrypoint is not explicitly configured by the user.) The files are always generated whether Java 8 or 9+, or whether `jib.container.entrypoint` is explicitly configured. The files can be helpful especially when setting a custom entrypoint for a shell script that needs to get the classpath and the main class computed by Jib, or for [AppCDS](https://github.com/GoogleContainerTools/jib/issues/2471). ([#3280](https://github.com/GoogleContainerTools/jib/pull/3280)) +- For Java 9+ apps, the default Java runtime classpath explicitly lists all the app dependencies, preserving the dependency loading order declared by Maven. This is done by changing the default entrypoint to use the new classpath JVM argument file (basically `java -cp @/app/jib-classpath-file`). As such, `` takes no effect for Java 9+. ([#3280](https://github.com/GoogleContainerTools/jib/pull/3280)) +- Timestamps of file entries in a tarball built with `jib:buildTar` are set to the epoch, making the tarball reproducible. ([#3158](https://github.com/GoogleContainerTools/jib/issues/3158)) + +## 3.0.0 + +### Added + +- New `` and `` options for ``. This enables copying a subset of files from the source directory using glob patterns. ([#2564](https://github.com/GoogleContainerTools/jib/issues/2564)) +- [Jib extensions](https://github.com/GoogleContainerTools/jib-extensions) can be loaded via the [Maven dependency injection mechanism](https://maven.apache.org/maven-jsr330.html). This also enables injecting arbitrary dependencies (for example, Maven components) into an extension. ([#3036](https://github.com/GoogleContainerTools/jib/issues/3036)) + +### Changed + +- [Switched the default base images](https://github.com/GoogleContainerTools/jib/blob/master/docs/default_base_image.md) from Distroless to [`adoptopenjdk:{8,11}-jre`](https://hub.docker.com/_/adoptopenjdk) and [`jetty`](https://hub.docker.com/_/jetty) (for WAR). ([#3124](https://github.com/GoogleContainerTools/jib/pull/3124)) + +### Fixed + +- Fixed an issue where some log messages used color in the "plain" console output. ([#2764](https://github.com/GoogleContainerTools/jib/pull/2764)) + +## 2.8.0 + +### Added + +- Added support for [configuring registry mirrors](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-am-hitting-docker-hub-rate-limits-how-can-i-configure-registry-mirrors) for base images. This is useful when hitting [Docker Hub rate limits](https://www.docker.com/increase-rate-limits). Only public mirrors (such as `mirror.gcr.io`) are supported. ([#3011](https://github.com/GoogleContainerTools/jib/issues/3011)) + +### Changed + +- Build will fail if Jib cannot create or read the [global Jib configuration file](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#global-jib-configuration). ([#2996](https://github.com/GoogleContainerTools/jib/pull/2996)) + +## 2.7.1 + +### Fixed + +- Updated jackson dependency version causing compatibility issues. ([#2931](https://github.com/GoogleContainerTools/jib/issues/2931)) + +## 2.7.0 + +### Changed + +- Added an option `` to preserve the order of loading dependencies as configured in a project. The option enumerates dependency JARs instead of using a wildcard (`/app/libs/*`) in the Java runtime classpath for an image entrypoint. ([#1871](https://github.com/GoogleContainerTools/jib/issues/1871), [#1907](https://github.com/GoogleContainerTools/jib/issues/1907), [#2228](https://github.com/GoogleContainerTools/jib/issues/2228), [#2733](https://github.com/GoogleContainerTools/jib/issues/2733)) + - The option is also useful for AppCDS. ([#2471](https://github.com/GoogleContainerTools/jib/issues/2471)) + - Turning on the option may result in a very long classpath string, and the OS may not support passing such a long string to JVM. + +### Fixed + +- Fixed `NullPointerException` when pulling an OCI base image whose manifest does not have `mediaType` information. ([#2819](https://github.com/GoogleContainerTools/jib/issues/2819)) +- Fixed build failure when using a Docker daemon base image (`docker://...`) that has duplicate layers. ([#2829](https://github.com/GoogleContainerTools/jib/issues/2829)) + +## 2.6.0 + +### Added + +### Changed + +- Previous locally cached base image manifests will be ignored, as the caching mechanism changed to enable multi-platform image building. ([#2730](https://github.com/GoogleContainerTools/jib/pull/2730), [#2711](https://github.com/GoogleContainerTools/jib/pull/2711)) +- Upgraded the ASM library to 9.0 to resolve an issue when auto-inferring main class in Java 15+. ([#2776](https://github.com/GoogleContainerTools/jib/pull/2776)) +- _Incubating feature_: can now configure multiple platforms (such as architectures) to build multiple images as a bundle and push as a manifest list (also known as a fat manifest). As an incubating feature, there are certain limitations. For example, OCI image indices are not supported, and building a manifest list is supported only for registry pushing (the `jib:build` goal). ([#2523](https://github.com/GoogleContainerTools/jib/issues/2523)) + ```xml + + ... image reference to a manifest list ... + + + arm64 + linux + + + + ``` + +### Fixed + +- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784)) +- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134)) + +## 2.5.2 + +### Fixed + +- Fixed the regression introduced in 2.5.1 that caused Jib to containerize a Spring Boot fat JAR instead of a normal thin JAR when `packaged` is set and the Spring Boot Maven plugin does not have a `` block. ([#2693](https://github.com/GoogleContainerTools/jib/pull/2693)) + +## 2.5.1 + +### Fixed + +- Fixed `NullPointerException` when `packaged` is set the Spring Boot Maven plugin does not have a `` block. ([#2687](https://github.com/GoogleContainerTools/jib/issues/2687)) + +## 2.5.0 + +### Added + +- Also tries `.exe` file extension for credential helpers on Windows. ([#2527](https://github.com/GoogleContainerTools/jib/issues/2527)) +- New system property `jib.skipExistingImages` (false by default) to skip pushing images (manifests) if the image already exists in the registry. ([#2360](https://github.com/GoogleContainerTools/jib/issues/2360)) +- _Incubating feature_: can now configure desired platform (architecture and OS) to select the matching manifest from a Docker manifest list for a base image. Currently supports building only one image. OCI image indices are not supported. ([#1567](https://github.com/GoogleContainerTools/jib/issues/1567)) + ```xml + + ... image reference to a manifest list ... + + + arm64 + linux + + + + ``` + +### Fixed + +- Fixed reporting a wrong credential helper name when the helper does not exist on Windows. ([#2527](https://github.com/GoogleContainerTools/jib/issues/2527)) +- Fixed `NullPointerException` when the `"auths":` section in `~/.docker/config.json` has an entry with no `"auth":` field. ([#2535](https://github.com/GoogleContainerTools/jib/issues/2535)) +- Fixed `NullPointerException` to return a helpful message when a server does not provide any message in certain error cases (400 Bad Request, 404 Not Found, and 405 Method Not Allowed). ([#2532](https://github.com/GoogleContainerTools/jib/issues/2532)) +- Now supports sending client certificate (for example, via the `javax.net.ssl.keyStore` and `javax.net.ssl.keyStorePassword` system properties) and thus enabling mutual TLS authentication. ([#2585](https://github.com/GoogleContainerTools/jib/issues/2585), [#2226](https://github.com/GoogleContainerTools/jib/issues/2226)) +- Fixed build failure with `packaged` in Spring Boot projects where Jib assumed a wrong JAR path when `` or `` is configured in Spring Boot. ([#2565](https://github.com/GoogleContainerTools/jib/issues/2565)) +- Fixed an issue where Jib cannot infer Kotlin main class that takes no arguments. ([#2666](https://github.com/GoogleContainerTools/jib/pull/2666)) + +## 2.4.0 + +### Added + +- Jib Extension Framework! The framework enables anyone to easily extend and tailor the Jib Maven plugin behavior to their liking. Check out the new [Jib Extensions](https://github.com/GoogleContainerTools/jib-extensions) GitHub repository to learn more. ([#2401](https://github.com/GoogleContainerTools/jib/issues/2401)) +- Project dependencies in a multi-module WAR project are now stored in a separate "project dependencies" layer (as currently done for a non-WAR project). ([#2450](https://github.com/GoogleContainerTools/jib/issues/2450)) + +### Changed + +- Previous locally cached application layers (`/target/jib-cache`) will be ignored because of changes to the caching selectors. ([#2499](https://github.com/GoogleContainerTools/jib/pull/2499)) + +### Fixed + +- Fixed authentication failure with Azure Container Registry when using an identity token defined in the `auths` section of Docker config (`~/.docker/config.json`). ([#2488](https://github.com/GoogleContainerTools/jib/pull/2488)) + +## 2.3.0 + +### Added + +- `` and `` fields to `` for configuring the source and target of an extra directory. ([#1581](https://github.com/GoogleContainerTools/jib/issues/1581)) + +### Fixed + +- Fixed the problem not inheriting `USER` container configuration from a base image. ([#2421](https://github.com/GoogleContainerTools/jib/pull/2421)) +- Fixed wrong capitalization of JSON properties in a loadable Docker manifest when building a tar image. ([#2430](https://github.com/GoogleContainerTools/jib/issues/2430)) +- Fixed an issue when using a base image whose image creation timestamp contains timezone offset. ([#2428](https://github.com/GoogleContainerTools/jib/issues/2428)) +- Fixed an issue inferring a wrong main class or using an invalid main class (for example, Spring Boot project containing multiple main classes). ([#2456](https://github.com/GoogleContainerTools/jib/issues/2456)) + +## 2.2.0 + +### Added + +- Glob pattern support for ``. ([#1200](https://github.com/GoogleContainerTools/jib/issues/1200)) +- Support for image references with both a tag and a digest. ([#1481](https://github.com/GoogleContainerTools/jib/issues/1481)) +- The `DOCKER_CONFIG` environment variable specifying the directory containing docker configs is now checked during credential retrieval. ([#1618](https://github.com/GoogleContainerTools/jib/issues/1618)) +- Also tries `.cmd` file extension for credential helpers on Windows. ([#2399](https://github.com/GoogleContainerTools/jib/issues/2399)) + +### Changed + +- `` now accepts more timezone formats:`+HHmm`. This allows for easier configuration of creationTime by external systems. ([#2320](https://github.com/GoogleContainerTools/jib/issues/2320)) + +## 2.1.0 + +### Added + +- Additionally reads credentials from `~/.docker/.dockerconfigjson` and legacy Docker config (`~/.docker/.dockercfg`). Also searches for `$HOME/.docker/*` (in addition to current `System.get("user.home")/.docker/*`). This may help retrieve credentials, for example, on Kubernetes. ([#2260](https://github.com/GoogleContainerTools/jib/issues/2260)) +- New skaffold configuration options that modify how jib's build config is presented to skaffold ([#2292](https://github.com/GoogleContainerTools/jib/pull/2292)): + - ``: a list of build files to watch + - ``: a list of project files to watch + - ``: a list of files to exclude from watching + - ``: a list of files to exclude from sync'ing + +### Fixed + +- Fixed a `skaffold init` issue with projects containing submodules specifying different parent poms. ([#2262](https://github.com/GoogleContainerTools/jib/issues/2262)) +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) + +## 2.0.0 + +### Added + +- Added json output file for image metadata after a build is complete. Writes to `target/jib-image.json` by default, configurable with ``. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) +- Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) + +### Changed + +- Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) +- Removed deprecated `` configuration in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- The default base image cache location has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` + - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location +- When giving registry credentials in `settings.xml`, specifying port in `` is no longer required. ([#2135](https://github.com/GoogleContainerTools/jib/issues/2135)) + +### Fixed + +- Fixed `` being ignored if `` are not explicitly defined. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2160)) +- Now `packaged` works as intended with Spring Boot projects that generate a fat JAR. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) +- Now `packaged` correctly identifies the packaged JAR generated at a non-default location when configured with the Maven Jar Plugin's `` and ``. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) +- `jib:buildTar` with `OCI` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Fixed an issue where configuring the `` property of the Maven WAR plugin fails the build. ([#2206](https://github.com/GoogleContainerTools/jib/issues/2206)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) + +## 1.8.0 + +### Changed + +- Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + +### Fixed + +- Fixed reporting wrong module name when `skaffold init` is run on multi-module projects. ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)) +- `` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) +- Fixed inefficient communications when interacting with insecure registries and servers (when `` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) + +## 1.7.0 + +### Added + +- `` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) + - `` configures output path of `jib:buildTar` (`target/jib-image.tar` by default) + - `` configures the output path of the image digest (`target/jib-image.digest` by default) + - `` configures output path of the image id (`target/jib-image.id` by default) +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) + +### Changed + +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `docker://` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) + +### Fixed + +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) +- Fixed `tar://` base image failing if tar does not contain explicit directory entries. ([#2067](https://github.com/GoogleContainerTools/jib/issues/2067)) +- Fixed an issue for WAR projects where Jib used an intermediate exploded WAR directory instead of exploding the final WAR file. ([#1091](https://github.com/GoogleContainerTools/jib/issues/1091)) + +## 1.6.1 + +### Fixed + +- Fixed an issue with using custom base images in Java 12+ projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) + +## 1.6.0 + +### Added + +- Support for local base images by prefixing `` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) + +### Changed + +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- `` is now configurable on all goals, not just `jib:dockerBuild`. ([#1932](https://github.com/GoogleContainerTools/jib/issues/1932)) + +### Fixed + +- Fixed the regression of slow network operations introduced at 1.5.0. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when `` is set. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) + +## 1.5.1 + +### Fixed + +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) + +## 1.5.0 + +### Added + +- Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) + +### Changed + +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `` has been deprecated in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) + +## 1.4.0 + +### Added + +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged`. ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) +- Can now use `scratch` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) + +### Changed + +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies. ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) + +### Fixed + +- Re-enabled cross-repository blob mounts. ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + +## 1.3.0 + +### Changed + +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers. ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) + +### Fixed + +- Fixed an issue where decyrpting Maven settings `settings.xml` wholesale caused the build to fail. We now decrypt only the parts that are required. ([#1709](https://github.com/GoogleContainerTools/jib/issues/1709)) + +## 1.2.0 + +### Added + +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with ``. ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline`. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) +- Now supports multiple extra directories with `{|}`. ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) + +### Changed + +- `(|)` are deprecated in favor of the new `{|}` configurations. ([#1626](https://github.com/GoogleContainerTools/jib/pull/1626)) + +### Fixed + +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) + +## 1.1.2 + +### Fixed + +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) + +## 1.1.1 + +### Fixed + +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + +## 1.1.0 + +### Added + +- Can now decrypt proxy configurations in `settings.xml`. ([#1369](https://github.com/GoogleContainerTools/jib/issues/1369)) + +### Changed + +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) + +### Fixed + +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) + +## 1.0.2 + +### Added + +- Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) +- Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) +- Batch mode now disables build progress bar. ([#1513](https://github.com/GoogleContainerTools/jib/issues/1513)) + +### Fixed + +- Fixed an issue where setting `` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) +- Crash on talking to servers that do not set the `Content-Length` HTTP header or send an incorrect value. ([#1512](https://github.com/GoogleContainerTools/jib/issues/1512)) + +## 1.0.1 + +### Added + +- Java 9+ projects are now supported and run on the distroless Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + +### Fixed + +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + +## 1.0.0 + +### Changed + +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy. ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + +## 1.0.0-rc2 + +### Added + +- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. +- Maven proxy settings are now supported. +- Now checks for system properties in pom as well as commandline. ([#1201](https://github.com/GoogleContainerTools/jib/issues/1201)) +- `` and `` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary. ([#468](https://github.com/GoogleContainerTools/jib/issues/468)) + +### Changed + +- Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) + +## 1.0.0-rc1 + +### Added + +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories. ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages. ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) + +### Changed + +- `gwt-app` packaging type now builds a WAR container. +- When building to Docker and no `` is defined, artifact ID is used as an image reference instead of project name. +- Removed `` parameter in favor of the `jib.useOnlyProjectCache` system property. ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) + +### Fixed + +- Builds failing due to dependency JARs with the same name. ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) + +## 0.10.1 + +### Added + +- Image ID is now written to `target/jib-image.id`. ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) +- `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. +- `` configuration parameter to set the working directory. ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) + +### Changed + +- Removed `jib:exportDockerContext` goal. ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) + +### Fixed + +- NullPointerException thrown with incomplete `auth` configuration. ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) + +## 0.10.0 + +### Added + +- Properties for each configuration parameter, allowing any parameter to be set via commandline. ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) +- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable. ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `` configuration parameter to configure the user and group to run the container as. ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects. ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) +- `` object with a `` and `` field. ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) + - `` configures the extra layer directory (still also configurable via `...`) + - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) +- Image digest is now written to `target/jib-image.digest`. ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) +- Adds the layer type to the layer history as comments. ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) + +### Changed + +- Removed deprecated ``, ``, ``, and `` in favor of the equivalents under ``. ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) +- `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support. ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) + +## 0.9.13 + +### Fixed + +- Adds environment variable configuration to Docker context generator. ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) + +## 0.9.11 + +### Added + +- `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property). ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) +- `` configuration parameter to configure environment variables. ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image. ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `` (list) defines additional tags to push to. ([#1026](https://github.com/GoogleContainerTools/jib/pull/1026)) + +### Fixed + +- Keep duplicate layers to match container history. ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) + +## 0.9.10 + +### Added + +- `` configuration parameter for configuring labels. ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `` configuration parameter to set the entrypoint. ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) +- `history` to layer metadata. ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image. ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) + +### Fixed + +- Corrects permissions for directories in the container filesystem. ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + +## 0.9.9 + +### Added + +- Passthrough labels from base image. ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) + +### Changed + +- Reordered classpath in entrypoint to allow dependency patching. ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) + +### Fixed + +## 0.9.8 + +### Added + +- `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin. ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties. ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Docker context generation now includes snapshot dependencies and extra files. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true`. ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) + +### Changed + +- Propagates environment variables from the base image. ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +- Skips execution if packaging is `pom`. ([#735](https://github.com/GoogleContainerTools/jib/pull/735)) +- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates). ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) + +### Fixed + +- Slow image reference parsing. ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth. ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same. ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) + +## 0.9.7 + +### Added + +- Snapshot dependencies are added as their own layer. ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load`. ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- `` parameter to set the image creation time to the build time. ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) +- Authentication over HTTP using the `sendCredentialsOverHttp` system property. ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property. ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) + +### Changed + +- Docker context export parameter `-Djib.dockerDir` to `-DjibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) + +### Fixed + +- Using multi-byte characters in container configuration. ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config. ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) +- Decrypting credentials from Maven settings. ([#592](https://github.com/GoogleContainerTools/jib/issues/592)) + +## 0.9.6 + +### Fixed + +- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true`. ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 ### Added -- Incubating feature to build `src/main/jib` as extra layer in image ([#565](https://github.com/GoogleContainerTools/jib/pull/565)) +- Incubating feature to build `src/main/jib` as extra layer in image. ([#565](https://github.com/GoogleContainerTools/jib/pull/565)) ## 0.9.4 ### Fixed -- Fixed handling case-insensitive `Basic` authentication method ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) -- Fixed regression that broke pulling base images from registries that required token authentication ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) +- Fixed handling case-insensitive `Basic` authentication method. ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) +- Fixed regression that broke pulling base images from registries that required token authentication. ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) ## 0.9.3 ### Fixed -- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol) ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) + +- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol). ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) ## 0.9.2 ### Changed -- Minor improvements and issue fixes + +- Minor improvements and issue fixes. ## 0.9.1 + ### Added -- `` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`) ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) -- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) + +- `` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`). ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) +- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP. ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) ### Changed -- Fetches credentials from inferred credential helper before Docker config ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) -- Container creation date set to timestamp 0 ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) -- Does not authenticate base image pull unless necessary - reduces build time by about 500ms ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) -- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) + +- Fetches credentials from inferred credential helper before Docker config. ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) +- Container creation date set to timestamp 0. ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) +- Does not authenticate base image pull unless necessary - reduces build time by about 500ms. ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) +- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object. ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) ### Fixed -- Using Azure Container Registry now works - define credentials in Maven settings ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) -- Supports `access_token` as alias to `token` in registry authentication ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) + +- Using Azure Container Registry now works - define credentials in Maven settings. ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) +- Supports `access_token` as alias to `token` in registry authentication. ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) ## 0.9.0 + ### Added -- Better feedback for build failures ([#197](https://github.com/google/jib/pull/197)) -- Warns if specified `mainClass` is not a valid Java class ([#206](https://github.com/google/jib/issues/206)) -- Warns if build may not be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- `jib:dockerBuild` maven goal to build straight to Docker daemon ([#266](https://github.com/GoogleContainerTools/jib/pull/266)) -- `mainClass` is inferred by searching through class files if configuration is missing ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) -- Can now specify target image with `-Dimage` ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) -- `args` parameter to define default main args ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) + +- Better feedback for build failures. ([#197](https://github.com/google/jib/pull/197)) +- Warns if specified `mainClass` is not a valid Java class. ([#206](https://github.com/google/jib/issues/206)) +- Warns if build may not be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- `jib:dockerBuild` maven goal to build straight to Docker daemon. ([#266](https://github.com/GoogleContainerTools/jib/pull/266)) +- `mainClass` is inferred by searching through class files if configuration is missing. ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) +- Can now specify target image with `-Dimage`. ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) +- `args` parameter to define default main args. ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) ### Changed -- Removed `enableReproducibleBuilds` parameter - application layers will always be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- Changed configuration schema to be more like configuration for `jib-gradle-plugin` - NOT compatible with prior versions of `jib-maven-plugin` ([#212](https://github.com/GoogleContainerTools/jib/issues/212)) -- `jib:dockercontext` has been changed to `jib:exportDockerContext` ([#350](https://github.com/GoogleContainerTools/jib/issues/350)) + +- Removed `enableReproducibleBuilds` parameter - application layers will always be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- Changed configuration schema to be more like configuration for `jib-gradle-plugin` - NOT compatible with prior versions of `jib-maven-plugin`. ([#212](https://github.com/GoogleContainerTools/jib/issues/212)) +- `jib:dockercontext` has been changed to `jib:exportDockerContext`. ([#350](https://github.com/GoogleContainerTools/jib/issues/350)) ### Fixed -- Directories in resources are added to classes layer ([#318](https://github.com/GoogleContainerTools/jib/issues/318)) + +- Directories in resources are added to classes layer. ([#318](https://github.com/GoogleContainerTools/jib/issues/318)) ## 0.1.7 + ### Fixed -- Using base images that lack entrypoints ([#284](https://github.com/GoogleContainerTools/jib/pull/284) + +- Using base images that lack entrypoints. ([#284](https://github.com/GoogleContainerTools/jib/pull/284)) ## 0.1.6 + ### Changed -- Base image layers are now cached on a user-level rather than a project level - disable with `useOnlyProjectCache` configuration ([#29](https://github.com/google/jib/issues/29)) + +- Base image layers are now cached on a user-level rather than a project level - disable with `useOnlyProjectCache` configuration. ([#29](https://github.com/google/jib/issues/29)) ### Fixed -- `jib:dockercontext` not building a `Dockerfile` ([#171](https://github.com/google/jib/pull/171)) -- Failure to parse Docker config with `HttpHeaders` field ([#175](https://github.com/google/jib/pull/175)) + +- `jib:dockercontext` not building a `Dockerfile`. ([#171](https://github.com/google/jib/pull/171)) +- Failure to parse Docker config with `HttpHeaders` field. ([#175](https://github.com/google/jib/pull/175)) ## 0.1.5 + ### Added -- Export a Docker context (including a Dockerfile) with `jib:dockercontext` ([#49](https://github.com/google/jib/issues/49)) + +- Export a Docker context (including a Dockerfile) with `jib:dockercontext`. ([#49](https://github.com/google/jib/issues/49)) ## 0.1.4 + ### Fixed -- Null tag validation generating NullPointerException ([#125](https://github.com/google/jib/issues/125)) -- Build failure on project with no dependencies ([#126](https://github.com/google/jib/issues/126)) + +- Null tag validation generating NullPointerException. ([#125](https://github.com/google/jib/issues/125)) +- Build failure on project with no dependencies. ([#126](https://github.com/google/jib/issues/126)) ## 0.1.3 + ### Added -- Build and push OCI container image ([#96](https://github.com/google/jib/issues/96)) + +- Build and push OCI container image. ([#96](https://github.com/google/jib/issues/96)) ## 0.1.2 + ### Added -- Use credentials from Docker config if none can be found otherwise ([#101](https://github.com/google/jib/issues/101)) -- Reproducible image building ([#7](https://github.com/google/jib/issues/7)) + +- Use credentials from Docker config if none can be found otherwise. ([#101](https://github.com/google/jib/issues/101)) +- Reproducible image building. ([#7](https://github.com/google/jib/issues/7)) ## 0.1.1 + ### Added -- Simple example `helloworld` project under `examples/` ([#62](https://github.com/google/jib/pull/62)) -- Better error messages when pushing an image manifest ([#63](https://github.com/google/jib/pull/63)) -- Validates target image configuration ([#63](https://github.com/google/jib/pull/63)) -- Configure multiple credential helpers with `credHelpers` ([#68](https://github.com/google/jib/pull/68)) -- Configure registry credentials with Maven settings ([#81](https://github.com/google/jib/pull/81)) + +- Simple example `helloworld` project under `examples/`. ([#62](https://github.com/google/jib/pull/62)) +- Better error messages when pushing an image manifest. ([#63](https://github.com/google/jib/pull/63)) +- Validates target image configuration. ([#63](https://github.com/google/jib/pull/63)) +- Configure multiple credential helpers with `credHelpers`. ([#68](https://github.com/google/jib/pull/68)) +- Configure registry credentials with Maven settings. ([#81](https://github.com/google/jib/pull/81)) ### Changed -- Removed configuration `credentialHelperName` ([#68](https://github.com/google/jib/pull/68)) + +- Removed configuration `credentialHelperName`. ([#68](https://github.com/google/jib/pull/68)) ### Fixed -- Build failure on Windows ([#74](https://github.com/google/jib/issues/74)) -- Infers common credential helper names (for GCR and ECR) ([#64](https://github.com/google/jib/pull/64)) -- Cannot use private base image ([#68](https://github.com/google/jib/pull/68)) -- Building applications with no resources ([#73](https://github.com/google/jib/pull/73)) -- Pushing to registries like Docker Hub and ACR ([#75](https://github.com/google/jib/issues/75)) -- Cannot build with files having long file names (> 100 chars) ([#91](https://github.com/google/jib/issues/91)) + +- Build failure on Windows. ([#74](https://github.com/google/jib/issues/74)) +- Infers common credential helper names (for GCR and ECR). ([#64](https://github.com/google/jib/pull/64)) +- Cannot use private base image. ([#68](https://github.com/google/jib/pull/68)) +- Building applications with no resources. ([#73](https://github.com/google/jib/pull/73)) +- Pushing to registries like Docker Hub and ACR. ([#75](https://github.com/google/jib/issues/75)) +- Cannot build with files having long file names (> 100 chars). ([#91](https://github.com/google/jib/issues/91)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 489dfce61b..2db193798b 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -1,28 +1,54 @@ -[![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-maven-plugin)](https://github.com/igrigorik/ga-beacon) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) +[![Maven Central](https://img.shields.io/maven-central/v/com.google.cloud.tools/jib-maven-plugin)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Maven project -Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and OCI images for your Java applications. +Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. -For information about the project, see the [Jib project README](../README.md). For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). -## Upcoming Features - -These features are not currently supported but will be added in later releases. +For information about the project, see the [Jib project README](../README.md). -* Support for WAR format -* Run and debug the built container +| ☑️ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + +## Table of Contents + +* [Upcoming Features](#upcoming-features) +* [Quickstart](#quickstart) + * [Setup](#setup) + * [Configuration](#configuration) + * [Build your image](#build-your-image) + * [Build to Docker Daemon](#build-to-docker-daemon) + * [Build an image tarball](#build-an-image-tarball) + * [Bind to a lifecycle](#bind-to-a-lifecycle) + * [Additional Build Artifacts](#additional-build-artifacts) +* [Multi Module Projects](#multi-module-projects) +* [Extended Usage](#extended-usage) + * [Configuration Properties](#configuration-properties) + * [Global Jib Configuration](#global-jib-configuration) + * [Example](#example) + * [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) + * [Authentication Methods](#authentication-methods) + * [Using Docker configuration files](#using-docker-configuration-files) + * [Using Docker Credential Helpers](#using-docker-credential-helpers) + * [Using Specific Credentials](#using-specific-credentials) + * [Using Maven Settings](#using-maven-settings) + * [Custom Container Entrypoint](#custom-container-entrypoint) + * [Jib Extensions](#jib-extensions) + * [WAR Projects](#war-projects) + * [Skaffold Integration](#skaffold-integration) +* [Need Help?](#need-help) +* [Community](#community) ## Quickstart You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.4:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:3.5.2:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -30,7 +56,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.4:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:3.5.2:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -48,7 +74,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.4 + 3.5.2 myimage @@ -103,12 +129,38 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b ```xml - registry.hub.docker.com/my-docker-id/my-app + docker.io/my-docker-id/my-app + + +``` + +#### Using [JFrog Container Registry (JCR)](https://jfrog.com/container-registry) or [JFrog Artifactory](https://jfrog.com/help/r/jfrog-artifactory-documentation/getting-started-with-artifactory-as-a-docker-registry)... + +*Make sure you have a [docker-credential-helper](https://github.com/docker/docker-credential-helpers#available-programs) set up. For example, on macOS, the credential helper would be `docker-credential-osxkeychain`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my-company-docker-local.jfrog.io/my-app`, the configuration would be: + +```xml + + + my-company-docker-local.jfrog.io/my-app ``` -#### *TODO: Add more examples for common registries.* +#### Using [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/)... + +*Make sure you have a [`ACR Docker Credential Helper`](https://github.com/Azure/acr-docker-credential-helper) installed and set up. For example, on Windows, the credential helper would be `docker-credential-acr-windows`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my_acr_name.azurecr.io/my-app`, the configuration would be: + +```xml + + + my_acr_name.azurecr.io/my-app + + +``` ### Build your image @@ -130,20 +182,36 @@ Jib can also build your image directly to a Docker daemon. This uses the `docker mvn compile jib:dockerBuild ``` -If you are using [`minikube`](https://github.com/kubernetes/minikube)'s remote Docker daemon, make sure you [set up the correct environment variables](https://github.com/kubernetes/minikube/blob/master/docs/reusing_the_docker_daemon.md) to point to the remote daemon: +If you are using [`minikube`](https://github.com/kubernetes/minikube)'s remote Docker daemon, make sure you [set up the correct environment variables](https://minikube.sigs.k8s.io/docs/handbook/pushing/#1-pushing-directly-to-the-in-cluster-docker-daemon-docker-env) to point to the remote daemon: ```shell eval $(minikube docker-env) mvn compile jib:dockerBuild ``` +Alternatively, you can set environment variables in the Jib configuration. See [`dockerClient`](#dockerclient-object) for more configuration options. + +#### Build an image tarball + +You can build and save your image to disk as a tarball with: + +```shell +mvn compile jib:buildTar +``` + +This builds and saves your image to `target/jib-image.tar`, which you can load into docker with: + +```shell +docker load --input target/jib-image.tar +``` + ### Bind to a lifecycle You can also bind `jib:build` to a Maven lifecycle, such as `package`, by adding the following execution to your `jib-maven-plugin` definition: ```xml - com.google.com.tools + com.google.cloud.tools jib-maven-plugin ... @@ -163,25 +231,14 @@ Then, you can build your container image by running: mvn package ``` -### Export to a Docker context - -Jib can also export a Docker context so that you can build with Docker, if needed: - -```shell -mvn compile jib:exportDockerContext -``` - -The Docker context will be created at `target/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `jib.dockerDir` parameter: +### Additional Build Artifacts -```shell -mvn compile jib:exportDockerContext -Djib.dockerDir=my/docker/context/ -``` +As part of an image build, Jib also writes out the _image digest_ and the _image ID_. By default, these are written out to `target/jib-image.digest` and `target/jib-image.id` respectively, but the locations can be configured using the `` and `` configuration properties. See [Extended Usage](#outputpaths-object) for more details. -You can then build your image with Docker: +## Multi Module Projects -```shell -docker build -t myimage my/docker/context/ -``` +Special handling of project dependencies is recommended when building complex +multi module projects. See [Multi Module Example](https://github.com/GoogleContainerTools/jib/tree/master/examples/multi-module) for detailed information. ## Extended Usage @@ -189,45 +246,176 @@ Extended configuration options provide additional options for customizing the im Field | Type | Default | Description --- | --- | --- | --- -`from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. +`from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. -`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Gradle projects. -`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS. Leaving this parameter set to false is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`extraDirectories` | [`extraDirectories`](#extradirectories-object) | See [`extraDirectories`](#extradirectories-object) | Configures the directories used to add arbitrary files to the image. +`outputPaths` | [`outputPaths`](#outputpaths-object) | See [`outputPaths`](#outputpaths-object) | Configures the locations of additional build artifacts generated by Jib. +`dockerClient` | [`dockerClient`](#dockerclient-object) | See [`dockerClient`](#dockerclient-object) | Configures Docker for building to/from the Docker daemon. +`skaffold` | [`skaffold`](#skaffold-integration) | See [`skaffold`](#skaffold-integration) | Configures the internal skaffold goals. This configuration should only be used when integrating with [`skaffold`](#skaffold-integration). | +`containerizingMode` | string | `exploded` | If set to `packaged`, puts the JAR artifact built at `${project.build.directory}/${project.build.finalName}.jar` (the default location where many JAR-building plugins put a JAR registered as a main artifact, such as the Maven JAR Plugin) into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. +`allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. +`skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. `from` is an object with the following properties: +Property | Type | Default | Description +--- | --- |-----------------------------------------------------------| --- +`image` | string | `eclipse-temurin:{8,11,17,21,25}-jre` (or `jetty` for WAR) | The image reference for the base image. The source type can be specified using a [special type prefix](#setting-the-base-image). +`auth` | [`auth`](#auth-object) | *None* | Specifies credentials directly (alternative to `credHelper`). +`credHelper` | string | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). +`platforms` | list | See [`platform`](#platform-object) | Configures platforms of base images to select from a manifest list. + +`to` is an object with the following properties: + Property | Type | Default | Description --- | --- | --- | --- -`image` | string | `gcr.io/distroless/java` | The image reference for the base image. -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`image` | string | *Required* | The image reference for the target image. This can also be specified via the `-Dimage` command line option. If the tag is not present here `:latest` is implied. +`auth` | [`auth`](#auth-object) | *None* | Specifies credentials directly (alternative to `credHelper`). +`credHelper` | string | *None* | Specifies a credential helper that can authenticate pushing the target image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). +`tags` | list | *None* | Additional tags to push to. -`to` is an object with the following properties: +`auth` is an object with the following properties (see [Using Specific Credentials](#using-specific-credentials)): + +Property | Type +--- | --- +`username` | string +`password` | string + +`platform` is an object with the following properties: Property | Type | Default | Description --- | --- | --- | --- -`image` | string | *Required* | The image reference for the target image. This can also be specified via the `-Dimage` command line option. -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`architecture` | string | `amd64` | The architecture of a base image to select from a manifest list. +`os` | string | `linux` | The OS of a base image to select from a manifest list. + +See [How do I specify a platform in the manifest list (or OCI index) of a base image?](../docs/faq.md#how-do-i-specify-a-platform-in-the-manifest-list-or-oci-index-of-a-base-image) for examples. `container` is an object with the following properties: Property | Type | Default | Description --- | --- | --- | --- -`jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. -`mainClass` | string | *Inferred\** | The main class to launch the application from. -`args` | list | *None* | Default main method arguments to run your application with. -`ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. +`args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. +`creationTime` | string | `EPOCH` | Sets the container creation time. (Note that this property does not affect the file modification times, which are configured using ``.) The value can be `EPOCH` to set the timestamps to Epoch (default behavior), `USE_CURRENT_TIMESTAMP` to forgo reproducibility and use the real creation time, or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. +`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags`, `mainClass`, `extraClasspath`, and `expandClasspathDependencies` are ignored. You may also set `INHERIT` (`INHERIT` in old Maven versions) to indicate that the `entrypoint` and `args` should be inherited from the base image.\* +`environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | list | *None* | Additional paths in the container to prepend to the computed Java classpath. +`expandClasspathDependencies` | boolean | `false` |

    • Java 8 *or* Jib < 3.1: When set to true, does not use a wildcard (for example, `/app/lib/*`) for dependency JARs in the default Java runtime classpath but instead enumerates the JARs. Has the effect of preserving the classpath loading order as defined by the Maven project.
    • Java >= 9 *and* Jib >= 3.1: The option has no effect. Jib *always* enumerates the dependency JARs. This is achieved by [creating and using an argument file](#custom-container-entrypoint) for the `--class-path` JVM argument.
    +`filesModificationTime` | string | `EPOCH_PLUS_SECOND` | Sets the modification time (last modified time) of files in the image put by Jib. (Note that this does not set the image creation time, which can be set using ``.) The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). +`jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. +`labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). +`mainClass` | string | *Inferred*\*\* | The main class to launch the application from. +`ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`user` | string | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. +`volumes` | list | *None* | Specifies a list of mount points on the container. +`workingDirectory` | string | *None* | The working directory in the container. + +`extraDirectories` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): + +Property | Type | Default | Description +--- | --- | --- | --- +`paths` | list | `[(project-dir)/src/main/jib]` | List of [`path`](#path-object) objects and/or extra directory paths. Can be absolute or relative to the project root. +`permissions` | list | *None* | Maps file paths (glob patterns) on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. + +`path` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): + +Property | Type | Default | Description +--- | --- | --- | --- +`from` | file | `[(project-dir)/src/main/jib]` | The source directory. Can be absolute or relative to the project root. +`into` | string | `/` | The absolute unix path on the container to copy the extra directory contents into. +`includes` | list | *None* | Glob patterns for including files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. +`excludes` | list | *None* | Glob patterns for excluding files. See [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) for an example. + +`outputPaths` is an object with the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`tar` | string | `(project-dir)/target/jib-image.tar` | The path of the tarball generated by `jib:buildTar`. Relative paths are resolved relative to the project root. +`digest` | string | `(project-dir)/target/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. +`imageId` | string | `(project-dir)/target/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. +`imageJson` | string | `(project-dir)/target/jib-image.json` | The path of the image metadata json file written out during the build. Relative paths are resolved relative to the project root. -*\* Uses `mainClass` from `maven-jar-plugin` or tries to find a valid main class.* +`dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`executable` | string | `docker` | Sets the path to the Docker executable that is called to load the image into the Docker daemon. **Please note**: Users are responsible for ensuring that the Docker path passed in is valid and has the right permissions to be executed. +`environment` | map | *None* | Sets environment variables used by the Docker executable. + +#### Configuration Properties + +Each of these parameters is configurable via commandline using properties. Jib's properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: +```shell +mvn compile jib:build \ + -Djib.to.image=myregistry/myimage:latest \ + -Djib.to.auth.username=$USERNAME \ + -Djib.to.auth.password=$PASSWORD + +mvn compile jib:dockerBuild \ + -Djib.dockerClient.executable=/path/to/docker \ + -Djib.container.environment=key1="value1",key2="value2" \ + -Djib.container.args=arg1,arg2,arg3 +``` + +The following table contains additional properties that are not available as build configuration parameters: + +Property | Type | Default | Description +--- | --- | --- | --- +`jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. +`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects (i.e. `jib.baseImageCache` defaults to `[project dir]/target/jib-cache` instead of `[user cache home]/google-cloud-tools-java/jib`). +`jib.baseImageCache` | string | *Platform-dependent*\*\*\* | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. +`jib.console` | string | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. + +*\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* + +*\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* + +*\*\*\* The default base image cache is in the following locations on each platform:* + * *Linux: `[cache root]/google-cloud-tools-java/jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/.cache/` if not set)* + * *Mac: `[cache root]/Google/Jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/Library/Caches/` if not set)* + * *Windows: `[cache root]\Google\Jib\Cache`, where `[cache root]` is `%XDG_CACHE_HOME%` (`%LOCALAPPDATA%` if not set)* + +### Global Jib Configuration + +Some options can be set in the global Jib configuration file. The file is at the following locations on each platform: + +* *Linux: `[config root]/google-cloud-tools-java/jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/.config/` if not set)* +* *Mac: `[config root]/Google/Jib/config.json`, where `[config root]` is `$XDG_CONFIG_HOME` (`$HOME/Library/Preferences/` if not set)* +* *Windows: `[config root]\Google\Jib\Config\config.json`, where `[config root]` is `%XDG_CONFIG_HOME%` (`%LOCALAPPDATA%` if not set)* + +#### Properties + +* `disableUpdateCheck`: when set to true, disables the periodic up-to-date version check. +* `registryMirrors`: a list of mirror settings for each base image registry. In the following example, if the base image configured in Jib is for a Docker Hub image, then `mirror.gcr.io`, `localhost:5000`, and the Docker Hub (`registry-1.docker.io`) are tried in order until Jib can successfully pull a base image. + +```json +{ + "disableUpdateCheck": false, + "registryMirrors": [ + { + "registry": "registry-1.docker.io", + "mirrors": ["mirror.gcr.io", "localhost:5000"] + }, + { + "registry": "quay.io", + "mirrors": ["private-mirror.test.com"] + } + ] +} +``` +**Note about `mirror.gcr.io`**: it is _not_ a Docker Hub mirror but a cache. It caches [frequently-accessed public Docker Hub images](https://cloud.google.com/container-registry/docs/pulling-cached-images), and it's often possible that your base image does not exist in `mirror.gcr.io`. In that case, Jib will have to fall back to use Docker Hub. ### Example In this configuration, the image: * Is built from a base of `openjdk:alpine` (pulled from Docker Hub) -* Is pushed to `localhost:5000/my-image:built-with-jib` -* Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` +* Is pushed to `localhost:5000/my-image:built-with-jib`, `localhost:5000/my-image:tag2`, and `localhost:5000/my-image:latest` +* Runs by calling `java -Dmy.property=example.value -Xms512m -Xdebug -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp +* Has two labels (key1:value1 and key2:value2) * Is built as OCI format ```xml @@ -238,12 +426,16 @@ In this configuration, the image: localhost:5000/my-image:built-with-jib osxkeychain + + tag2 + latest + + -Dmy.property=example.value -Xms512m -Xdebug - -Xmy:flag=jib-rules mypackage.MyApp @@ -254,14 +446,114 @@ In this configuration, the image: 1000 2000-2003/udp + + value1 + value2 + OCI ``` +### Setting the Base Image + +There are three different types of base images that Jib accepts: an image from a container registry, an image stored in the Docker daemon, or an image tarball on the local filesystem. You can specify which you would like to use by prepending the `` configuration with a special prefix, listed below: + +Prefix | Example | Type +--- | --- | --- +*None* | `openjdk:11-jre` | Pulls the base image from a registry. +`registry://` | `registry://eclipse-temurin:11-jre` | Pulls the base image from a registry. +`docker://` | `docker://busybox` | Retrieves the base image from the Docker daemon. +`tar://` | `tar:///path/to/file.tar` | Uses an image tarball stored at the specified path as the base image. Also accepts relative paths (e.g. `tar://target/jib-image.tar`). + +### Adding Arbitrary Files to the Image + +You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the target directory (`/` by default) in the image, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). + +Note that Jib does not follow symbolic links in the container image. If a symbolic link is present, _it will be removed_ prior to placing the files and directories. + +You can configure different directories by using the `` parameter in your `pom.xml`: +```xml + + + + + + src/main/custom-extra-dir + + + /home/user/jib-extras + /extras + + + + +``` + +Alternatively, the `` parameter can be used as an object to set custom extra directories, as well as the extra files' permissions on the container: + +```xml + + src/main/custom-extra-dir + + + /path/on/container/to/fileA + 755 + + + /path/to/another/file + 644 + + + /glob/pattern/**/*.sh + 755 + + + +``` + +You may also specify the target of the copy and include or exclude files: + +```xml + + + + // copies the contents of 'src/main/extra-dir' into '/' on the container + src/main/extra-dir + + + // copies the contents of 'src/main/another/dir' into '/extras' on the container + src/main/another/dir + /extras + + + // copies a single-file.xml + src/main/resources/xml-files + /dest-in-container + single-file.xml + + + // copies only .txt files except for 'hidden.txt' at the source root + build/some-output + /txt-files + *.txt,**/*.txt + + hidden.txt + + + + +``` + ### Authentication Methods -Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers) or [defined in your Maven settings](#using-maven-settings). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. +Pushing/pulling from private registries require authorization credentials. + +#### Using Docker configuration files + +* Jib looks from credentials from `$XDG_RUNTIME_DIR/containers/auth.json`, `$XDG_CONFIG_HOME/containers/auth.json`, `$HOME/.config/containers/auth.json`, `$DOCKER_CONFIG/config.json`, and `$HOME/.docker/config.json`. + +See [this issue](/../../issues/101) and [`man containers-auth.json`](https://www.mankier.com/5/containers-auth.json) for more information about the files. #### Using Docker Credential Helpers @@ -272,12 +564,11 @@ Some common credential helpers include: * Google Container Registry: [`docker-credential-gcr`](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper) * AWS Elastic Container Registry: [`docker-credential-ecr-login`](https://github.com/awslabs/amazon-ecr-credential-helper) * Docker Hub Registry: [`docker-credential-*`](https://github.com/docker/docker-credential-helpers) - +* Azure Container Registry: [`docker-credential-acr-*`](https://github.com/Azure/acr-docker-credential-helper) Configure credential helpers to use by specifying them as a `credHelper` for their respective image. -*Example configuration:* +*Example configuration:* ```xml ... @@ -293,9 +584,47 @@ Configure credential helpers to use by specifying them as a `credHelper` for the ``` +#### Using Specific Credentials + +You can specify credentials directly in the `` parameter for the `from` and/or `to` images. In the example below, `to` credentials are retrieved from the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. + +```xml + + ... + + aws_account_id.dkr.ecr.region.amazonaws.com/my-base-image + + my_username + my_password + + + + gcr.io/my-gcp-project/my-app + + ${env.REGISTRY_USERNAME} + ${env.REGISTRY_PASSWORD} + + + ... + +``` + +Alternatively, you can specify credentials via commandline using the following system properties. + +Property | Description +--- | --- +`-Djib.from.auth.username` | Username for base image registry. +`-Djib.from.auth.password` | Password for base image registry. +`-Djib.to.auth.username` | Username for target image registry. +`-Djib.to.auth.password` | Password for target image registry. + +e.g. `mvn compile jib:build -Djib.to.auth.username=user -Djib.to.auth.password=pass` + +**Note:** This method of authentication should be used only as a last resort, as it is insecure to make your password visible in plain text. Note that often cloud registries (for example, Google GCR, Amazon ECR, and Azure ACR) do not accept "user credentials" (such as Gmail account name and password) but require different forms of credentials. For example, you may use [`oauth2accesstoken` or `_json_key`](https://cloud.google.com/container-registry/docs/advanced-authentication) as the username for GCR, and [`AWS`](https://serverfault.com/questions/1004915/what-is-the-proper-way-to-log-in-to-ecr) for ECR. For ACR, you may use a [_service principle_](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-auth-service-principal). + #### Using Maven Settings -Registry credentials can be added to your [Maven settings](https://maven.apache.org/settings.html). These credentials will be used if credentials could not be found in any specified Docker credential helpers. +Registry credentials can be added to your [Maven settings](https://maven.apache.org/settings.html). These credentials will be used if credentials could not be found in any specified Docker credential helpers. If you're considering putting credentials in Maven, we highly *recommend* using [maven password encryption](https://maven.apache.org/guides/mini/guide-encryption.html). @@ -314,17 +643,112 @@ If you're considering putting credentials in Maven, we highly *recommend* using ``` -* The `id` field should be the registry server these credentials are for. +* The `id` field should be the registry server these credentials are for. * We *do not* recommend putting your raw password in `settings.xml`. -## How Jib Works -See the [Jib project README](/../../#how-jib-works). +### Custom Container Entrypoint + +If you don't set ``, the default container entrypoint to launch your app will be basically `java -cp `. (The final `java` command can be further configured by setting `{||||}`.) + +Sometimes, you'll want to set a custom entrypoint to use a shell to wrap the `java` command. For example, to let `sh` or `bash` [expand environment variables](https://stackoverflow.com/a/59361658/1701388), or to have more sophisticated logic to construct a launch command. (Note, however, that running a command with a shell forks a new child process unless you run it with `exec` like `sh -c "exec java ..."`. Whether to run the JVM process as PID 1 or a child process of a PID-1 shell is a [decision you should make carefully](https://github.com/GoogleContainerTools/distroless/issues/550#issuecomment-791610603).) In this scenario, you will want to have a way inside a shell script to reliably know the default runtime classpath and the main class that Jib would use by default. To help this, Jib >= 3.1 creates two JVM argument files under `/app` (the default app root) inside the built image. + +- `/app/jib-classpath-file`: runtime classpath that Jib would use for default app launch +- `/app/jib-main-class-file`: main class + +Therefore, *for example*, the following commands will be able to launch your app: + +- (Java 9+) `java -cp @/app/jib-classpath-file @/app/jib-main-class-file` +- (with shell) `java -cp $( cat /app/jib-classpath-file ) $( cat /app/jib-main-class-file )` + + +### Jib Extensions + +The Jib build plugins have an extension framework that enables anyone to easily extend Jib's behavior to their needs. We maintain select [first-party](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party) plugins for popular use cases like [fine-grained layer control](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-layer-filter-extension-gradle), builds a [GraalVM native image](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-native-image-extension-maven), and [Quarkus support](https://github.com/GoogleContainerTools/jib-extensions/tree/master/first-party/jib-quarkus-extension-gradle), but anyone can write and publish an extension. Check out the [jib-extensions](https://github.com/GoogleContainerTools/jib-extensions) repository for more information. + + +### WAR Projects + +Jib also containerizes WAR projects. If the Maven project uses [the `war`-packaging type](https://maven.apache.org/plugins/maven-war-plugin/index.html), Jib will by default use [`jetty`](https://hub.docker.com/_/jetty) as a base image to deploy the project WAR. No extra configuration is necessary other than having the packaging type to `war`. -## Frequently Asked Questions (FAQ) +Note that Jib will work slightly differently for WAR projects from JAR projects: + - `` and `` are ignored. + - The WAR will be exploded into `/var/lib/jetty/webapps/ROOT`, which is the expected WAR location for the Jetty base image. -See the [Jib project README](/../../#frequently-asked-questions-faq). +To use a different Servlet engine base image, you can customize ``, ``, and ``. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may not need to configure them. However, you will most likely have to set `` to a proper location depending on the base image. Here is an example of using a Tomcat image: + +```xml + + + tomcat:8.5-jre8-alpine + + + + /usr/local/tomcat/webapps/ROOT + + +``` +When specifying a [`jetty`](https://hub.docker.com/_/jetty) image yourself with ``, you may run into an issue ([#3204](https://github.com/GoogleContainerTools/jib/issues/3204)) and need to override the entrypoint. +```xml + + + jetty:11.0.2-jre11 + + + java,-jar,/usr/local/jetty/start.jar + + +``` + + +### Skaffold Integration + +Jib is an included builder in [Skaffold](https://github.com/GoogleContainerTools/skaffold). Jib passes build information to skaffold through special internal goals so that skaffold understands when it should rebuild or synchronize files. For complex builds, the defaults may not be sufficient, so the jib plugin provides a `skaffold` configuration object which exposes: + +Field | Type | Default | Description +--- | --- | --- | --- +`watch` | [`watch`](#skaffold-watch-object) | *None* | Additional configuration for file watching +`sync` | [`sync`](#skaffold-sync-object) | *None* | Additional configuration for file synchronization + +`watch` is an object with the following properties: + +Field | Type | Default | Description +--- | --- | --- | --- +`buildIncludes` | `List` | *None* | Additional build files that skaffold should watch +`includes` | `List` | *None* | Additional project files or directories that skaffold should watch +`excludes` | `List` | *None* | Files and directories that skaffold should not watch + +`sync` is an object with the following properties: + +Field | Type | Default | Description +--- | --- | --- | --- +`excludes` | `List` | *None* | Files and directories that skaffold should not sync + +## Need Help? + +A lot of questions are already answered! + +* [Frequently Asked Questions (FAQ)](../docs/faq.md) +* [Stack Overflow](https://stackoverflow.com/questions/tagged/jib) +* [GitHub issues](https://github.com/GoogleContainerTools/jib/issues) + +_For usage questions, please ask them on Stack Overflow._ + +## Privacy + +See the [Privacy page](docs/privacy.md). + +## Upcoming Features + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. ## Community See the [Jib project README](/../../#community). + +## Disclaimer + +This is not an officially supported Google product. diff --git a/jib-maven-plugin/build.gradle b/jib-maven-plugin/build.gradle new file mode 100644 index 0000000000..f8ead158cd --- /dev/null +++ b/jib-maven-plugin/build.gradle @@ -0,0 +1,88 @@ +plugins { + id 'io.freefair.maven-plugin' + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' +} + +// only maven specific dependencies should be versioned, everything else should be defined by constrains in +// parent build.gradle +dependencies { + sourceProject project(':jib-core') + sourceProject project(':jib-plugins-common') + ensureNoProjectDependencies() + + implementation dependencyStrings.MAVEN_EXTENSION + + implementation dependencyStrings.MAVEN_API + implementation dependencyStrings.MAVEN_CORE + + // compileOnly + testImplementation equivalent to "provided" + compileOnly dependencyStrings.MAVEN_PLUGIN_ANNOTATIONS + // needed to suppress "unknown enum constant" warnings + testImplementation dependencyStrings.MAVEN_PLUGIN_ANNOTATIONS + + // maven-plugin-testing-harness pulls in conflicting implementations of DefaultPlexusContainer + // (sisu (correct) vs default-plexus-container (wrong)) so ensure this is first in the test classpath + testImplementation dependencyStrings.SISU_PLEXUS + testImplementation dependencyStrings.MAVEN_TESTING_HARNESS + + testImplementation dependencyStrings.JUNIT + testImplementation dependencyStrings.TRUTH + testImplementation dependencyStrings.TRUTH8 + testImplementation dependencyStrings.MOCKITO_CORE + testImplementation dependencyStrings.SLF4J_API + testImplementation dependencyStrings.SYSTEM_RULES + + testImplementation dependencyStrings.MAVEN_VERIFIER + testImplementation dependencyStrings.MAVEN_COMPAT + testImplementation dependencyStrings.SLF4J_SIMPLE + + integrationTestImplementation dependencyStrings.JBCRYPT + + testImplementation project(path:':jib-plugins-common', configuration:'tests') + integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') +} + +/* TESTING */ +test.dependsOn publishToMavenLocal +integrationTest.dependsOn publishToMavenLocal +/* TESTING */ + +/* RELEASE */ +configureMavenRelease() + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Jib' + description = 'A Maven plugin for building container images for your Java applications.' + } + } + } +} + + +release { + tagTemplate = 'v$version-maven' + ignoredSnapshotDependencies = [ + 'com.google.cloud.tools:jib-core', + 'com.google.cloud.tools:jib-plugins-common', + ] + git { + requireBranch = /^maven-release-v\d+.*$/ //regex + } +} +/* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] +eclipse.classpath.file.whenMerged { + entries.each { + if (it.path == 'src/test/resources') { + it.excludes += 'maven/projects/' + } + } +} +/* ECLIPSE */ diff --git a/jib-maven-plugin/config/checkstyle-header.xml b/jib-maven-plugin/config/checkstyle-header.xml deleted file mode 100644 index 3fb2952123..0000000000 --- a/jib-maven-plugin/config/checkstyle-header.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/jib-maven-plugin/config/google-checks-no-indent.xml b/jib-maven-plugin/config/google-checks-no-indent.xml deleted file mode 100644 index 9efa9a7967..0000000000 --- a/jib-maven-plugin/config/google-checks-no-indent.xml +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties new file mode 100644 index 0000000000..c15987a85c --- /dev/null +++ b/jib-maven-plugin/gradle.properties @@ -0,0 +1 @@ +version = 3.5.3-SNAPSHOT diff --git a/jib-maven-plugin/kokoro/release_build.sh b/jib-maven-plugin/kokoro/release_build.sh index 6fa083b173..185ef98611 100644 --- a/jib-maven-plugin/kokoro/release_build.sh +++ b/jib-maven-plugin/kokoro/release_build.sh @@ -1,14 +1,14 @@ #!/bin/bash # Fail on any error. -set -e +set -o errexit # Display commands to stderr. -set -x +set -o xtrace -cd github/jib/jib-maven-plugin -./mvnw -Prelease -B -U verify +# Append to JAVA_TOOL_OPTIONS to suppress warnings from kokoro container os. +if [ "${KOKORO_JOB_CLUSTER}" = "GCP_UBUNTU_DOCKER" ]; then + JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS} -Xlog:os+container=error" +fi -# copy pom with the name expected in the Maven repository -ARTIFACT_ID=$(mvn -B help:evaluate -Dexpression=project.artifactId 2>/dev/null | grep -v "^\[") -PROJECT_VERSION=$(mvn -B help:evaluate -Dexpression=project.version 2>/dev/null| grep -v "^\[") -cp pom.xml target/${ARTIFACT_ID}-${PROJECT_VERSION}.pom +cd github/jib +./gradlew :jib-maven-plugin:prepareRelease diff --git a/jib-maven-plugin/kokoro/release_publish.sh b/jib-maven-plugin/kokoro/release_publish.sh deleted file mode 100644 index 1243c4b313..0000000000 --- a/jib-maven-plugin/kokoro/release_publish.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e - -CREDENTIALS="$SONATYPE_USERNAME:$SONATYPE_PASSWORD" - -# Display commands to stderr. -set -x - -# Goes to the GCS directory. -cd $KOKORO_GFILE_DIR - -# Finds the latest ubuntu/release-sign build directory. -LAST_BUILD=$(ls prod/jib/maven/ubuntu/release-sign/ | sort -rV | head -1) - -# Finds the bundled jar file in the latest signed artifact directory. -BUNDLED_JAR_FILE=$(find `pwd`/prod/jib/maven/ubuntu/release-sign/${LAST_BUILD}/* -type f \( -iname \*-bundle.jar \)) - -# Usage: GetSessionID -# Uses the environment variable CREDENTIALS. -# Stores the Nexus session ID in the given variable. -GetSessionID() { - local __resultvar=$1 - - # Makes a temporary file to store the login cookies. - local cookies_temp=$(mktemp /tmp/sonatype_cookies.XXXXXXX) - - # Sends a login request. - { local login_response=$(curl 'https://oss.sonatype.org/service/local/authentication/login' -X 'GET' -u "$CREDENTIALS" -c $cookies_temp 2> /dev/null); } 2> /dev/null - - # Checks if login was successful. - echo $login_response | grep -q 'true' - - local login_check=$(echo -n $?) - - if [ "$login_check" -eq "1" ]; then - return 1 - fi - - # Extracts the session ID from the cookies. - local nxsessionid - local nxsessionid_line=$(cat $cookies_temp | grep 'NXSESSIONID') - nxsessionid=$(echo -n $nxsessionid_line | awk '{print $NF}') - - eval $__resultvar="'$nxsessionid'" -} - -# Usage: UploadJAR -# Uploads the bundled JAR file to the Nexus Staging Repository. -UploadJAR() { - curl 'https://oss.sonatype.org/service/local/staging/bundle_upload' -H "Cookie: NXSESSIONID=$1" -H 'Content-Type: multipart/form-data' --compressed -F "file=@$2" -} - -# Gets the session ID. -GetSessionID NXSESSIONID -if [ $? -eq 1 ]; then - echo 'Login failed!' - exit 1 -fi -echo 'Login successful.' - -# Uploads the bundled JAR file. -echo 'Uploading artifact...' -UploadJAR $NXSESSIONID $BUNDLED_JAR_FILE diff --git a/jib-maven-plugin/kokoro/release_sign.sh b/jib-maven-plugin/kokoro/release_sign.sh deleted file mode 100644 index 093305378d..0000000000 --- a/jib-maven-plugin/kokoro/release_sign.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e -# Display commands being run. -set -x - -cd $KOKORO_GFILE_DIR -mkdir signed && chmod 777 signed - -# find the latest directory under prod/jib/maven/gcp_ubuntu/release-build/ -LAST_BUILD=$(ls prod/jib/maven/gcp_ubuntu/release-build/ | sort -rV | head -1) - -# find the jars and the pom in the latest build artifact directory -FILES=$(find `pwd`/prod/jib/maven/gcp_ubuntu/release-build/${LAST_BUILD}/* -type f \( -iname \*.jar -o -iname \*.pom \)) - -for f in $FILES -do - echo "Processing $f file..." - filename=$(basename "$f") - mv $f signed/$filename - if /escalated_sign/escalated_sign.py -j /escalated_sign_jobs -t linux_gpg_sign \ - `pwd`/signed/$filename - then echo "Signed $filename" - else - echo "Could not sign $filename" - exit 1 - fi -done - -# bundle the artifacts for manual deploy to the Maven staging repository -cd signed -POM_NAME=$(ls *.pom) -BUNDLE_NAME=${POM_NAME%.pom}-bundle.jar -jar -cvf ${BUNDLE_NAME} * diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml deleted file mode 100644 index 5c4d3214ff..0000000000 --- a/jib-maven-plugin/pom.xml +++ /dev/null @@ -1,436 +0,0 @@ - - 4.0.0 - - com.google.cloud.tools - jib-maven-plugin - 0.9.7-SNAPSHOT - maven-plugin - - Jib - A tool for building container images for your Java applications. - https://github.com/GoogleContainerTools/jib - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - coollog - Qingyang Chen - qingyangc@google.com - - - loosebazooka - Appu Goundan - appu@google.com - - - - - scm:git:https://github.com/GoogleContainerTools/jib.git - - scm:git:https://github.com/GoogleContainerTools/jib.git - - https://github.com/GoogleContainerTools/jib - - - - UTF-8 - UTF-8 - 1.8 - 1.8 - - - - - - com.google.http-client - google-http-client - 1.23.0 - compile - - - org.apache.commons - commons-compress - 1.17 - compile - - - com.google.guava - guava - 23.5-jre - compile - - - com.fasterxml.jackson.core - jackson-databind - 2.9.6 - compile - - - org.slf4j - slf4j-api - 1.7.25 - compile - - - org.javassist - javassist - 3.22.0-GA - compile - - - - - org.apache.maven - maven-plugin-api - 3.5.2 - - - - - org.apache.maven.plugin-tools - maven-plugin-annotations - 3.5 - provided - - - - org.apache.maven - maven-core - 3.5.2 - - - org.apache.maven.shared - maven-verifier - 1.6 - - - - junit - junit - 4.12 - test - - - - org.mockito - mockito-core - 2.12.0 - test - - - - - - - integration-tests - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.21.0 - - ${project.build.directory}/classes - - **/*IntegrationTest.java - - - <_JIB_DISABLE_USER_AGENT>true - - - - - integration-tests - - integration-test - verify - - - - - - - - - - - release - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.0 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar - - - - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - add-jib-core-sources - generate-sources - - add-source - - - - ${basedir}/../jib-core/src/main/java - - - - - add-jib-core-resources - generate-sources - - add-resource - - - - - ${basedir}/../jib-core/src/main/resources - - - - - - add-jib-core-test-sources - generate-test-sources - - add-test-source - - - - ${basedir}/../jib-core/src/test/java - - - - - add-jib-core-test-resources - generate-test-sources - - add-test-resource - - - - - ${basedir}/../jib-core/src/test/resources - - - - - - add-jib-core-integration-test-sources - generate-test-sources - - add-test-source - - - - ${basedir}/../jib-core/src/integration-test/java - - - - - add-jib-core-integration-test-resources - generate-test-sources - - add-test-resource - - - - - ${basedir}/../jib-core/src/integration-test/resources - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.7.0 - - - compile-with-nullaway - compile - - compile - - - javac-with-errorprone - true - - ${maven.compiler.source} - ${maven.compiler.target} - true - - - com.uber.nullaway - nullaway - 0.4.2 - - - - -Xep:NullAway:ERROR - -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools - - - - - - - org.codehaus.plexus - plexus-compiler-javac-errorprone - 2.8 - - - - com.google.errorprone - error_prone_core - 2.2.0 - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.20.1 - - - **/*IntegrationTest.java - - - - - - org.apache.maven.plugins - maven-plugin-plugin - 3.5 - - - - - com.coveo - fmt-maven-plugin - 2.5.0 - - - com.google.googlejavaformat - google-java-format - 1.6 - - - - - validate - - check - - - - - - - - maven-checkstyle-plugin - 2.17 - - - com.puppycrawl.tools - checkstyle - 8.7 - - - - true - true - warning - ${project.build.sourceDirectory} - - - - google-checks - validate - - check - - - config/google-checks-no-indent.xml - - - - header-check - validate - - check - - - config/checkstyle-header.xml - config/header-java.txt - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.0 - - - - - true - true - - - - - - - - diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh deleted file mode 100755 index a738fe8c54..0000000000 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -# Usage: ./scripts/prepare_release.sh - -set -e - -Colorize() { - echo "$(tput setff $2)$1$(tput sgr0)" -} - -EchoRed() { - echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" -} -EchoGreen() { - echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" -} - -Die() { - EchoRed "$1" - exit 1 -} - -DieUsage() { - Die "Usage: ./scripts/prepare_release.sh " -} - -# Usage: CheckVersion -CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." -} - -# Usage: IncrementVersion -IncrementVersion() { - local version=$1 - local minorVersion=$(echo $version | sed 's/[0-9][0-9]*\.[0-9][0-9]*\.\([0-9][0-9]\)*/\1/') - local nextMinorVersion=$((minorVersion+1)) - echo $version | sed "s/\([0-9][0-9]*\.[0-9][0-9]*\)\.[0-9][0-9]*/\1.$nextMinorVersion/" -} - -[ $# -ne 2 ] || DieUsage - -EchoGreen '===== RELEASE SETUP SCRIPT =====' - -VERSION=$1 -CheckVersion ${VERSION} - -NEXT_VERSION=$(IncrementVersion $VERSION) -CheckVersion ${NEXT_VERSION} - -if [[ $(git status -uno --porcelain) ]]; then - Die 'There are uncommitted changes.' -fi - -# Runs integration tests. -./mvnw -X -Pintegration-tests verify - -# Checks out a new branch for this version release (eg. 1.5.7). -BRANCH=maven_release_v${VERSION} -git checkout -b ${BRANCH} - -# Updates the pom.xml with the version to release. -mvn versions:set versions:commit -DnewVersion=${VERSION} - -# Tags a new commit for this release. -TAG=v${VERSION}-maven -git commit -am "preparing release ${VERSION}" -git tag ${TAG} - -# Updates the pom.xml with the next snapshot version. -# For example, when releasing 1.5.7, the next snapshot version would be 1.5.8-SNAPSHOT. -NEXT_SNAPSHOT=${NEXT_VERSION}-SNAPSHOT -mvn versions:set versions:commit -DnewVersion=${NEXT_SNAPSHOT} - -# Commits this next snapshot version. -git commit -am "${NEXT_SNAPSHOT}" - -# Pushes the tag and release branch to Github. -git push origin ${BRANCH} -git push origin ${TAG} - -# File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. -EchoGreen 'File a PR for the new release branch:' -echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} diff --git a/jib-maven-plugin/scripts/update_gcs_latest.sh b/jib-maven-plugin/scripts/update_gcs_latest.sh new file mode 100755 index 0000000000..2b71b00a63 --- /dev/null +++ b/jib-maven-plugin/scripts/update_gcs_latest.sh @@ -0,0 +1,41 @@ +#!/bin/bash - +# Usage: ./jib-maven-plugin/scripts/update_gcs_latest.sh + +set -o errexit + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." +} + +[ $# -ne 1 ] && Die "Usage: ./jib-maven-plugin/scripts/update_gcs_latest.sh " + +CheckVersion $1 + +versionString="{\"latest\":\"$1\"}" +destination="gs://jib-versions/jib-maven" + +echo $versionString > jib-maven +gcloud storage cp jib-maven $destination +gcloud storage objects update --add-acl-grant=entity=allUsers,role=READER $destination +rm jib-maven + +gcsResult=$(curl https://storage.googleapis.com/jib-versions/jib-maven) +if [ "$gcsResult" == "$versionString" ] +then + EchoGreen "Version updated successfully" +else + Die "Version update failed" +fi \ No newline at end of file diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java new file mode 100644 index 0000000000..d41781d223 --- /dev/null +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -0,0 +1,288 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.tools.jib.Command; +import java.io.IOException; +import java.security.DigestException; +import java.util.Arrays; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for {@link BuildDockerMojo}. */ +public class BuildDockerMojoIntegrationTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); + + @ClassRule + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); + + private static void buildToDockerDaemon(TestProject project, String imageReference, String pomXml) + throws VerificationException, DigestException, IOException { + Verifier verifier = new Verifier(project.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setAutoclean(false); + verifier.addCliOption("--file=" + pomXml); + verifier.executeGoal("package"); + + verifier.executeGoal("jib:dockerBuild"); + verifier.verifyErrorFreeLog(); + + BuildImageMojoIntegrationTest.readDigestFile( + project.getProjectRoot().resolve("target/jib-image.digest")); + } + + /** + * Builds and runs jib:buildDocker on a project at {@code projectRoot} pushing to {@code + * imageReference}. + */ + private static String buildToDockerDaemonAndRun(TestProject project, String imageReference) + throws VerificationException, IOException, InterruptedException, DigestException { + buildToDockerDaemon(project, imageReference, "pom.xml"); + + String dockerInspectVolumes = + new Command("docker", "inspect", "-f", "'{{json .Config.Volumes}}'", imageReference).run(); + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + + MatcherAssert.assertThat( + dockerInspectVolumes, CoreMatchers.containsString("\"/var/log\":{},\"/var/log2\":{}")); + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/udp\":{},\"2001/udp\":{},\"2002/udp\":{},\"2003/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + + return new Command("docker", "run", "--rm", imageReference).run(); + } + + @Test + public void testExecute_simple() + throws VerificationException, IOException, InterruptedException, DigestException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", + buildToDockerDaemonAndRun(simpleTestProject, targetImage)); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + } + + @Test + public void testExecute_simple_extraDirectoriesFiltering() + throws DigestException, IOException, InterruptedException, VerificationException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-extra-dirs-filtering.xml"); + String output = + new Command("docker", "run", "--rm", "--entrypoint=ls", targetImage, "-1R", "/extras") + .run(); + + // /extras/cat.txt + // /extras/foo + // /extras/sub/ + // /extras/sub/a.json + assertThat(output).isEqualTo("/extras:\ncat.txt\nfoo\nsub\n\n/extras/sub:\na.json\n"); + } + + @Test + public void testExecute_dockerClient() + throws VerificationException, IOException, InterruptedException { + Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); + new Command( + "chmod", "+x", simpleTestProject.getProjectRoot().resolve("mock-docker.sh").toString()) + .run(); + + String targetImage = "simpleimage:maven" + System.nanoTime(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setAutoclean(false); + verifier.addCliOption("--file=pom-dockerclient.xml"); + verifier.addCliOption("--debug"); + verifier.executeGoal("package"); + + verifier.executeGoal("jib:dockerBuild"); + verifier.verifyTextInLog("Docker load called. value1 value2"); + verifier.verifyErrorFreeLog(); + } + + @Test + public void testExecute_empty() + throws InterruptedException, IOException, VerificationException, DigestException { + String targetImage = "emptyimage:maven" + System.nanoTime(); + + Assert.assertEquals("", buildToDockerDaemonAndRun(emptyTestProject, targetImage)); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + } + + @Test + public void testExecute_defaultTarget() + throws VerificationException, IOException, InterruptedException, DigestException { + Assert.assertEquals( + "Hello, world. An argument.\n", + buildToDockerDaemonAndRun( + defaultTargetTestProject, "default-target-name:default-target-version")); + } + + @Test + public void testExecute_jibSkip() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibSkip(emptyTestProject, BuildDockerMojo.GOAL_NAME); + } + + @Test + public void testExecute_jibContainerizeSkips() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibContainerizeSkips(emptyTestProject, BuildDockerMojo.GOAL_NAME); + } + + @Test + public void testExecute_userNumeric() + throws VerificationException, IOException, InterruptedException, DigestException { + String targetImage = "emptyimage:maven" + System.nanoTime(); + buildToDockerDaemon(emptyTestProject, targetImage, "pom.xml"); + Assert.assertEquals( + "12345:54321", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } + + @Test + public void testExecute_userNames() + throws VerificationException, IOException, InterruptedException, DigestException { + String targetImage = "brokenuserimage:maven" + System.nanoTime(); + buildToDockerDaemon(emptyTestProject, targetImage, "pom-broken-user.xml"); + Assert.assertEquals( + "myuser:mygroup", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } + + @Test + public void testExecute_noToImageAndInvalidProjectName() + throws DigestException, VerificationException, IOException, InterruptedException { + buildToDockerDaemon(simpleTestProject, "image reference ignored", "pom-no-to-image.xml"); + Assert.assertEquals( + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", "my-artifact-id:1").run()); + } + + @Test + public void testExecute_jarContainerization() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "jarcontainerizationimage:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-jar-containerization.xml"); + Assert.assertEquals( + "Hello, world. \nImplementation-Title: hello-world\nImplementation-Version: 1\n", + new Command("docker", "run", "--rm", targetImage).run()); + } + + @Test + public void testExecute_jarContainerizationOnMissingJar() throws IOException { + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", "erroronmissingjar"); + verifier.setAutoclean(false); + verifier.addCliOption("--file=pom-jar-containerization.xml"); + verifier.executeGoals(Arrays.asList("clean", "jib:dockerBuild")); + Assert.fail(); + + } catch (VerificationException ex) { + MatcherAssert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Obtaining project build output files failed; make sure you have packaged your " + + "project before trying to build the image. (Did you accidentally run \"mvn " + + "clean jib:build\" instead of \"mvn clean package jib:build\"?)")); + } + } + + @Test + public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + // this plugin should match 1.0 + verifier.setSystemProperty("jib.requiredVersion", "1.0"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); + verifier.verifyErrorFreeLog(); + } + + @Test + public void testExecute_jibRequireVersion_fail() throws IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); + Assert.fail(); + } catch (VerificationException ex) { + MatcherAssert.assertThat( + ex.getMessage(), CoreMatchers.containsString("but is required to be [,1.0]")); + } + } + + @Test + public void testCredHelperConfigurationSimple() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-cred-helper-1.xml"); + Assert.assertEquals( + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", targetImage).run()); + } + + @Test + public void testCredHelperConfigurationComplex() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-cred-helper-2.xml"); + Assert.assertEquals( + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", targetImage).run()); + } + + @Test + public void testMultiPlatform() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "multiplatformproject:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-multiplatform-build.xml"); + Assert.assertEquals( + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", targetImage).run()); + } +} diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java new file mode 100644 index 0000000000..52675002c2 --- /dev/null +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -0,0 +1,811 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.HttpRequestTester; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.base.Splitter; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Integration tests for {@link BuildImageMojo}. */ +public class BuildImageMojoIntegrationTest { + + @ClassRule + public static final LocalRegistry localRegistry = + new LocalRegistry(5000, "testuser", "testpassword"); + + private static final String dockerHost = + System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost"; + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); + + @ClassRule public static final TestProject skippedTestProject = new TestProject("empty"); + + @ClassRule + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); + + @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); + + @ClassRule public static final TestProject springBootProject = new TestProject("spring-boot"); + + private static String getTestImageReference(String label) { + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; + return nameBase + label + System.nanoTime(); + } + + static String readDigestFile(Path digestPath) throws IOException, DigestException { + assertThat(Files.exists(digestPath)).isTrue(); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + return DescriptorDigest.fromDigest(digest).toString(); + } + + private static boolean isJava11RuntimeOrHigher() { + Iterable split = Splitter.on(".").split(System.getProperty("java.version")); + return Integer.valueOf(split.iterator().next()) >= 11; + } + + private static Verifier build( + Path projectRoot, String imageReference, String pomXml, boolean buildTwice) + throws VerificationException, IOException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + if (imageReference.startsWith(dockerHost)) { + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + } + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=" + pomXml); + verifier.executeGoals(Arrays.asList("clean", "compile")); + + if (!buildTwice) { + verifier.executeGoal("jib:build"); + return verifier; + } + + // Builds twice, and checks if the second build took less time. + verifier.addCliOption("-Djib.alwaysCacheBaseImage=true"); + verifier.executeGoal("jib:build"); + float timeOne = getBuildTimeFromVerifierLog(verifier); + + verifier.resetStreams(); + verifier.executeGoal("jib:build"); + float timeTwo = getBuildTimeFromVerifierLog(verifier); + + // The first build should take longer than the second build. + assertThat(timeOne).isGreaterThan(timeTwo); + return verifier; + } + + /** + * Builds with {@code jib:build} on a project at {@code projectRoot} pushing to {@code + * imageReference} and run the image after pulling it. + */ + private static String buildAndRun( + Path projectRoot, String imageReference, String pomXml, boolean buildTwice) + throws VerificationException, IOException, InterruptedException, DigestException { + build(projectRoot, imageReference, pomXml, buildTwice).verifyErrorFreeLog(); + + String output = pullAndRunBuiltImage(imageReference); + + try { + // Test pulling/running using image digest + String digest = readDigestFile(projectRoot.resolve("target/jib-image.digest")); + String imageReferenceWithDigest = + ImageReference.parse(imageReference).withQualifier(digest).toString(); + assertThat(pullAndRunBuiltImage(imageReferenceWithDigest)).isEqualTo(output); + + // Test running using image id + String id = readDigestFile(projectRoot.resolve("target/jib-image.id")); + assertThat(id).isNotEqualTo(digest); + assertThat(new Command("docker", "run", "--rm", id).run()).isEqualTo(output); + + } catch (InvalidImageReferenceException ex) { + throw new AssertionError("error replacing tag with digest", ex); + } + + return output; + } + + private static String buildAndRunFromLocalBase( + Path projectRoot, String targetImage, String baseImage, boolean buildTwice) + throws VerificationException, IOException, InterruptedException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setSystemProperty("_BASE_IMAGE", baseImage); + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=pom-localbase.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile")); + if (!buildTwice) { + verifier.executeGoal("jib:build"); + return pullAndRunBuiltImage(targetImage); + } + + verifier.executeGoal("jib:build"); + float timeOne = getBuildTimeFromVerifierLog(verifier); + + verifier.resetStreams(); + verifier.executeGoal("jib:build"); + float timeTwo = getBuildTimeFromVerifierLog(verifier); + + // The first build should take longer than the second build. + assertThat(timeOne).isGreaterThan(timeTwo); + return pullAndRunBuiltImage(targetImage); + } + + private static String buildAndRunAdditionalTag( + Path projectRoot, String imageReference, String additionalTag) + throws VerificationException, InvalidImageReferenceException, IOException, + InterruptedException, DigestException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); + if (imageReference.startsWith(dockerHost)) { + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + } + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + String additionalImageReference = + ImageReference.parse(imageReference).withQualifier(additionalTag).toString(); + + String output = pullAndRunBuiltImage(imageReference); + String additionalOutput = pullAndRunBuiltImage(additionalImageReference); + assertThat(additionalOutput).isEqualTo(output); + + String digest = readDigestFile(projectRoot.resolve("target/jib-image.digest")); + String digestImageReference = + ImageReference.parse(imageReference).withQualifier(digest).toString(); + String digestOutput = pullAndRunBuiltImage(digestImageReference); + assertThat(digestOutput).isEqualTo(output); + + assertThat(getCreationTime(imageReference)).isEqualTo(Instant.EPOCH); + assertThat(getCreationTime(additionalImageReference)).isEqualTo(Instant.EPOCH); + + return output; + } + + private static String buildAndRunComplex(String imageReference, String pomFile) + throws VerificationException, IOException, InterruptedException { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_DOCKER_HOST", dockerHost); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setSystemProperty("_TARGET_USERNAME", "testuser"); + verifier.setSystemProperty("_TARGET_PASSWORD", "testpassword"); + verifier.setSystemProperty("sendCredentialsOverHttp", "true"); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=" + pomFile); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + // Verify output + localRegistry.pull(imageReference); + assertDockerInspectParameters(imageReference); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + /** + * Pulls a built image and attempts to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + assertDockerInspectParameters(imageReference); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + private static void assertDockerInspectParameters(String imageReference) + throws IOException, InterruptedException { + String dockerInspectExposedPorts = + new Command("docker", "inspect", "-f", "'{{json .Config.ExposedPorts}}'", imageReference) + .run(); + String dockerInspectLabels = + new Command("docker", "inspect", "-f", "'{{json .Config.Labels}}'", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + + MatcherAssert.assertThat( + dockerInspectExposedPorts, + CoreMatchers.containsString( + "\"1000/tcp\":{},\"2000/udp\":{},\"2001/udp\":{},\"2002/udp\":{},\"2003/udp\":{}")); + MatcherAssert.assertThat( + dockerInspectLabels, + CoreMatchers.containsString("\"key1\":\"value1\",\"key2\":\"value2\"")); + assertThat(history).contains("jib-maven-plugin"); + } + + private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOException { + Pattern pattern = Pattern.compile("Building and pushing image : (?

    The source files are specified as a list instead of a set to define the order in which - * they are added. - * - * @param sourceFiles the source files to build from. Source files that are directories will - * have all subfiles in the directory added (but not the directory itself) - * @param destinationOnImage Unix-style path to add the source files to in the container image - * filesystem - * @return this - */ - public Builder addEntry(List sourceFiles, String destinationOnImage) { - Preconditions.checkArgument(!sourceFiles.contains(null)); - this.layerEntries.add(new LayerEntry(ImmutableList.copyOf(sourceFiles), destinationOnImage)); - return this; - } - - /** - * Returns the built {@link LayerConfiguration}. - * - * @return the built {@link LayerConfiguration} - */ - public LayerConfiguration build() { - return new LayerConfiguration(layerEntries.build()); - } - } - - public static Builder builder() { - return new Builder(); - } - - private final ImmutableList layerEntries; - - /** - * Constructs a new layer configuration. - * - * @param layerEntries the list of {@link LayerEntry}s - */ - private LayerConfiguration(ImmutableList layerEntries) { - this.layerEntries = layerEntries; - } - - public ImmutableList getLayerEntries() { - return layerEntries; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java deleted file mode 100644 index 88f7f90cfb..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import java.util.Objects; -import javax.annotation.Nullable; - -/** Holds port number and protocol for an exposed port. */ -public class Port { - - /** Represents the protocol portion of the port. */ - public enum Protocol { - TCP("tcp"), - UDP("udp"); - - private final String stringRepresentation; - - Protocol(String stringRepresentation) { - this.stringRepresentation = stringRepresentation; - } - - @Override - public String toString() { - return stringRepresentation; - } - - /** - * Returns the {@link Protocol} given its string representation. - * - * @param protocolString the case insensitive string (e.g. "tcp", "udp") - * @return the {@link Protocol} with the corresponding name, or {@code Protocol#TCP} by default - * if invalid - */ - public static Protocol getFromString(@Nullable String protocolString) { - if (protocolString == null) { - return TCP; - } - return UDP.toString().equalsIgnoreCase(protocolString) ? UDP : TCP; - } - } - - private final int port; - private final Protocol protocol; - - public Port(int port, Protocol protocol) { - this.port = port; - this.protocol = protocol; - } - - public int getPort() { - return port; - } - - public Protocol getProtocol() { - return protocol; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - if (other == null || !(other instanceof Port)) { - return false; - } - Port otherPort = (Port) other; - return port == otherPort.port && protocol == otherPort.protocol; - } - - @Override - public int hashCode() { - return Objects.hash(port, protocol); - } - - @Override - public String toString() { - return port + "/" + protocol; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java new file mode 100644 index 0000000000..37d0e3ef2d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java @@ -0,0 +1,314 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.docker; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; +import com.google.cloud.tools.jib.api.ImageDetails; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; +import com.google.cloud.tools.jib.image.ImageTarball; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Calls out to the {@code docker} CLI. */ +public class CliDockerClient implements DockerClient { + + /** + * Contains the size, image ID, and diff IDs of an image inspected with {@code docker inspect}. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class DockerImageDetails implements JsonTemplate, ImageDetails { + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class RootFsTemplate implements JsonTemplate { + @JsonProperty("Layers") + private final List layers = Collections.emptyList(); + } + + @JsonProperty("Size") + private long size; + + @JsonProperty("Id") + private String imageId = ""; + + @JsonProperty("RootFS") + private final RootFsTemplate rootFs = new RootFsTemplate(); + + @Override + public long getSize() { + return size; + } + + @Override + public DescriptorDigest getImageId() throws DigestException { + return DescriptorDigest.fromDigest(imageId); + } + + /** + * Return a list of diff ids of the layers in the image. + * + * @return a list of diff ids + * @throws DigestException if a digest is invalid + */ + @Override + public List getDiffIds() throws DigestException { + List processedDiffIds = new ArrayList<>(rootFs.layers.size()); + for (String diffId : rootFs.layers) { + processedDiffIds.add(DescriptorDigest.fromDigest(diffId.trim())); + } + return processedDiffIds; + } + } + + /** Default path to the docker executable. */ + public static final Path DEFAULT_DOCKER_CLIENT = Paths.get("docker"); + + /** + * 10 minute timeout to ensure that Jib doesn't get stuck indefinitely when expecting a docker + * output. + */ + public static final Long DOCKER_OUTPUT_TIMEOUT = (long) 10 * 60 * 1000; + + /** + * Checks if Docker is installed on the user's system by running the `docker` command. + * + * @return {@code true} if Docker is installed on the user's system and accessible + */ + public static boolean isDefaultDockerInstalled() { + try { + new ProcessBuilder(DEFAULT_DOCKER_CLIENT.toString()).start(); + return true; + } catch (IOException ex) { + return false; + } + } + + /** + * Checks if Docker is installed on the user's system and by verifying if the executable path + * provided has the appropriate permissions. + * + * @param dockerExecutable path to the executable to test running + * @return {@code true} if Docker is installed on the user's system and accessible + */ + public static boolean isDockerInstalled(Path dockerExecutable) { + return Files.exists(dockerExecutable); + } + + /** + * Gets a function that takes a {@code docker} subcommand and gives back a {@link ProcessBuilder} + * for that {@code docker} command. + * + * @param dockerExecutable path to {@code docker} + * @return the default {@link ProcessBuilder} factory for running a {@code docker} subcommand + */ + @VisibleForTesting + static Function, ProcessBuilder> defaultProcessBuilderFactory( + String dockerExecutable, ImmutableMap dockerEnvironment) { + return dockerSubCommand -> { + List dockerCommand = new ArrayList<>(1 + dockerSubCommand.size()); + dockerCommand.add(dockerExecutable); + dockerCommand.addAll(dockerSubCommand); + + ProcessBuilder processBuilder = new ProcessBuilder(dockerCommand); + Map environment = processBuilder.environment(); + environment.putAll(dockerEnvironment); + + return processBuilder; + }; + } + + private static String getStderrOutput(Process process) { + try (InputStreamReader stderr = + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { + return CharStreams.toString(stderr); + } catch (IOException ex) { + return "unknown (failed to read error message from stderr due to " + ex.getMessage() + ")"; + } + } + + /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ + private final Function, ProcessBuilder> processBuilderFactory; + + /** + * Instantiates with a {@code docker} executable and environment variables. + * + * @param dockerExecutable path to {@code docker} + * @param dockerEnvironment environment variables for {@code docker} + */ + public CliDockerClient(Path dockerExecutable, Map dockerEnvironment) { + this( + defaultProcessBuilderFactory( + dockerExecutable.toString(), ImmutableMap.copyOf(dockerEnvironment))); + } + + @VisibleForTesting + CliDockerClient(Function, ProcessBuilder> processBuilderFactory) { + this.processBuilderFactory = processBuilderFactory; + } + + @Override + public boolean supported(Map parameters) { + return true; + } + + @Override + public DockerInfoDetails info() throws IOException, InterruptedException { + // Runs 'docker info'. + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future readerFuture = executor.submit(this::fetchInfoDetails); + try { + DockerInfoDetails details = readerFuture.get(DOCKER_OUTPUT_TIMEOUT, TimeUnit.MILLISECONDS); + return details; + } catch (TimeoutException e) { + readerFuture.cancel(true); // Interrupt the reader thread + throw new IOException("Timeout reached while waiting for 'docker info' output"); + } catch (ExecutionException e) { + throw new IOException("Failed to read output of 'docker info': " + e.getMessage()); + } finally { + executor.shutdownNow(); + } + } + + @Override + public String load(ImageTarball imageTarball, Consumer writtenByteCountListener) + throws InterruptedException, IOException { + // Runs 'docker load'. + Process dockerProcess = docker("load"); + + try (NotifyingOutputStream stdin = + new NotifyingOutputStream(dockerProcess.getOutputStream(), writtenByteCountListener)) { + imageTarball.writeTo(stdin); + + } catch (IOException ex) { + // Tries to read from stderr. Not using getStderrOutput(), as we want to show the error + // message from the tarball I/O write failure when reading from stderr fails. + String error; + try (InputStreamReader stderr = + new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { + error = CharStreams.toString(stderr); + } catch (IOException ignored) { + // This ignores exceptions from reading stderr and uses the original exception from + // writing to stdin. + error = ex.getMessage(); + } + throw new IOException("'docker load' command failed with error: " + error, ex); + } + + try (InputStreamReader stdout = + new InputStreamReader(dockerProcess.getInputStream(), StandardCharsets.UTF_8)) { + String output = CharStreams.toString(stdout); + + if (dockerProcess.waitFor() != 0) { + throw new IOException( + "'docker load' command failed with error: " + getStderrOutput(dockerProcess)); + } + + return output; + } + } + + @Override + public void save( + ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) + throws InterruptedException, IOException { + Process dockerProcess = docker("save", imageReference.toString()); + + try (InputStream stdout = new BufferedInputStream(dockerProcess.getInputStream()); + OutputStream fileStream = new BufferedOutputStream(Files.newOutputStream(outputPath)); + NotifyingOutputStream notifyingFileStream = + new NotifyingOutputStream(fileStream, writtenByteCountListener)) { + ByteStreams.copy(stdout, notifyingFileStream); + } + + if (dockerProcess.waitFor() != 0) { + throw new IOException( + "'docker save' command failed with error: " + getStderrOutput(dockerProcess)); + } + } + + @Override + public DockerImageDetails inspect(ImageReference imageReference) + throws IOException, InterruptedException { + Process inspectProcess = + docker("inspect", "-f", "{{json .}}", "--type", "image", imageReference.toString()); + + try (InputStreamReader stdout = + new InputStreamReader(inspectProcess.getInputStream(), StandardCharsets.UTF_8)) { + String output = CharStreams.toString(stdout); + + if (inspectProcess.waitFor() != 0) { + throw new IOException( + "'docker inspect' command failed with error: " + getStderrOutput(inspectProcess)); + } + + return JsonTemplateMapper.readJson(output, DockerImageDetails.class); + } + } + + /** Runs a {@code docker} command. */ + private Process docker(String... subCommand) throws IOException { + return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); + } + + private DockerInfoDetails fetchInfoDetails() throws IOException, InterruptedException { + Process infoProcess = docker("info", "-f", "{{json .}}"); + + try (InputStreamReader stdout = + new InputStreamReader(infoProcess.getInputStream(), StandardCharsets.UTF_8)) { + String output = CharStreams.toString(stdout); + + if (infoProcess.waitFor() != 0) { + throw new IOException( + "'docker info' command failed with error: " + getStderrOutput(infoProcess)); + } + + return JsonTemplateMapper.readJson(output, DockerInfoDetails.class); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java deleted file mode 100644 index 1ffe8ae5aa..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.CharStreams; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - -/** Calls out to the {@code docker} CLI. */ -public class DockerClient { - - /** - * @param dockerSubCommand the subcommand to run after {@code docker} - * @return the default {@link ProcessBuilder} factory for running a {@code docker} subcommand - */ - private static ProcessBuilder defaultProcessBuilder(List dockerSubCommand) { - List dockerCommand = new ArrayList<>(1 + dockerSubCommand.size()); - dockerCommand.add("docker"); - dockerCommand.addAll(dockerSubCommand); - return new ProcessBuilder(dockerCommand); - } - - /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ - private final Function, ProcessBuilder> processBuilderFactory; - - public DockerClient() { - this(DockerClient::defaultProcessBuilder); - } - - @VisibleForTesting - DockerClient(Function, ProcessBuilder> processBuilderFactory) { - this.processBuilderFactory = processBuilderFactory; - } - - /** - * @return {@code true} if Docker is installed on the user's system and accessible as {@code - * docker} - */ - public boolean isDockerInstalled() { - try { - docker(); - return true; - - } catch (IOException ex) { - return false; - } - } - - /** - * Loads an image tarball into the Docker daemon. - * - * @see https://docs.docker.com/engine/reference/commandline/load - * @param imageTarballBlob the built container tarball. - * @return stdout from {@code docker}. - * @throws InterruptedException if the 'docker load' process is interrupted. - * @throws IOException if streaming the blob to 'docker load' fails. - */ - public String load(Blob imageTarballBlob) throws InterruptedException, IOException { - // Runs 'docker load'. - Process dockerProcess = docker("load"); - - try (OutputStream stdin = dockerProcess.getOutputStream()) { - try { - imageTarballBlob.writeTo(stdin); - - } catch (IOException ex) { - // Tries to read from stderr. - String error; - try (InputStreamReader stderr = - new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { - error = CharStreams.toString(stderr); - - } catch (IOException ignored) { - // This ignores exceptions from reading stderr and throws the original exception from - // writing to stdin. - throw ex; - } - throw new IOException("'docker load' command failed with error: " + error, ex); - } - } - - try (InputStreamReader stdout = - new InputStreamReader(dockerProcess.getInputStream(), StandardCharsets.UTF_8)) { - String output = CharStreams.toString(stdout); - - if (dockerProcess.waitFor() != 0) { - throw new IOException("'docker load' command failed with output: " + output); - } - - return output; - } - } - - /** Runs a {@code docker} command. */ - private Process docker(String... subCommand) throws IOException { - return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClientResolver.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClientResolver.java new file mode 100644 index 0000000000..025114013c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClientResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.docker; + +import com.google.cloud.tools.jib.api.DockerClient; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; + +public class DockerClientResolver { + + private DockerClientResolver() {} + + /** + * Look for supported DockerClient. + * + * @param parameters needed by the dockerClient + * @return dockerClient if any is found + */ + public static Optional resolve(Map parameters) { + ServiceLoader dockerClients = ServiceLoader.load(DockerClient.class); + for (DockerClient dockerClient : dockerClients) { + if (dockerClient.supported(parameters)) { + return Optional.of(dockerClient); + } + } + return Optional.empty(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java deleted file mode 100644 index 91bbafd809..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.docker; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.builder.EntrypointBuilder; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.io.MoreFiles; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Generates a Docker context that mimics how Jib builds the image. - * - *